Skip to content

JDGonzal/design-video-store

Repository files navigation

design-video-store

Course based on Youtube video with jotredev

Diseño UI tienda de videojuegos con React JS y Tailwind CSS totalmente responsivo

0a. Preconditions

  1. Install the NPM and NODEJS in your system Nodejs Download
  2. Check in $path or %path% the nodeJS and npm are on it
C:/Program Files/nodejs
  1. Install also pnpm pnpm installation, it is more fast than npm
  2. Install Visual Studio Code Visual Studio Download

0b. Starting the proyect

  1. I used Vite, the best way to start any front-end project, with Typescrypt and a lot of templates:
npm init vite@latest design-video-store --template react-ts
  1. Following the instructions, install the applications based on the package.json file.
pnpm install
  1. To activate tha Alias, check this page Setup path aliases w/ React + Vite + TS, the run this command:
pnpm i -D @types/node

Steps to configure the "@" alias

0c. Install Tailwind CSS and check

  1. Go the Tailwind CSS, and select "Framework Guides" option.
  2. Becasue I used the "Vite", select "Vite".
  3. Run this command of the 2 step "Install Tailwind CSS" option, in a terminal:
pnpm install -D tailwindcss postcss autoprefixer
  1. Run this process to Initialize the tailwind css or create the config Tailwind file:
npx tailwindcss init -p
  1. Add the paths into the content: [], to all of your template files in your "tailwind.config.js" file.
  content: [
    "./index.html",
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  1. Delete all code into "scr/index.css" file, to let only the @tailwind directives:
@tailwind base;
@tailwind components;
@tailwind utilities;
  1. Delete App.css" file.
  2. Inside the "App.tsx" delete all into the first <> element, below the return, and also delete all unused code, this is only code to let it:
function App() {
  return (
    <>
    </>
  )
}
export default App
  1. Become the <> element to <div> with a className:
      <div className="bg-red-400">
  1. And run the application.
pnpm dev

0d. Install React icons

  1. From the React Icons site, install the react icons:
pnpm install react-icons --save

Note: this is the goal to do in this process:

vide-store

1. Adding the "Header" or Top Navegation.

  1. Put a basic color in the "index.html" file using the <body>and adding a class:
<body class="bg-[#181A20]">
  1. Change the Title in "index,html" file:
<title>Video Games Store Online</title>
  1. Create a directory called "pages".
  2. Create a file "Home.tsx" and run rfce snippet. Remember to delete the first line, not require the import of react.
  3. Create a directory called "components", inside the "pages" directory.
  4. Create a file "Header.tsx" and run rfce snippet. Remember to delete the first line, not require the import of react.
  5. Create a "index.ts" file into "components", an put this info:
export { default as Header } from './Header';
  1. Add the import of Headerinto "Home.tsx" file:
import {Header} from './components';
  1. Add a "index.ts" file into "pages" directory, whit this info:
export { default as Home } from './Home';
export * from './components';
  1. Finally add the import of Home into "App.tsx" file:
  1. Call into the <div> element below return, the <Home/> element, this is the current "App.tsx" file:
import { Home } from "@/pages"
function App() {
  return ( <div><Home/></div> )
}
export default App
  1. Instead of "Home" in "Home.tsx" file add the <Header/> element.
  2. To the <div> element in "Home.tsx" add a className with some attributes:
    <div className="min-h-screen">
      <Header />
    </div>
  1. Change the <div> element of "Header.tsx" file by <Header> and add a className:
return <header className="text-gray-300">
  1. Instead of Header text change for <ul> and <li> elements.
  2. The element into <li> could be <a href ="#"> or <Link>, We use react-router-dom, then install it:
pnpm install react-router-dom
  1. Add an import into "App.tsx" file:
import { BrowserRouter, Routes, Route } from 'react-router-dom';
  1. In "App.tsx" file Add the BrowserRouter, and Routes components are arround the <Header />, and use the Route path= point to Home:
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />} />
      </Routes>
    </BrowserRouter>
  1. for each <li> use a Link to point to root.
    <header className="text-gray-400">
      <ul>
        <li> <Link to="/">Home</Link> </li>
        <li> <Link to="/">Streams</Link> </li>
        <li> <Link to="/">Games Store</Link> </li>
        <li> <Link to="/">News</Link> </li>
      </ul>
    </header>
  1. importthe Remix Icon into "Header.tsx" file, to use in the second <ul> element:
import { RiShoppingCartLine, RiHeart2Line } from "react-icons/ri";
  1. Copy this picture into "/src/assets" directory: avatar194938.png

  2. Add Other <ul> element to show the Icons at right:

      <ul>
        <li> <button><RiShoppingCartLine/></button> </li>
        <li> <button><RiHeart2Line/></button> </li>
        <li> <button><img src="/src/assets/avatar194938.png" className="w-8 h-8 object-cover rounded-full"/></button> </li>
      </ul>
  1. Add to <header a padding y of 4, padding x of 10, item center, flex , y justify between:
<header className="text-gray-400 py-4 px-10 flex items-center justify-between">
  1. Add a className to first <ul> element:
<ul className="flex items-center gap-6">
  1. Add a Hoverto each <li> element, using two variables:
const orangeColor = 'text-[#E58D27] transition-colors';
  const hoverColor = "hover:"+orangeColor;
  1. Add a className to the second <ul>:
<ul className="flex items-center gap-6 text-xl">
  1. Add the same className to the each <button>.

2a. Sidebar or Left hand navigation Menu

  1. Add two component into '/pages/components' called "Sidebar.tsx" and "Content.tsx" files.
  2. run the rafce, delete the first line, and update the "index.ts" file.
  3. Add in "Home.tsx" file the components <Sidebar/>, and <Content/> below the Header into a <div>:
    <div className="min-h-screen">
      <Header />
      <div className="h-[90vh] p-8">
      <Sidebar/>
      <Content/>
      </div>
    </div>
  1. Need to add the 10% missing in "Header.tsx" file in the <header element:
 <header className={"h-[10vh] text-gray-400 py-4 px-10 flex items-center justify-between "+darkBrown}>
  1. Add a className into the <div> of "Sidebar.tsx" file:
<div className="w-64 h-full overflow-y-scroll">
  1. Add a className into the <div> of "Content.tsx" file, to expand the rest to the right:
