Diseño UI tienda de videojuegos con React JS y Tailwind CSS totalmente responsivo
- Install the NPM and NODEJS in your system Nodejs Download
- Check in $path or %path% the nodeJS and npm are on it
C:/Program Files/nodejs- Install also
pnpmpnpm installation, it is more fast thannpm - Install Visual Studio Code Visual Studio Download
- 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- Following the instructions, install the applications based on the
package.jsonfile.
pnpm install- To activate tha Alias, check this page Setup path aliases w/ React + Vite + TS, the run this command:
pnpm i -D @types/node- Go the Tailwind CSS, and select "Framework Guides" option.
- Becasue I used the "Vite", select "Vite".
- Run this command of the 2 step "Install Tailwind CSS" option, in a terminal:
pnpm install -D tailwindcss postcss autoprefixer- Run this process to Initialize the tailwind css or create the config Tailwind file:
npx tailwindcss init -p- 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}",
],- Delete all code into "scr/index.css" file, to let only the
@tailwinddirectives:
@tailwind base;
@tailwind components;
@tailwind utilities;- Delete App.css" file.
- Inside the "App.tsx" delete all into the first
<>element, below thereturn, and also delete all unused code, this is only code to let it:
function App() {
return (
<>
</>
)
}
export default App- Become the
<>element to<div>with aclassName:
<div className="bg-red-400">- And run the application.
pnpm dev- From the React Icons site, install the react icons:
pnpm install react-icons --save- Put a basic color in the "index.html"
file using the
<body>and adding a class:
<body class="bg-[#181A20]">- Change the Title in "index,html" file:
<title>Video Games Store Online</title>- Create a directory called "pages".
- Create a file "Home.tsx" and run
rfcesnippet. Remember to delete the first line, not require the import of react. - Create a directory called "components", inside the "pages" directory.
- Create a file "Header.tsx" and run
rfcesnippet. Remember to delete the first line, not require the import of react. - Create a "index.ts" file into "components", an put this info:
export { default as Header } from './Header';- Add the
importofHeaderinto "Home.tsx" file:
import {Header} from './components';- Add a "index.ts" file into "pages" directory, whit this info:
export { default as Home } from './Home';
export * from './components';- Finally add the
importofHomeinto "App.tsx" file:
- Call into the
<div>element belowreturn, the<Home/>element, this is the current "App.tsx" file:
import { Home } from "@/pages"
function App() {
return ( <div><Home/></div> )
}
export default App- Instead of "Home" in "Home.tsx" file add the
<Header/>element. - To the
<div>element in "Home.tsx" add aclassNamewith some attributes:
<div className="min-h-screen">
<Header />
</div>- Change the
<div>element of "Header.tsx" file by<Header>and add aclassName:
return <header className="text-gray-300">- Instead of
Headertext change for<ul>and<li>elements. - The element into
<li>could be<a href ="#">or<Link>, We usereact-router-dom, then install it:
pnpm install react-router-dom- Add an
importinto "App.tsx" file:
import { BrowserRouter, Routes, Route } from 'react-router-dom';- In "App.tsx" file Add the
BrowserRouter, andRoutescomponents are arround the<Header />, and use theRoute path=point toHome:
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
</Routes>
</BrowserRouter>- for each
<li>use aLinkto 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>importthe Remix Icon into "Header.tsx" file, to use in the second<ul>element:
import { RiShoppingCartLine, RiHeart2Line } from "react-icons/ri"; <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>- Add to
<headera 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">- Add a
classNameto first<ul>element:
<ul className="flex items-center gap-6">- Add a
Hoverto each<li>element, using two variables:
const orangeColor = 'text-[#E58D27] transition-colors';
const hoverColor = "hover:"+orangeColor;- Add a
classNameto the second<ul>:
<ul className="flex items-center gap-6 text-xl">- Add the same
classNameto the each<button>.
- Add two component into '/pages/components' called "Sidebar.tsx" and "Content.tsx" files.
- run the
rafce, delete the first line, and update the "index.ts" file. - Add in "Home.tsx" file the components
<Sidebar/>, and<Content/>below theHeaderinto a<div>:
<div className="min-h-screen">
<Header />
<div className="h-[90vh] p-8">
<Sidebar/>
<Content/>
</div>
</div>- Need to add the 10% missing in "Header.tsx" file in the
<headerelement:
<header className={"h-[10vh] text-gray-400 py-4 px-10 flex items-center justify-between "+darkBrown}>- Add a
classNameinto the<div>of "Sidebar.tsx" file:
<div className="w-64 h-full overflow-y-scroll">- Add a
classNameinto the<div>of "Content.tsx" file, to expand the rest to the right:
<div className=" flex-1 h-full overflow-y-scroll">- Add two
<div>into "Sidebar.tsx" file. - 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>- 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. - Below the
<h4>add<div>arround all<inputcheckbox type. - Add to this
<div>aclassName:
<div className="flex flex-col gap-2">- Add another
<h4>with Paltforms
<h4 className="my-6">Platforms</h4>- Repeat the
<div>of each check box times more to complete this list: PC, PlayStation 5, PlasyStation 4, Xbox Series, Nintendo Switch. - Copy another
<h4>for Price:
<h4 className="my-6">Price</h4>- 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>- 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>- Add an
<span>, and duplicate the<input type="number" - Add a
classNameto the<form>:
- When start the app, the colors don't appear then remove the
importfrom "colors.ts" file , and put manually the colors to each element. - Add a
<button>just over the</form>in "Sidebar.tsx" file, with atype="submit":
<button type="submit"> </button>- Put a
<div>arround the both<div className="relative">, and add aclassName, 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>- Add to the
<form>a newclassName:
<form className="flex flex-col gap-6">- Add a
classNameto the<button>:
<button type="submit" className="bg-[#E58D27] text-black rounded-full w-full p-2">Apply Filters</button>- Add to the three
<h4>inclassName:
<h4 className="my-6 text-white text-lg">- Add a
hover:to the<button>, into theclassName:
hover:-translate-y-1 transition-all duration-200- 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>- Add a
classNameto check box:
className="accent-[#E58D27]"- Add a Picture
<img>into "content.tsx" file. - 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>
);
};- 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"
/>- All the
<Cardar into a<div>withclassName, and the very important there isflex-wrap:
<div className="flex items-center justify-between flex-wrap gap-8">- Add a
<button>with thisRiMenu2Lineicon in "Header.tsx" file over the First Menu:
<button className="lg:hidden"><RiMenu2Line/></button>- Add a
hiddento the First Menu in "Header.tsx" file:
<ul className="hidden lg:flex items-center gap-6">- 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>- Add
useStatelike this:
const [showMenu, setShowMenu ] = useState(false);- The
<button>Activate or not the menu, and the menu appear based on theshowMemenustatus:
<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>- Add a
<button>over the<ul>Mobile Menu:
<button onClick={()=> setShowMenu(!showMenu)}><RiCloseLine/></button>- Center the items of Menu only in new
<ul>adding aclassName:
<ul className=" w-full mt-20">- For the Menu add some elements to
className, asblock text-center p-4 text-4xl.
- Put some elements in the
classNameof 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">- Add a
<>element and move all inside this element - Add a
<button>withRiFilterLineicon:
<button className="text-lg fixed bottom-4 right-4 bg-[#E58D27] rounded-full z-40 lg:hidden"><RiFilterLine/></button>- Add an
useStateto show the Filter:
const [showSidebar, setShowSidebar] = useState(false);- 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`}>- the last
<button>add a condition to show theRiCloseLineorRiFilterLine:
{showSidebar? <RiCloseLine/>:<RiFilterLine/>}- for the 'IPad Air' device add
classNameof first<div>the use ofmd:
`... md:w-[40%] ...`- Change
classNameelements of first image in "Content.tsx" file:
className="w-full h-[500px] object-cover object-right md:object-top rounded-2xl"- 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">- Change in
classNameof<div>container of all cards thejustify-betweenbyjustify-arroundin "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">- Change in "Cards.tsx" file in
<img>some elements in theclassName:
<img
src={props.img}
className="w-full lg:w-52 h-72 lg:h-52 object-cover rounded-2xl"
/>- Adding to
classNameof "Sidebar.tsx" file thislg:p-0. - 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`}>- Create in "pages" directory, two new directories: Home and Login.
- Move the "Home.tsx" file to the "Home" directory.
- Create a "Login.tsx" file into "Login" directory, run the
rfcesnippet, delete first line. - Create a "components" directory into "src" directory and move the "Colors.ts" and "Header.tsx" files.
- Into the each directory of "pages" create a "components" directory for the exclusives components for each page.
- Add or Update the "index.ts" file into each directory.
- Add a "AuthWrapper.tsx" fiel into "components of root, run the
rfcesnippet, delete first line. - Add this code to "AuthWrapper.tsx" file:
const AuthWrapper=(props: {
isAuthenticated:boolean;}
) => {
return props.isAuthenticated ? (
<Navigate to="/home" replace />
) : (
<Navigate to="/login" replace />
);
}- Changes in "App.tsx" file like this, the first page to show will be "login" while the
isAuthnticadedkeeps 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>
);
}- The "Login.tsx" file add a
<form>. - 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>- Add some
classNamewith Tailwind CSS elements. - Add
useStateto show the Registry options:
const [showRegistry, setShowRegistry] = useState(false);- Use this
useStatein the last<button>.
<button className="..." onClick={() => setShowRegistry(!showRegistry)}>- To the
<form>add aonSubmit={handleSubmit}, and create the function:
const handleSubmit = (e: any) => {
e.preventDefault(); // Avoid page refreshing.
};- Add some Elements when
showRegistryis 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>- Create a "components" directory into "Login" directory.
- Create a "Login-Email.tsx" file, run the
rfcesnippet and delete the first line. - Move all regarding email from "Login.tsx" to the "LoginEmail.tsx" component and call the new component
<LoginEmail/>into "Login.tsx" file. - 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
classNameof<label/>,e.g.:
<label htmlFor="" className="control-label">
Correo
</label>- To the
<input>add arequiredvalue
<input ... required={true} />- Play with a
useStateto send a function can be executed in a Children component, in this case is "Login-Email.tsx" file.
pnpm install @reduxjs/toolkit react-redux- Create a "models" directory in the root ("src/models").
- 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",
}- Create a "redux" directory, and put there the "store.ts" file, with those lines:
import { configureStore } from "@reduxjs/toolkit";
export default configureStore({});- 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;- 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,
}
});- In the main file "App.tsx" add a
Providerto include all the others
import { Provider } from "react-redux";- Import the
storefrom "store.ts", in "App.tsx" file:
import store from "./redux/store";- Finally I change the return of "App.tsx" file like this:
return (
<Provider store={store}>
...
</Provider>
);- Add in "validationsSlice.ts" a
reducerswith the name:addValidation, like this:
addValidation: (state, action) => {
const validationFound = state.find(validation => validation.id === action.payload.id);
if (!validationFound) {
state.push(action.payload);
}
}- Add this
addValidationin "Login-Email.tsx" file in auseEffect, to run only once:
useEffect(() =>{
dispatch( addValidation({
id: "email",
value: email,
type: "string",
isValid: false,
isVisible: props.isVisible,
message: "Email",
}));
},[]);- Create an
useStatein "Login-Email.tsx" file, to control de changes on the field:
const [email, setEmail] = useState("");- Add a function called
handleChangeto update theemailuf theuseState:
const handleChange = (e: any) => {
setEmail(e.target.value);
};- Call this function in the
<input>element usingonChange={handleChange}. - Add in "validationsSlice.ts" a
reducerswith 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;
}
}- Add a function called
handleBlurto validate the email and feedvalidationswithupdateValidationin "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,
}));
}- Call this function in the
<input>element usingonBlur={handleBlur}. - Just for validate in the
handleSubmitof "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);
};- Copy the "Login-Email.tsx" in "Login-Password.tsx".
- Rename in "Login-Password.tsx" the
LoginEmailbyLoginPassword. - Renamame all
EmailbyPasswordandemailbypassword. - Move all the Information from "Login.tsx" to "Login-Password.tsx" regarding the
Password. - Add two
useStateforstrengthBadge, andbackgroundColor. - 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,}))"
);- Add a function to use the Regular Expresions and feed the
strengthBadgeandbackgroundColor:
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;
};- Call in Login the New Component:
<LoginPassword isVisible={showRegistry}/>- Copy the "Login-Email.tsx" in "Login-MedicalCenter.tsx".
- Rename in "Login-MedicalCenter.tsx" the
LoginEmailbyLoginMedicalCenter. - Renamame all
EmailbyMedicalCenterandemailbymedicalCenter. - Move all the Information from "Login.tsx" to "Login-Password.tsx" regarding the
MedicalCenter. - 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;
}- Because the Medical Center is a JSON, create a
initialValue, to use in theuseState:
const initialState: MedicalCenterableInterface = {
id: 0, name: "",
address: "", telephone: 0,
stateId: 0, cityId: 0,
};
const [medicalCenter, setMedicalCenter] = useState(initialState);
const dispatch = useDispatch();- For
handleChangejust add theidin thismedicalCenterobject:
const handleChange = async(e: any) => {
setMedicalCenter({
...medicalCenter, id: parseInt(e.target.value),
});
};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
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.
- Add a
constin "Login-MedicalCenter.tsx" file assiteMedicalCenter:
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;
}- Add a
useRefto control the first time ofuseEffect
const isFirstTime = useRef(true);- Change the
useEffectto 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]);- In the future complete the Fields based on the API answer and let to select the
state(Departamento) and thecity(Ciudad), verify the Password and the UserType (Clínica, Laboratorio, Administrador).
- 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",
...}- Move the
fetchprocess 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;
}
}- Add an
adapterin "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: '',
});- 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;
}- Changes in the "Login-MedicalCenter.tsx" component are in the
refreshMedicalCentersmethod, the first part is the use of thegetMedicalCenterservice and theadapter:
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);
}- Next in
refreshMedicalCentersmethod, is the error control, using a new component called "BannerAlert.tsx" (Using somethig better than an simplealert)
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,
}) );
} }
}) };- Add a Model called "banner-alert.model.ts" file:
export interface BannerAlertableInterface{
title: string;
message: string;
textColor: string;
background: string;
timeout: number;
isVisible:boolean;
}- Add a new
sliceto 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;- Add the new
sliceto the "store.ts" file:
export interface AppStore {
validations: ValidationListableInterface[];
bannerAlert: BannerAlertableInterface;
}
export default configureStore<AppStore>({
reducer: {
validations: validationsSlice.reducer,
bannerAlert: bannerAlertSlice.reducer,
}
});- 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.)
- Create two new
adaptersfor City and Estado (state or Deparment). - Create two new
servicesfor City and Estado (state or Deparment). - Create two new
modelsfor City and Estado (state or Deparment). - Create two new
slicesfor 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).
- Moving the basic Alert message to the "utilities" directory.
- Add the
interfacefrom models and thereducerfrom Slice in "states" directory to "store.ts" file. - Changes in
refreshMedicalCentersmethod. - Adding a
useEffectin "Login.tsx" file. - This
useEffectwill fill the Estados and Cities.
- Add an Utility, to control all the data from UI by
<Input>or<select>, to get the value ornumberorstring, called "valueType.utility.ts". - Add a new component called "FeedTables.tsx" just to complete the data of
estadosListandcitiesList, use that in "App.tsx" file. - Move all data for creation of List based on "Cities" and "States" from "Login.tsx" to the new "FeedTables.tsx".
- 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;
}- 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;
}- For
adaptersjust to add the type of each origin value. - 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>- The
handleChangemethod, 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));
}
};- The
handleBlurmethod, let to manage all field and doing some validation byswitch:
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;
}
};- The Estados Slice, changes based on the new model and adding two new
reducers, called:setMainEstado, andgetMainEstado:
...
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;
}
},
}- 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;
}
},
}- Install the
rxjsfrom the Installation Instructions
pnpm install rxjs @reactivex/rxjs- Check the Example in Sharing between components
- 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()
};- The component to set or feed the data is "Login.tsx", like this, remember
showRegistryhas auseState:
useEffect(() => {
dataSharedService.setDataShared(showRegistry);
}, [showRegistry]);- The other componets get the info based on
suscribe:
useEffect(() => {
dataSharedService.getDataShared().subscribe( data =>{
if(data!==null)setIsVisible(data as boolean);
});- Eliminate the
propsand change byuseState, like this:
const LoginMedicalCenter = () => {
...
const [isVisible, setIsVisible] = useState(false);- I don't do the same for the other components
LoginEmailandLoginPassword, because theisVisibleis always true, then I keep deprops.
- Create a "Login-MedicalCenter-id.tsx" file and move all regarding from "Login-MedicalCenter.tsx" Component.
- Create a "Login-MedicalCenter-StateNCity.tsx" file and move all regarding from "Login-MedicalCenter.tsx" Component.
- Create a "medicalCenter.service.ts" file and use the
rxjspackage:
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()
};- all the Components get and set the new
rxjsvalue, in theuseEffecthook:
medicalCenterService.getMedicalCenter().subscribe((data) => {
if (data) setMedicalCenter(data as MedicalCenterableInterface);
});
...
medicalCenterService.setMedicalCenter(medicalCenter);- Before the normal
setMedicalCenterfromuseStatehook, call themedicalCenterService.getMedicalCenter(). - in the "Login-MedicalCenter-Id.tsx" file for the
refreshMedicalCentersmethod, add to complete thestateNameandcityName, options:
if (adapted.cityId > 0 && adapted.stateId > 0) {
dispatch(getMainEstado(adapted.stateId));
adapted['stateName']= estadosList.estadoName;
dispatch(getMainCity(adapted.cityId));
adapted['cityName']= citiesList.cityName;
}- Some improvements in the "medicalCenter.adapter.ts" to validate each field or return a default value.
- Create both New Compononents.
- Back to the
propselement because the rendering time was so long using therxjsforisVisisblevariable. - Keeps the
medicalCenter.serviceusing therxjspackage.
- 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",
}- 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,
}- 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;
}- Delete the "estado.service.ts" and "city.service.ts" files.
- Using in
FeedTablescomponent the newanyFetchservice:
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');
}
}, []);- The "anyFetch.service" moved to "services" directory
- Change the
medicalCenterServicebyanyFetch, in "Login-MedicalCenter-Id.tsx" file. - Adding a
RiEyeCloseLine, andRiEyeLineto show or hide the password, inLoginPasswordcomponent. - Adding a
RiThumbDownLine, andRiThumbUpLineto show if the password confirmed is ok, inLoginConfirmPasswordcomponent. - Delete the "medicalCenter.service.ts" file.
- Add to the "anyFetch.utility.ts" file a
body:and movieng thesignal:out ofheaders::
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",
}- 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,
}
};- Adding the
isFirstTimeasuseRef(true)in "Login-ConfirmPassword.tsx", "Login-Password.tsx" , and "Login-UserType.tsx" files or components. - Adding the main process in "Login.tsx" file to validate the user-Login in
handleSubmitmethod. - The "Home.tsx" file return to root if tokenAccess.ok is
false. - Adding the "TokenSlice.ts" and "token.model.ts" files.
- Controlling the Login process with an API and feeting the "TokenAccess" in the Redux Store.
- 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.
- 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}
});
}- Adjusting the the
Interfaceinto the model files.
- Change in "validation.model.ts" file adding the
percentIsValidandquantityIsVisible:
export interface ValidationsListableInterface{
percentIsValid: number;
quantityIsVisible:number;
validationsList: ValidationListableInterface[];
}
export interface ValidationListableInterface{
id: string;
value: string;- Changes in "store.ts" file:
import { ..., ValidationsListableInterface, ... } from "@/models";
export interface AppStore {
validations: ValidationsListableInterface;
...
}- Changes in "validationsSlice.ts" file , to
addValidationandupdateValidationreducers, to use the newvalidationList:
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;
}
},- Adding two elements in "validationsSlice.ts" file,
howManyIsVisibleandhowManyIsValid, 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;
}
},- Change in "loginValidateAllFiels.ts" the current values just calling the
dispatchforhowManyIsVisibleandhowManyIsValid:
import { howManyIsValid, howManyIsVisible } from "@/redux";
export const loginValidateAllFieldsUtility = async(dispatch: any) => {
console.log('loginValidateAllFieldsUtility');
await dispatch(howManyIsVisible(null));
await dispatch(howManyIsValid(null));
};- Change in all "utilities" of "Login.tsx" file the
validationsbyvalidationsList. - Add to each component of "Login.tsx" the icons
RiCheckLine,RiCloseLine, to show if it is validor not. - 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>- 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;
}