<div className=" flex-1 h-full overflow-y-scroll">
  1. Add two <div> into "Sidebar.tsx" file.
  2. Add in the first <div> element, a <h4>, and more elements to show checkbox:
      <div className={"bg-" + brownColor + " rounded-xl p-4"}>
        <h4>Categories</h4>
        <div className="flex items-center gap-4">
          <input type="checkbox" id="indy" />
          <label htmlFor="indy">Indy</label>
        </div>
      </div>
  1. Repeat the <div> of each check box 7 times more to complete this list: Indy, Adventure, MMO, Casual Game, Strategy, Simulator, Sports Game, Action Game.
  2. Below the <h4> add <div> arround all <input checkbox type.
  3. Add to this <div> a className:
<div className="flex flex-col gap-2">
  1. Add another <h4> with Paltforms
<h4 className="my-6">Platforms</h4>
  1. Repeat the <div> of each check box times more to complete this list: PC, PlayStation 5, PlasyStation 4, Xbox Series, Nintendo Switch.
  2. Copy another <h4> for Price:
<h4 className="my-6">Price</h4>
  1. Add a <form> to include the <input> element with only numbers:
        <form action="submint">
          <div>
            <input type="number" className={"bg-"+darkBrown+" py-2 px-4 rounded-xl"}/>
            </div>
        </form>
  1. Add a Dolar symbol as an icon and put into the <input>:
          <div className="relative">
            <RiMoneyDollarCircleLine className="absolute left-2 top-1/2 -translate-y-1/2 text-xl" />
            <input
              type="number"
              className={
                "bg-" + darkBrown + " py-2 pl-8 pr-4 rounded-xl w-full"
              }
            />
          </div>
  1. Add an <span>, and duplicate the <input type="number"
  2. Add a className to the <form>:

2b. Side bar the Filter button and Social Media

  1. When start the app, the colors don't appear then remove the import from "colors.ts" file , and put manually the colors to each element.
  2. Add a <button> just over the </form> in "Sidebar.tsx" file, with a type="submit":
        <button type="submit"> </button>
  1. Put a <div> arround the both <div className="relative">, and add a className, movirn form the <form>:
        <form>
          <div className="flex items-center justify-between gap-2">
            <div className="relative">
              ...
            </div>
            <span>-</span>
            <div className="relative">
             ...
            </div>
          </div>
          <button type="submit">Apply Filters</button>
        </form>
  1. Add to the <form> a new className:
        <form className="flex flex-col gap-6">
  1. Add a className to the <button>:
<button type="submit" className="bg-[#E58D27] text-black rounded-full w-full p-2">Apply Filters</button>
  1. Add to the three <h4> in className:
      <h4 className="my-6 text-white text-lg">
  1. Add a hover: to the <button>, into the className:
hover:-translate-y-1 transition-all duration-200
  1. Adding the Social Media:
<ul className="flex items-center justify-between">
  <li><a href="https://www.twitter.com" target="_blank" className="text-2xl"><RiTwitterLine/></a> </li>
  <li><a href="https://www.instagram.com" target="_blank" className="text-2xl"> <RiInstagramLine/></a> </li>
  <li><a href="https://www.youtube.com" target="_blank" className="text-2xl"><RiYoutubeLine/> </a> </li>
  <li><a href="https://www.facebook.com" target="_blank" className="text-2xl"><RiFacebookLine/> </a> </li>
</ul>

3. Complete the Content or Right side.

  1. Add a className to check box:
className="accent-[#E58D27]"
  1. Add a Picture <img> into "content.tsx" file.
  2. Add a "Card.tsx" file.
const Card = (props:{ img:string, title:string, category:string, price:string }) => {
  return (
    <div className="bg-[#362C29]/50 p-6 rounded-2xl flex flex-col gap-2">
      {" "}
      <img
        src={props.img}
        className="w-52 h-52 object-cover rounded-2x1"
      />
      <h1 className="text-xl text-white">{props.title}</h1>
      <span className="text-gray-400">{props.category}</span>
      <div className="flex items-center gap-4">
        <h5 className="text-3xl text-[#E58D27]">${props.price}</h5>
        <button
          className="bg-[#E58D27] text-black font-bold rounded-full w-full p-3 hover:-translate-y-1 transition-all
duration-200"
        >
          Buy
        </button>
      </div>
    </div>
  );
};
  1. Call this car into "content.tsx" file:
        <Card
          img="https://image.api.playstation.com/vulcan/img/rnd/202011/0714/vuF88yWPSnDfmFJVTyNJpVwW.png"
          title="Marvel's Spider-Man"
          category="PS5"
          price="51"
        />
  1. All the <Card ar into a <div> with className, and the very important there is flex-wrap:
    <div className="flex items-center justify-between flex-wrap gap-8">

4a. Responsive the First Top-Menu or Header

  1. Add a <button> with this RiMenu2Line icon in "Header.tsx" file over the First Menu:
<button className="lg:hidden"><RiMenu2Line/></button>
  1. Add a hidden to the First Menu in "Header.tsx" file:
<ul className="hidden lg:flex items-center gap-6">
  1. Add a Menu Mobile, below the first <button>:
      <div className="fixed left-0 top-0 w-full h-full z-50 bg-[#181A20]">
        <ul>Menu Mobile</ul>
      </div>
  1. Add useState like this:
const [showMenu, setShowMenu ] = useState(false);
  1. The <button> Activate or not the menu, and the menu appear based on the showMemenu status:
      <button onClick={()=> setShowMenu(!showMenu)} className="lg:hidden">
        <RiMenu2Line />
      </button>
      <div className={`fixed left-0 w-full h-full z-50 bg-[#181A20] ${showMenu?"top-0":"-top-full"} transition-all`}>
        <ul>Menu Mobile</ul>
      </div>
  1. Add a <button> over the <ul>Mobile Menu:
        <button onClick={()=> setShowMenu(!showMenu)}><RiCloseLine/></button>
  1. Center the items of Menu only in new <ul> adding a className:
<ul className=" w-full mt-20">
  1. For the Menu add some elements to className, as block text-center p-4 text-4xl.

4b. Responsive the Sidebar

  1. Put some elements in the className of first <div> in "Sidebar.tsx" file, to hide when is mobile, and only shows in PC:
<div className="w-[80%] fixed lg:static top-0 -left-full lg:w-80 h-full overflow-y-scroll text-gray-400 bg-[#181A20] transition-all duration-200 p-3">
  1. Add a <> element and move all inside this element
  2. Add a <button> with RiFilterLine icon:
      <button className="text-lg fixed bottom-4 right-4 bg-[#E58D27] rounded-full z-40 lg:hidden"><RiFilterLine/></button>
  1. Add an useState to show the Filter:
const [showSidebar, setShowSidebar] = useState(false);
  1. Use the condition in the first <div>:
<div className={`w-[80%] fixed lg:static top-0 ${showSidebar?"left-0":"-left-full"} lg:w-80 h-full overflow-y-scroll text-gray-400 bg-[#181A20] transition-all duration-200 p-3`}>
  1. the last <button> add a condition to show the RiCloseLine or RiFilterLine :
{showSidebar? <RiCloseLine/>:<RiFilterLine/>}
  1. for the 'IPad Air' device add className of first <div>the use of md:
      `... md:w-[40%] ...`

4c. Responsive the elemens in Content

  1. Change className elements of first image in "Content.tsx" file:
        className="w-full h-[500px] object-cover object-right md:object-top rounded-2xl"

Note: Better object-cover than object-container.

  1. Change in "Cart.tsx" file the first <div>:
    <div className="bg-[#362C29]/50 p-6 rounded-2xl flex flex-col gap-2 w-full md:w-auto">
  1. Change in classNameof <div> container of all cards the justify-between by justify-arround in "Content.tsx" file:
      <div className="flex md:grid md:grid-cols-2 lg:flex items-center justify-around  lg:justify-between flex-wrap gap-8">
  1. Change in "Cards.tsx" file in <img> some elements in the className:
      <img
        src={props.img}
        className="w-full lg:w-52 h-72 lg:h-52 object-cover rounded-2xl"
      />
  1. Adding to className of "Sidebar.tsx" file this lg:p-0.
  2. Changes to classNameof "Sidebar.tsx" file in first <div>:
    <div className={`w-[80%] md:w-[40%] fixed lg:static top-0 ${showSidebar?"left-0":"-left-full"} lg:w-80 h-full overflow-y-scroll lg:overflow-y-auto text-gray-400 bg-[#181A20] transition-all duration-200 p-3 lg:p-0`}>

5a. Adding "Login" Component with basic elements

  1. Create in "pages" directory, two new directories: Home and Login.
  2. Move the "Home.tsx" file to the "Home" directory.
  3. Create a "Login.tsx" file into "Login" directory, run the rfce snippet, delete first line.
  4. Create a "components" directory into "src" directory and move the "Colors.ts" and "Header.tsx" files.
  5. Into the each directory of "pages" create a "components" directory for the exclusives components for each page.
  6. Add or Update the "index.ts" file into each directory.
  7. Add a "AuthWrapper.tsx" fiel into "components of root, run the rfce snippet, delete first line.
  8. Add this code to "AuthWrapper.tsx" file:
const AuthWrapper=(props: {
  isAuthenticated:boolean;}
  ) => {
  return props.isAuthenticated ? (
    <Navigate to="/home" replace />
  ) : (
    <Navigate to="/login" replace />
  );
}
  1. Changes in "App.tsx" file like this, the first page to show will be "login" while the isAuthnticaded keeps on false:
function App() {
  const isAuthenticated = false;
  return (
    <BrowserRouter>
      <Routes>
        <Route
          path="/"
          element={<AuthWrapper isAuthenticated={isAuthenticated} />}
        />
        <Route path="/home" element={<Home />} />
        <Route path="/login" element={<Login />} />
      </Routes>
    </BrowserRouter>
  );
}
  1. The "Login.tsx" file add a <form>.
  2. Add in <form> the some <label>'s, <input>'s, and <button>'s.' :
        <form onSubmit={handleSubmit} className="w-[40vh] rounded-xl p-4 m-8 bg-slate-200 items-center text-center">
          <h4>Inicio Sesión"</h4>
          <div>
            <label htmlFor="">Correo</label>
            <input type="text" />
          </div>
          <div>
            <label htmlFor="">Contraseña</label>
            <input type="password" />
          </div>
          <div>
            <button type="submit">Iniciar Sesión</button>
          </div>
          <div>
            <button>No tiene una cuenta, Registrarse Aquí</button>
          </div>
        </form>
  1. Add some className with Tailwind CSS elements.
  2. Add useStateto show the Registry options:
const [showRegistry, setShowRegistry] = useState(false);
  1. Use this useState in the last <button>.
    <button className="..." onClick={() => setShowRegistry(!showRegistry)}>
  1. To the <form> add a onSubmit={handleSubmit}, and create the function:
const handleSubmit = (e: any) => {
    e.preventDefault(); // Avoid page refreshing.
  };
  1. Add some Elements when showRegistry is in true. that keeps hide some elements.e.g.:
          <div
            className={`${
              showRegistry ? "visible" : "hidden"
            } ...`}
          >
            <label>Confirmar contraseña</label>
            <input
              className="rounded-md" type="password"
              placeholder="Confirmar contraseña"
            />
          </div>

5b. The Login-Email first component from Login

  1. Create a "components" directory into "Login" directory.
  2. Create a "Login-Email.tsx" file, run the rfce snippet and delete the first line.
  3. Move all regarding email from "Login.tsx" to the "LoginEmail.tsx" component and call the new component <LoginEmail/> into "Login.tsx" file.
  4. To become a field with label as a Mandatory, need:
  • Add in "index.css" file in the root, this:
.form-group.required .control-label:after { 
  content:"*";
  color:red;
}
  • The Component add some elements in className of <label/>, e.g.:
      <label htmlFor="" className="control-label">
        Correo
      </label>
  • To the <input> add a required value
      <input ... required={true} />
  1. Play with a useState to send a function can be executed in a Children component, in this case is "Login-Email.tsx" file.

6. Install Redux and share info to validate fields

  1. Install Redux Toolkik an React-Redux
pnpm install @reduxjs/toolkit react-redux
  1. Create a "models" directory in the root ("src/models").
  2. Add a "validation.model.ts" file wih this information:
export interface ValidationListableInterface{
  id: string;
  value: string;
  type: ValidationType;
  isValid: boolean;
  isVisible:boolean;
  message?: string;
}
export enum ValidationType{
  String_ = "string",
  Number_ = "number",
  Boolean_ = "boolean",
}
  1. Create a "redux" directory, and put there the "store.ts" file, with those lines:
import { configureStore } from "@reduxjs/toolkit";
export default configureStore({});
  1. Create a "states" directory into "redux" directory, and create the "validationsSlice.ts" file, with this inside:
import { createSlice } from '@reduxjs/toolkit';
import { ValidationListableInterface } from '@/models';
const initialState: ValidationListableInterface[] = [];
export const validationsSlice = createSlice({
  name: 'validation', initialState: initialState, 
  reducers:{ }
});
export default validationsSlice.reducer;

Note: Always create the "index.ts" in each directory file and update the barrels.

  1. Update the "store.ts" file and it is the expected result:
import { configureStore } from "@reduxjs/toolkit";
import { validationsSlice } from "./states";
import { ValidationListableInterface } from "@/models";
export interface AppStore {
  validations: ValidationListableInterface[];
}
export default configureStore<AppStore>({
  reducer: {
    validations: validationsSlice.reducer,
  }
});
  1. In the main file "App.tsx" add a Provider to include all the others
import { Provider } from "react-redux";
  1. Import the store from "store.ts", in "App.tsx" file:
import store from "./redux/store";
  1. Finally I change the return of "App.tsx" file like this:
    return (
      <Provider store={store}>
      ...
      </Provider>
    );
  1. Add in "validationsSlice.ts" a reducers with the name: addValidation, like this:
    addValidation: (state, action) => {
      const validationFound = state.find(validation => validation.id === action.payload.id);
      if (!validationFound) {
        state.push(action.payload);
      }
    }
  1. Add this addValidation in "Login-Email.tsx" file in a useEffect, to run only once:
  useEffect(() =>{
    dispatch( addValidation({ 
      id: "email",
      value: email,
      type: "string",
      isValid: false,
      isVisible: props.isVisible,
      message: "Email",
    }));
  },[]);
  1. Create an useState in "Login-Email.tsx" file, to control de changes on the field:
const [email, setEmail] = useState("");
  1. Add a function called handleChange to update the email uf the useState:
  const handleChange = (e: any) => {
    setEmail(e.target.value);
  };
  1. Call this function in the <input> element using onChange={handleChange}.
  2. Add in "validationsSlice.ts" a reducers with the name: updateValidation, like this:
    updateValidation: (state, action) => {
      const { id, value, isValid } = action.payload;
      const validationFound = state.find(validation => validation.id === id);
      if (validationFound) {
        validationFound.value = value;
        validationFound.isValid = isValid;
      }
    }
  1. Add a function called handleBlur to validate the email and feed validations with updateValidation in "Login-Email.tsx" file:
  const handleBlur = async (e: any) =>{
    const isOk = await /^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$/g.test(e.target.value);
    dispatch( updateValidation({ 
      id: "email",
      value: email,
      isValid: isOk,
    }));
  }
  1. Call this function in the <input> element using onBlur={handleBlur}.
  2. Just for validate in the handleSubmit of "Login.tsx" file, to show the validation:
  const validations = useSelector((state: AppStore) => state.validations);
  const handleSubmit = (e: any) => {
    e.preventDefault(); // Avoid page refreshing.
    console.log('Validations:', validations);
  };

7. Adding a Login-Password as new Component

  1. Copy the "Login-Email.tsx" in "Login-Password.tsx".
  2. Rename in "Login-Password.tsx" the LoginEmail by LoginPassword.
  3. Renamame all Email by Password and email by password.
  4. Move all the Information from "Login.tsx" to "Login-Password.tsx" regarding the Password.
  5. Add two useState for strengthBadge, and backgroundColor.
  6. Create two Regular Expressions:
  const strongPassword = new RegExp(
    "(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[^A-Za-z0-9])(?=.{8,})"
  );
  const mediumPassword = new RegExp(
    "((?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[^A-Za-z0-9])(?=.{6,}))|((?=.*[a-z])(?=.*[A-Z])(?=.*[^A-Za-z0-9])(?=.{8,}))"
  );
  1. Add a function to use the Regular Expresions and feed the strengthBadge and backgroundColor:
 const strengthChecker = async (password: string) => {
    let isValid = false;
    if (await strongPassword.test(password) && password.length >= 10) {
      setbackgroundColor(" bg-green-700");
      setstrengthBadge("Fuerte");
      isValid=true;
    } else if ( await mediumPassword.test(password) && password.length >= 6) {
      setbackgroundColor(" bg-orange-500");
      setstrengthBadge("Medio");
      isValid=true;
    } else {
      setbackgroundColor(" bg-red-700");
      setstrengthBadge("Débil");
    }
    return isValid;
  };
  1. Call in Login the New Component:
    <LoginPassword isVisible={showRegistry}/>

8. Adding the Component called Login-MedicalCenter

  1. Copy the "Login-Email.tsx" in "Login-MedicalCenter.tsx".
  2. Rename in "Login-MedicalCenter.tsx" the LoginEmail by LoginMedicalCenter.
  3. Renamame all Email by MedicalCenter and email by medicalCenter.
  4. Move all the Information from "Login.tsx" to "Login-Password.tsx" regarding the MedicalCenter.
  5. Create a Model called "medical-center.model.ts" with this structure (update the "intex.ts" or the barrel, after this one):
export interface MedicalCenterableInterface{
  id: number;
  name: string;
  address: string;
  telephone: number;
  stateId:number;
  cityId: number;
}
  1. Because the Medical Center is a JSON, create a initialValue, to use in the useState:
  const initialState: MedicalCenterableInterface = {
    id: 0, name: "",
    address: "", telephone: 0,
    stateId: 0, cityId: 0,
  };
  const [medicalCenter, setMedicalCenter] = useState(initialState);
  const dispatch = useDispatch();
  1. For handleChange just add the id in this medicalCenter object:
  const handleChange = async(e: any) => {
    setMedicalCenter({
      ...medicalCenter, id: parseInt(e.target.value),
    });
  };

9. The Medical Center Component adding data from the API

This API added here in the future will be moved to the other Component. This is for fast test used.

1 Create two files ".env", you could based on this Generic dotenv Format, or in our case using the React Custom Environment Variables. Put both files in the root, even before the "src" directory

dotenv files in root 2. The names of Environment Variables must start with VITE_. 3. Create a directory in the root, called "src/utilities" 4. Inside this "utilities" create a File called "env.utility.ts" file, using the data from ".env" file:

export const{VITE_API_URL, VITE_PHOTO_URL} = import.meta.env;

Note: The ".env" or ".env.xxx" file never uploads to the repository and is never exposed in any document, it is totally secret.

  1. Add a const in "Login-MedicalCenter.tsx" file as siteMedicalCenter:
const siteMedicalCenter = "medicalcenter";

6 Create a function refreshMedicalCenters() to use the API information. 7. Modify the "medical-center.model.ts" file based on the API answer:

export interface MedicalCenterableInterface{
  id:                     number;
  ok:                     boolean;
  found:                  number;
  medicalCenterName:      string;
  medicalCenterAddress:   string;
  medicalCenterTelNumber: number;
  StateStateId:           number;
  CityCityId:             number;
}
  1. Add a useRef to control the first time of useEffect
const isFirstTime = useRef(true);
  1. Change the useEffect to be pending for Add or Update the Validations:
  useEffect(() => {
    if (isFirstTime.current) {
      isFirstTime.current = false;
      dispatch(
        addValidation({
          id: "medicalCenter",
          value: medicalCenter,
          type: ValidationType.Json_,
          isValid: false,
          isVisible: props.isVisible,
          message: "MedicalCenter",
        })
      );
    } else {
      dispatch(
        updateValidation({
          id: "medicalCenter",
          value: medicalCenter,
          isValid: isOkMedicalCenter,
          isVisible: props.isVisible,
        })
      );
    }
  }, [dispatch, medicalCenter, props.isVisible, isOkMedicalCenter]);
  1. In the future complete the Fields based on the API answer and let to select the state(Departamento) and the city(Ciudad), verify the Password and the UserType (Clínica, Laboratorio, Administrador).

10. Lets to apply some Clean Architecture

  1. Changing the way to start or run the application in the"scripts" of "package.json" file:
 "scripts": {
    "local": "vite",
    "staging": "vite --mode staging",
    "dev": "vite --mode dev",
    "prod": "vite --mode prod",
    ...}

Note: For this changes the files ".env" files in the root, must call ".env.dev" and ".env.prod".

  1. Move the fetch process form "Login-MedicalCenter.tsx" component to a new "api-service" directory into the file called: "medicalCenter.service.ts" :
import { VITE_API_URL } from "@/utilities";
const site = 'medicalcenter';
export const getMedicalCenter = async (id: number) => {
  const apiUrl = await `${VITE_API_URL}${site}/medicalcentername/${id}`;
  if (await !isNaN(id)) {
    let response: any = {};
    try { console.log('getMedicalCenter:', apiUrl);
      const result = await fetch(apiUrl, {
        method: "GET",
        headers: { Accept: "application/json",
          "Content-Type": "application/json", }, });
      response = await result.json();
    } catch (err) { console.log(err);
      return err; }
    return response;
  }
}
  1. Add an adapter in "adapters" directry, called "medicalCenter.adapter.ts", like this:
import { MedicalCenterableInterface } from "@/models";
export const createMedicalCenterAdapter = (data: any): MedicalCenterableInterface => ({
  id: 0,
  ok: data.ok,
  found: data.found, nh
  name: data.medicalCenterName,
  address: data.medicalCenterAddress,
  phone: data.medicalCenterTelNumber,
  stateId: data.StateStateId,
  stateName: '',
  cityId: data.CityCityId,
  cityName: '',
});
  1. Change again the "medical-center.model.ts" file:
export interface MedicalCenterableInterface{
  id:         number;
  ok:         boolean;
  found:      number;
  name:       string;
  address:    string;
  phone:      number;
  stateId:    number;
  stateName:  string;
  cityId:     number;
  cityName:   string;
}
  1. Changes in the "Login-MedicalCenter.tsx" component are in the refreshMedicalCenters method, the first part is the use of the getMedicalCenter service and the adapter:
const refreshMedicalCenters = async () => {
    await setIsNewMedicalCenter(0);
    await getMedicalCenter(medicalCenter.id).then((data: any) => {
      console.log("data:", data);
      if (data && data.ok) {
        const adapded = createMedicalCenterAdapter(data);
        adapded["id"] = medicalCenter.id;
        setMedicalCenter({ ...adapded });
        setIsNewMedicalCenter(data.found);
      }
  1. Next in refreshMedicalCenters method, is the error control, using a new component called "BannerAlert.tsx" (Using somethig better than an simple alert)
      if (data === undefined) {
        console.log("undefined");
      } else {
        if (!data.ok) {
          dispatch( createAlert({ title: "Error",
              message:
                "Se ha presentado una falla.\nPor favor avisarle al administrador",
              textColor: "text-red-500", background: "bg-yellow-300",
              timeout: 5000, isVisible: true,
            }) );
        } }
    }) };
  1. Add a Model called "banner-alert.model.ts" file:
export interface BannerAlertableInterface{
  title: string;
  message: string;
  textColor: string;
  background: string;
  timeout: number;
  isVisible:boolean;
}
  1. Add a new slice to control elements of the new "BannerAlert" component, called "bannerAlertSlice.ts":
import { createSlice } from '@reduxjs/toolkit';
import { BannerAlertableInterface } from '@/models';
const initialState: BannerAlertableInterface = {
  title: '', message: '',
  textColor: '', background: '',
  timeout: 0, isVisible: false,
};
export const bannerAlertSlice = createSlice({
  name: 'bannerAlert',
  initialState: initialState,
  reducers: {
    createAlert: (_state, action) => action.payload,
    modifyAlert: (state, action) => ({ ...state, ...action.payload }),
    resetAlert: () => initialState,
  }
});
export const { createAlert, modifyAlert, resetAlert } = bannerAlertSlice.actions;
export default bannerAlertSlice.reducer;
  1. Add the new slice to the "store.ts" file:
export interface AppStore {
  validations: ValidationListableInterface[];
  bannerAlert: BannerAlertableInterface;
}
export default configureStore<AppStore>({
  reducer: {
    validations: validationsSlice.reducer,
    bannerAlert: bannerAlertSlice.reducer,
  }
});
  1. Create a Component in the "src/components" directory, called "BannerAlert.tsx" with this simple data:
import { AppStore, resetAlert } from "@/redux";
import { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
function BannerAlert() {
  const bannerAlert = useSelector((state: AppStore) => state.bannerAlert);
  const dispatch = useDispatch();
  useEffect(() => {
    const timer = setTimeout(() =>{ dispatch(resetAlert());}, bannerAlert.timeout);
    return()=>clearTimeout(timer);
  }, [bannerAlert, dispatch]);

  return (
    <div
      className={`${bannerAlert.background} bordermedicalCenter.data-t border-b border-blue-500 ${bannerAlert.textColor} px-4 py-3 fixed 
      ${bannerAlert.isVisible ? "left-0" : "-left-full"}`} role="alert"
    >
      <div className="justify-between flex">
        <p className="font-bold">{bannerAlert.title}</p>
        <button onClick={() => dispatch(resetAlert())}>X</button>
      </div>
      <p className="text-sm">{bannerAlert.message}</p>
    </div>
  );
}

export default BannerAlert;

11. Improvement: The Banner-Alert, only must show in real fail, pending for solution. (Just Put the Error detection at first in the Method.)

11. For Login-MedicalCenter.tsx to show the "Departamento" and "Municipio"

  1. Create two new adapters for City and Estado (state or Deparment).
  2. Create two new services for City and Estado (state or Deparment).
  3. Create two new models for City and Estado (state or Deparment).
  4. Create two new slices for City and Estado (state or Deparment).

Note: Normaly for Deparment or State I could use "state" but it can generate issues, then it was changes by "Estado" (spanish).

  1. Moving the basic Alert message to the "utilities" directory.
  2. Add the interface from models and the reducer from Slice in "states" directory to "store.ts" file.
  3. Changes in refreshMedicalCenters method.
  4. Adding a useEffect in "Login.tsx" file.
  5. This useEffect will fill the Estados and Cities.

12. Control of the "Departamento" and "Ciudad" if the Medical Center is New

  1. Add an Utility, to control all the data from UI by <Input> or <select>, to get the value or number or string, called "valueType.utility.ts".
  2. Add a new component called "FeedTables.tsx" just to complete the data of estadosList and citiesList, use that in "App.tsx" file.
  3. Move all data for creation of List based on "Cities" and "States" from "Login.tsx" to the new "FeedTables.tsx".
  4. The Estado Model get a big change to put before the array the data I can search:
export interface StatesListableInterface{
  estadoId:    number;
  estadoName:  string;
  estadosList: StatesListableInterface[];
}
export interface StatesListableInterface{
    estadoId:    number;
    estadoName:  string;
}
  1. Same situation for the City Model, the change to show the data I can search:
export interface CitiesListableInterface{
  cityId:     number;
  cityName:   string;
  estadoId:   number;
  citiesList: CityListableInterface[]; 
}
export interface CityListableInterface{
  cityId:     number;
  cityName:   string;
  estadoId:   number;
}
  1. For adapters just to add the type of each origin value.
  2. Adding all the elements to show when the user is new, as "Departamento" (State) and "Municipio" (City):
      <div className="flex flex-col-2 gap-1">
        <select
          className="w-[50%] form-select" placeholder="Departamento"
          value={medicalCenter.stateId} name={"stateId"}
          onChange={handleChange} onBlur={handleBlur}
        >
          <option hidden defaultValue="0" key="0">
            Departamento
          </option>
          {estadosList.estadosList.map((estado) => (
            <option value={estado.estadoId} key={estado.estadoId}>
              {estado.estadoName}
            </option>
          ))}
        </select>
        <select
          className="w-[50%]" placeholder="Municipio"
          value={medicalCenter.cityId} name={"cityId"}
          onChange={handleChange} onBlur={handleBlur}
        >
          <option hidden defaultValue="0" key="0">
            Municipio
          </option>
          {citiesList.citiesList.map((city) =>
            city.estadoId === medicalCenter.stateId ? (
              <option value={city.cityId} key={city.cityId}>
                {city.cityName}
              </option> ) : ( false )
          )}
        </select>
      </div>
  1. The handleChange method, let to manage all fields, no matther the type:
  const handleChange = async (e: any) => {
    console.log(e.target.name, e.target.type);
    setMedicalCenter({
      ...medicalCenter,
      [e.target.name]: valueTypeUtility(e.target.value,e.target.type),
    });
    if ((await e.target.name) === "id") {
      isOkMedicalCenter = String(e.target.value).length >= 6;
      if (isOkMedicalCenter) await refreshMedicalCenters(valueTypeUtility(e.target.value,e.target.type));
    }
  };
  1. The handleBlur method, let to manage all field and doing some validation by switch:
  const handleBlur = async (e: any) => {
    isOkMedicalCenter = String(e.target.value).length >= 6;
    switch (await e.target.name) {
      case "id":
        if (isOkMedicalCenter) await refreshMedicalCenters(valueTypeUtility(e.target.value,e.target.type)); else setLastMedicalCenter(0);
        break;
      case "stateId":
        dispatch(getMainEstado(valueTypeUtility(e.target.value,e.target.type)));
        setMedicalCenter({
          ...medicalCenter,
           stateName: estadosList.estadoName,
        });
        break;
      case "cityId":
        dispatch(getMainCity(valueTypeUtility(e.target.value,e.target.type)));
        setMedicalCenter({
          ...medicalCenter,
           cityName: citiesList.cityName,
        });
        break;
      default:
        console.log("end");
        break;
    }
  };
  1. The Estados Slice, changes based on the new model and adding two new reducers, called: setMainEstado, and getMainEstado:
  ...
  reducers: {
    ...
    setMainEstado:( state, action) =>{
      const { estadoId, estadoName } = action.payload;
      state.estadoId = estadoId;
      state.estadoName = estadoName;
    },
    getMainEstado: (state, action) =>{
      const estadoId = action.payload;
      const estadoFound = state.estadosList.find(estadoList => estadoList.estadoId === estadoId);
      if (estadoFound) {
      state.estadoId = estadoFound.estadoId;
      state.estadoName = estadoFound.estadoName;
      }
    },
  }
  1. The same for Cities Slice, after the model change and the new reducers:
  ...
  reducers: {
    ...
    setMainCity:( state, action) =>{
      const { cityId, cityName, estadoId } = action.payload;
      state.cityId = cityId;
      state.cityName = cityName;
      state.estadoId = estadoId;
    },
    getMainCity: (state, action) =>{
      const cityId = action.payload;
      const cityFound = state.citiesList.find(cityList => cityList.cityId === cityId);
      if (cityFound) {
      state.cityId = cityFound.cityId;
      state.cityName = cityFound.cityName;
      state.estadoId = cityFound.estadoId;
      }
    },
  }

13. Change the "isVisible" as a props, to a "rxjs" manager.

  1. Install the rxjs from the Installation Instructions
pnpm install rxjs @reactivex/rxjs
  1. Check the Example in Sharing between components
  2. Create a Service called "data-shared.service.ts" file:
    const subject = new Subject();
    export const dataSharedService = {
      setDataShared: (value: any) => subject.next(value),
      clearDataShared: () => subject.next(null),
      getDataShared: () => subject.asObservable()
    };
  1. The component to set or feed the data is "Login.tsx", like this, remember showRegistry has a useState:
    useEffect(() => {
      dataSharedService.setDataShared(showRegistry);
    }, [showRegistry]);
  1. The other componets get the info based on suscribe:
    useEffect(() => {
      dataSharedService.getDataShared().subscribe( data =>{
        if(data!==null)setIsVisible(data as boolean);
      });
  1. Eliminate the props and change by useState, like this:
  const LoginMedicalCenter = () => {
    ...
    const [isVisible, setIsVisible] = useState(false);
  1. I don't do the same for the other components LoginEmail and LoginPassword, because the isVisible is always true, then I keep de props.

14. Using the "rxjs" package dividing the big Login-MedicalCenter Component

  1. Create a "Login-MedicalCenter-id.tsx" file and move all regarding from "Login-MedicalCenter.tsx" Component.
  2. Create a "Login-MedicalCenter-StateNCity.tsx" file and move all regarding from "Login-MedicalCenter.tsx" Component.
  3. Create a "medicalCenter.service.ts" file and use the rxjs package:
    import { MedicalCenterInitial, MedicalCenterableInterface } from '@/models';
    import { Subject } from 'rxjs';
    const subject = new Subject();
    export const medicalCenterService = {
        setMedicalCenter: (value: MedicalCenterableInterface) => subject.next(value),
        clearMedicalCenter: () => subject.next(MedicalCenterInitial),
        getMedicalCenter: () => subject.asObservable()
    };
  1. all the Components get and set the new rxjs value, in the useEffect hook:
    medicalCenterService.getMedicalCenter().subscribe((data) => {
      if (data) setMedicalCenter(data as MedicalCenterableInterface);
    });
    ...
    medicalCenterService.setMedicalCenter(medicalCenter);
  1. Before the normal setMedicalCenter from useState hook, call the medicalCenterService.getMedicalCenter().
  2. in the "Login-MedicalCenter-Id.tsx" file for the refreshMedicalCenters method, add to complete the stateName and cityName, options:
          if (adapted.cityId > 0 && adapted.stateId > 0) {
            dispatch(getMainEstado(adapted.stateId));
            adapted['stateName']= estadosList.estadoName;
            dispatch(getMainCity(adapted.cityId));
            adapted['cityName']= citiesList.cityName;
          }
  1. Some improvements in the "medicalCenter.adapter.ts" to validate each field or return a default value.

15. Adding Two new Components "LoginConfirmPassword" and "LoginUserType"

  1. Create both New Compononents.
  2. Back to the props element because the rendering time was so long using the rxjs for isVisisble variable.
  3. Keeps the medicalCenter.service using the rxjs package.

16. Improvement to the Fetch Process.

  1. Create a "anyFetch.utility.ts" file, to feed with some paramters the mor utils info and the method:
    export const anyFetchUtility = (method:methodType, token?: string, abort?: any ): RequestInit => {
      return {
        method: method,
        headers: {
          'Accept': 'application/json',
          'Content-Type': 'application/json',
          'x-auth-token': ((token)?token: ''),
          'signal': ((abort)?abort:null),
        }
      }
    }
    export enum methodType{
      Get = "GET",
      Post = "POST",
      Delete = "DELETE",
    }
  1. Create a "api-answer.model.ts" file, to control the answer of the fetch process:
    export interface ApiAnswerableInterface{
      loading: boolean;
      error: any;
      data: any;
      abort: any;
    }
    export const ApiAnswerInit: ApiAnswerableInterface={
      loading: true,
      error: null,
      data: null,
      abort: null,
    }
  1. Create a "anyFetch.service.ts" file, with the process for any api-url:
    import { ApiAnswerInit, ApiAnswerableInterface } from "@/models";
    export const anyFetch = async (apiUrl: string | URL, init?: RequestInit): Promise<ApiAnswerableInterface> => {
      let apiAnswer: ApiAnswerableInterface = ApiAnswerInit;
      let data: any = {};
      const abortController = new AbortController();
      try {
        const response = await fetch(apiUrl, ({...init, signal:abortController.signal}),);
        data = await response.json();
        const abort =()=> {
          abortController.abort();
        }
        apiAnswer = ({
          ...apiAnswer,
          data: data,
          abort:abort,
        });
      } catch (err) {
        apiAnswer = ({
          ...apiAnswer,
          error: err
        })
      } finally {
        apiAnswer = ({
          ...apiAnswer,
          loading: false,
        })
      }
      return apiAnswer;
    }
  1. Delete the "estado.service.ts" and "city.service.ts" files.
  2. Using in FeedTables component the new anyFetch service:
  useEffect(() => {
    const apiState = `${VITE_API_URL}state`;
    anyFetch(apiState, anyFetchUtility(methodType.Get)).then(
      ({ data: estados, error, /* loading, abort */ }) => {
        if (!estados || error) {
          dispatch(createAlert(alertMessageUtility));
        } else {
          estados.map(async (estado: any) => {
            dispatch(createEstado(createEstadoAdapter(estado)));
            const apiState = `${VITE_API_URL}city/${estado.stateId}`
            anyFetch(apiState, anyFetchUtility(methodType.Get)).then(
              ({ data: cities, error, /* loading, abort */ }) => {
                if (!cities || error) {
                  dispatch(createAlert(alertMessageUtility));
                } else {
                  cities.map(async (city: any) => {
                    await dispatch(createCity(createCityAdapter(city)));
                  });
                }
              }
            );
          });
        }
        console.log('FeedTables.Estados:', estados);
      }
    );
    return()=>{
      console.log('FeedTables.End');
    }
  }, []);

17. Apply the "anyFetch.service" to MedicalCenter.Service

  1. The "anyFetch.service" moved to "services" directory
  2. Change the medicalCenterService by anyFetch, in "Login-MedicalCenter-Id.tsx" file.
  3. Adding a RiEyeCloseLine, and RiEyeLine to show or hide the password, in LoginPassword component.
  4. Adding a RiThumbDownLine, and RiThumbUpLine to show if the password confirmed is ok, in LoginConfirmPassword component.
  5. Delete the "medicalCenter.service.ts" file.

18. Doing the Login using the API to validate the user credentials

  1. Add to the "anyFetch.utility.ts" file a body: and movieng the signal: out of headers::
export const anyFetchUtility = (method:methodType, token?: string, body?: BodyInit, abort?: AbortSignal ): RequestInit => {
  return {
    method: method,
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/json',
      'x-auth-token': ((token)?token: ''),
    },
    body:((body)?body:null),
    signal: ((abort)?abort:null),
  }
}
export enum methodType{
  Get = "GET",
  Post = "POST",
  Delete = "DELETE",
}
  1. Improvement to "alertMessage.utility.ts" file letting to use another values:
import { BannerAlertableInterface } from "@/models";
export const alertMessageUtility = (title?: string, message?: string, color?: string, back?: string, timeout?: number): BannerAlertableInterface => {
  return {
    title: (title ? title : "Error"),
    message: (message ? message : "Se ha presentado una falla.\nPor favor avisarle al administrador"),
    textColor: (color ? color : "text-blue-700"),
    background: (back ? back : "bg-yellow-300"),
    timeout: (timeout ? timeout : 5000),
    isVisible: true,
  }
};
  1. Adding the isFirstTime as useRef(true) in "Login-ConfirmPassword.tsx", "Login-Password.tsx" , and "Login-UserType.tsx" files or components.
  2. Adding the main process in "Login.tsx" file to validate the user-Login in handleSubmit method.
  3. The "Home.tsx" file return to root if tokenAccess.ok is false.
  4. Adding the "TokenSlice.ts" and "token.model.ts" files.
  5. Controlling the Login process with an API and feeting the "TokenAccess" in the Redux Store.
  6. Because the "Login.tsx" file become so big with a lot of methods, then I distribute them into "utilities" files in the same "Login" directory.
  7. Adding a Error control in the "anyFetch.service.ts" file, for data in the response:
    const response = await fetch(apiUrl, ({...init, signal:abortController.signal}),);
    const {ok,status, statusText, type } = response;
    if(!ok){
      apiAnswer = ({
        ...apiAnswer,
        error: {ok, status, statusText, type}
      });
    }
  1. Adjusting the the Interface into the model files.

19. Improvement to "Validations" data staring in the Model

  1. Change in "validation.model.ts" file adding the percentIsValid and quantityIsVisible:
    export interface ValidationsListableInterface{
      percentIsValid: number;
      quantityIsVisible:number;
      validationsList: ValidationListableInterface[];
    }
    export interface ValidationListableInterface{
      id: string;
      value: string;
  1. Changes in "store.ts" file:
import { ..., ValidationsListableInterface, ... } from "@/models";

export interface AppStore {
  validations: ValidationsListableInterface;
  ...
}
  1. Changes in "validationsSlice.ts" file , to addValidation and updateValidation reducers, to use the new validationList:
    addValidation: (state, action) => {
      const validationFound = state.validationsList.find(validation => validation.id === action.payload.id); // To avoid duplicates
      if (!validationFound) {
        state.validationsList.push(action.payload);
      }
    },
    updateValidation: (state, action) => {
      const { id, value, isValid, isVisible } = action.payload;
      const validationFound = state.validationsList.find(validation => validation.id === id);
      if (validationFound) {
        validationFound.value = value;
        validationFound.isValid = isValid;
        validationFound.isVisible = isVisible;
      }
    },
  1. Adding two elements in "validationsSlice.ts" file, howManyIsVisible and howManyIsValid, remember export the new ones:
   howManyIsVisible: (state, _action) => {
      let quantity = 0;
      state.validationsList.map((validation: ValidationListableInterface) => {
        if (validation.isVisible) quantity += 1;
      });
      state.quantityIsVisible = quantity;
    },
    howManyIsValid: (state, _action) => {
      if (state.quantityIsVisible > 0) {
        let quantity = 0;
        state.validationsList.map((validation: ValidationListableInterface) => {
          if (validation.isValid && validation.isVisible) quantity += 1;
        });
        state.percentIsValid = (quantity / state.quantityIsVisible) * 100;
        if (state.percentIsValid > 95) state.percentIsValid = 100;
        if (state.percentIsValid < 5) state.percentIsValid = 0;
      }
    },
  1. Change in "loginValidateAllFiels.ts" the current values just calling the dispatch for howManyIsVisible and howManyIsValid:
    import { howManyIsValid, howManyIsVisible } from "@/redux";
    export const loginValidateAllFieldsUtility = async(dispatch: any) => {
      console.log('loginValidateAllFieldsUtility');
      await dispatch(howManyIsVisible(null));
      await dispatch(howManyIsValid(null));
    };
  1. Change in all "utilities" of "Login.tsx" file the validations by validationsList.
  2. Add to each component of "Login.tsx" the icons RiCheckLine, RiCloseLine, to show if it is validor not.
  3. Add a bar in "login.tsx" file to show the percent of valid fields:
          <div className="w-full bg-gray-200 rounded-full h-2.5 dark:bg-gray-700">
            <div
              className={`h-2.5 rounded-full ${
                validations.percentIsValid < 70 ? "bg-red-600" : "bg-green-600"
              }`}
              style={{ width: `${validations.percentIsValid}%` }}
            ></div>
          </div>
  1. To validate all Fields in the Medical Center fields, adding a "loginIsValidMedicalCenter.utility.ts" file:
    import { MedicalCenterableInterface } from "@/models";
    export const isValidMedicalCenterUtility = (medicalCenter: MedicalCenterableInterface) => {
      let isValid = false;
      const { id, name, address: _address, phone, cityId, cityName,stateId , stateName } = medicalCenter;
      isValid =
        id.toString().length >= 6 &&
        name.toString().length >= 5 &&
        phone.toString().length >= 6 &&
        _address.toString().length >= 5 &&
        (cityName.toString().length >=2 || cityId > 1000) &&
        (stateName.toString().length >=2 || stateId > 1) ;
      if (isValid) console.log('isValidMedicalCenterUtility');
      return isValid;
    }

Releases

No releases published

Packages

 
 
 

Contributors

Languages