├── src ├── vite-env.d.ts ├── layouts │ ├── index.ts │ ├── DashboardLayout.tsx │ └── AuthLayout.tsx ├── components │ ├── index.ts │ ├── shared │ │ ├── sidemenu │ │ │ ├── SideMenu.css │ │ │ ├── SideMenuItem.tsx │ │ │ └── SideMenu.tsx │ │ └── cards │ │ │ └── WhiteCard.tsx │ └── jira │ │ └── JiraTasks.tsx ├── pages │ ├── index.ts │ ├── 02-objects │ │ └── JiraPage.tsx │ ├── 01-basic │ │ ├── BearPage.tsx │ │ └── PersonPage.tsx │ ├── dashboard │ │ └── DashboardPage.tsx │ ├── auth │ │ └── LoginPage.tsx │ └── 03-slices │ │ └── WeddingInvitationPage.tsx ├── Root.tsx ├── main.tsx ├── index.css ├── router │ └── router.tsx └── assets │ └── react.svg ├── public ├── screenshot.png └── vite.svg ├── postcss.config.js ├── vite.config.ts ├── tailwind.config.js ├── tsconfig.node.json ├── .gitignore ├── index.html ├── README.md ├── .eslintrc.cjs ├── tsconfig.json └── package.json /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/layouts/index.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | export * from './AuthLayout'; 4 | export * from './DashboardLayout'; -------------------------------------------------------------------------------- /public/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Klerith/zustand-mini-curso/HEAD/public/screenshot.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | export * from './jira/JiraTasks'; 4 | 5 | 6 | export * from './shared/sidemenu/SideMenu'; 7 | export * from './shared/cards/WhiteCard'; -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [ 4 | "./index.html", 5 | "./src/**/*.{js,ts,jsx,tsx}", 6 | ], 7 | theme: { 8 | extend: {}, 9 | }, 10 | plugins: [], 11 | } 12 | 13 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /src/components/shared/sidemenu/SideMenu.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | #nav a { 6 | @apply w-full px-2 inline-flex space-x-2 items-center border-b border-slate-700 py-3 hover:bg-white/5 transition ease-linear duration-150; 7 | } 8 | 9 | #nav a.active { 10 | @apply bg-blue-800; 11 | } -------------------------------------------------------------------------------- /src/pages/index.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | export * from './dashboard/DashboardPage'; 4 | export * from './01-basic/BearPage'; 5 | export * from './01-basic/PersonPage'; 6 | export * from './02-objects/JiraPage'; 7 | export * from './03-slices/WeddingInvitationPage'; 8 | 9 | 10 | export * from './auth/LoginPage'; -------------------------------------------------------------------------------- /src/Root.tsx: -------------------------------------------------------------------------------- 1 | import { Navigate, Outlet, useLocation } from 'react-router-dom'; 2 | 3 | 4 | export const Root = () => { 5 | 6 | const { pathname } = useLocation(); 7 | 8 | if (pathname === '/') { 9 | return ; 10 | } 11 | 12 | return ( 13 |
14 | 15 |
16 | ) 17 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | 4 | import './index.css' 5 | import { RouterProvider } from 'react-router-dom'; 6 | import { router } from './router/router.tsx'; 7 | 8 | ReactDOM.createRoot(document.getElementById('root')!).render( 9 | 10 | 11 | , 12 | ) 13 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite + Zustand + TailwindCSS + ReactRouterDom 2 | 3 | Este es un cascarón de proyecto, siéntete libre de usarlo para tus proyectos. 4 | 5 | Dashboard Screenshot 6 | 7 | 8 | 9 | ## Instalar 10 | 11 | 1. Clonar proyecto 12 | 2. Instalar dependencias ```npm install``` 13 | 3. Correr en desarrollo ```npm run dev``` 14 | 15 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | ], 9 | ignorePatterns: ['dist', '.eslintrc.cjs'], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['react-refresh'], 12 | rules: { 13 | 'react-refresh/only-export-components': [ 14 | 'warn', 15 | { allowConstantExport: true }, 16 | ], 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /src/layouts/DashboardLayout.tsx: -------------------------------------------------------------------------------- 1 | import { SideMenu } from '../components'; 2 | import { Outlet } from 'react-router-dom'; 3 | 4 | export const DashboardLayout = () => { 5 | return ( 6 |
7 |
8 | 9 | 10 |
11 | 12 |
13 | 14 |
15 | 16 |
17 | ); 18 | }; -------------------------------------------------------------------------------- /src/components/shared/cards/WhiteCard.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | 3 | interface Props { 4 | children?: React.ReactNode; 5 | centered?: boolean; 6 | className?: string; 7 | } 8 | 9 | 10 | export const WhiteCard = ( { children, centered, className }: Props ) => { 11 | return ( 12 |
17 | { children } 18 |
19 | ); 20 | }; -------------------------------------------------------------------------------- /src/pages/02-objects/JiraPage.tsx: -------------------------------------------------------------------------------- 1 | import { JiraTasks } from '../../components'; 2 | 3 | export const JiraPage = () => { 4 | return ( 5 | <> 6 |

Tareas

7 |

Manejo de estado con objectos de Zustand

8 |
9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 | ); 26 | }; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /src/layouts/AuthLayout.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { Outlet } from 'react-router-dom'; 3 | 4 | export const AuthLayout = () => { 5 | return ( 6 |
7 |
8 | Zustand 9 | {/* Placeholder Image */} 12 |
13 |
14 | 15 |
16 |
17 | ); 18 | }; -------------------------------------------------------------------------------- /src/components/shared/sidemenu/SideMenuItem.tsx: -------------------------------------------------------------------------------- 1 | 2 | import type { IconType } from 'react-icons'; 3 | import { NavLink } from 'react-router-dom'; 4 | 5 | interface Props { 6 | href: string; 7 | Icon: IconType; 8 | title: string; 9 | subTitle: string 10 | } 11 | 12 | 13 | export const SideMenuItem = ({ href, Icon, title, subTitle }: Props) => { 14 | return ( 15 | 20 |
21 | 22 |
23 |
24 | { title } 25 | { subTitle } 26 |
27 |
28 | ); 29 | } -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | 6 | h1 { 7 | @apply text-3xl font-bold text-gray-800; 8 | } 9 | 10 | h2 { 11 | @apply text-2xl font-semibold text-gray-800; 12 | } 13 | 14 | hr { 15 | @apply my-1 border-gray-300 border mb-3; 16 | } 17 | 18 | button { 19 | @apply transition-all hover:bg-indigo-800 bg-indigo-600 px-8 py-2 rounded-3xl text-gray-100 font-semibold uppercase tracking-wide shadow-md; 20 | } 21 | 22 | input:not(.swal2-input)[type=text],input:not(.swal2-input)[type=password], input:not(.swal2-input)[type=email], input:not(.swal2-input)[type=number], input:not(.swal2-input)[type=date], input:not(.swal2-input)[type=time] { 23 | @apply w-full border border-gray-300 rounded-md py-2 px-3 focus:outline-none focus:border-blue-500; 24 | } 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zustand-dash", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "classnames": "^2.3.2", 14 | "react": "^18.2.0", 15 | "react-dom": "^18.2.0", 16 | "react-icons": "^4.11.0", 17 | "react-router-dom": "^6.16.0" 18 | }, 19 | "devDependencies": { 20 | "@types/react": "^18.2.15", 21 | "@types/react-dom": "^18.2.7", 22 | "@typescript-eslint/eslint-plugin": "^6.0.0", 23 | "@typescript-eslint/parser": "^6.0.0", 24 | "@vitejs/plugin-react": "^4.0.3", 25 | "autoprefixer": "^10.4.16", 26 | "eslint": "^8.45.0", 27 | "eslint-plugin-react-hooks": "^4.6.0", 28 | "eslint-plugin-react-refresh": "^0.4.3", 29 | "postcss": "^8.4.31", 30 | "tailwindcss": "^3.3.3", 31 | "typescript": "^5.0.2", 32 | "vite": "^4.4.5" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/pages/01-basic/BearPage.tsx: -------------------------------------------------------------------------------- 1 | import { WhiteCard } from '../../components'; 2 | 3 | export const BearPage = () => { 4 | return ( 5 | <> 6 |

Contador de Osos

7 |

Manejo de estado simple de Zustand

8 |
9 | 10 |
11 | 12 | 13 |

Osos Negros

14 | 15 |
16 | 17 | 0 18 | 19 |
20 | 21 |
22 | 23 | 24 |

Osos Polares

25 | 26 |
27 | 28 | 0 29 | 30 | 31 |
32 | 33 |
34 | 35 | 36 |

Osos Pandas

37 | 38 |
39 | 40 | 0 41 | 42 |
43 | 44 |
45 | 46 | 47 | 48 | 49 |
50 | 51 | 52 | ); 53 | }; -------------------------------------------------------------------------------- /src/router/router.tsx: -------------------------------------------------------------------------------- 1 | import { createBrowserRouter } from 'react-router-dom'; 2 | 3 | import { Root } from '../Root'; 4 | import { AuthLayout, DashboardLayout } from '../layouts'; 5 | import { BearPage, Dashboard, JiraPage, LoginPage, PersonPage, WeddingInvitationPage } from '../pages'; 6 | 7 | 8 | export const router = createBrowserRouter( [ 9 | { 10 | path: '/', 11 | element: , 12 | children: [ 13 | /// Dashboard Routes 14 | { 15 | path: 'dashboard', 16 | element: , 17 | children: [ 18 | { 19 | path: '', 20 | element: 21 | }, 22 | { 23 | path: 'bears', 24 | element: 25 | }, 26 | { 27 | path: 'person', 28 | element: 29 | }, 30 | { 31 | path: 'tasks', 32 | element: 33 | }, 34 | { 35 | path: 'wedding-invitation', 36 | element: 37 | } 38 | 39 | ] 40 | }, 41 | 42 | /// Auth Routes 43 | { 44 | path: 'auth', 45 | element: , 46 | children: [ 47 | { 48 | path: 'login', 49 | element: 50 | } 51 | ] 52 | 53 | }, 54 | 55 | ], 56 | }, 57 | ] ); -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pages/dashboard/DashboardPage.tsx: -------------------------------------------------------------------------------- 1 | import { IoAccessibilityOutline, IoHeartOutline, IoListOutline, IoLockClosedOutline, IoPawOutline } from 'react-icons/io5'; 2 | import { WhiteCard } from '../../components'; 3 | 4 | export const Dashboard = () => { 5 | return ( 6 | <> 7 |

Dashboard

8 |

Información colectiva de varios stores de Zustand

9 |
10 | 11 |
12 | 13 | 14 | 15 |

Osos

16 |

Información

17 |
18 | 19 | 20 | 21 | 22 |

Persona

23 |

Información

24 |
25 | 26 | 27 | 28 | 29 |

Tareas

30 |

Información

31 |
32 | 33 | 34 | 35 | 36 |

Boda

37 |

Información

38 |
39 | 40 | 41 | 42 | 43 |

Auth

44 |

Información

45 |
46 | 47 | 48 | 49 |
50 | 51 | 52 | ); 53 | }; -------------------------------------------------------------------------------- /src/pages/auth/LoginPage.tsx: -------------------------------------------------------------------------------- 1 | import { FormEvent } from 'react'; 2 | 3 | export const LoginPage = () => { 4 | 5 | const onSubmit = (event: FormEvent ) => { 6 | event.preventDefault(); 7 | // const { username, password, remember } = event.target as HTMLFormElement; 8 | const { username, password,remember } = event.target as typeof event.target & { 9 | username: { value: string }; 10 | password: { value: string }; 11 | remember: { checked: boolean } 12 | }; 13 | console.log(username.value, password.value, remember.checked); 14 | 15 | username.value = ''; 16 | password.value = ''; 17 | remember.checked = false; 18 | } 19 | 20 | 21 | return ( 22 | <> 23 |

Login

24 | 25 |
26 | 27 |
28 | 29 | 30 |
31 | 32 |
33 | 34 | 35 |
36 | 37 |
38 | 39 | 40 |
41 | 42 |
43 | Forgot Password? 44 |
45 | 46 | 47 |
48 |
49 | Sign up Here 50 |
51 | 52 | ); 53 | }; -------------------------------------------------------------------------------- /src/pages/01-basic/PersonPage.tsx: -------------------------------------------------------------------------------- 1 | import { WhiteCard } from '../../components'; 2 | 3 | 4 | 5 | export const PersonPage = () => { 6 | return ( 7 | <> 8 |

Persona

9 |

Información que se compartirá a otro store, Session Storage y Firebase

10 |
11 | 12 | 13 |
14 |
15 |
16 |
17 |
18 | 23 | 29 |
30 |
31 |
32 |
33 | 38 | 44 |
45 |
46 |
47 | 48 |
49 |               {
50 |                 JSON.stringify({
51 |                   firstName: '',
52 |                   lastName: ''
53 |                 }, null, 2)
54 |               }
55 |             
56 |
57 |
58 |
59 | 60 | ); 61 | }; -------------------------------------------------------------------------------- /src/components/jira/JiraTasks.tsx: -------------------------------------------------------------------------------- 1 | import { IoCheckmarkCircleOutline, IoEllipsisHorizontalOutline, IoReorderTwoOutline } from 'react-icons/io5'; 2 | 3 | interface Props { 4 | title: string; 5 | value: 'pending' | 'in-progress' | 'done'; 6 | } 7 | 8 | 9 | export const JiraTasks = ({ title }: Props) => { 10 | return ( 11 |
12 | 13 | 14 | {/* Task Header */ } 15 |
16 | 17 |
18 | 19 |
20 | 21 | 22 | 23 |
24 | 25 |

{ title }

26 |
27 | 28 | 31 | 32 |
33 | 34 | {/* Task Items */ } 35 |
36 | 37 |
38 |
39 |

40 | Tarea número 1 41 |

42 |
43 | 44 | 45 | 46 |
47 | 48 |
49 |
50 |

51 | Tarea número 2 52 |

53 |
54 | 55 | 56 | 57 |
58 | 59 | 60 | 61 |
62 |
63 | ); 64 | }; -------------------------------------------------------------------------------- /src/components/shared/sidemenu/SideMenu.tsx: -------------------------------------------------------------------------------- 1 | import type { IconType } from 'react-icons'; 2 | import { IoSpeedometerOutline, IoPawOutline, IoLogOutOutline, IoHeartOutline, IoListOutline, IoAccessibilityOutline } from 'react-icons/io5'; 3 | import { NavLink } from 'react-router-dom'; 4 | import './SideMenu.css'; 5 | import { SideMenuItem } from './SideMenuItem'; 6 | 7 | 8 | interface MenuItem { 9 | title: string; 10 | subTitle: string; 11 | href: string; 12 | Icon: IconType; 13 | } 14 | 15 | const menuItems: MenuItem[] = [ 16 | { title: 'Dashboard', subTitle: 'Visualizar data', href: '/dashboard', Icon: IoSpeedometerOutline }, 17 | { title: 'Osos', subTitle: 'Manejador de osos', href: '/dashboard/bears', Icon: IoPawOutline }, 18 | { title: 'Persona', subTitle: 'Nombre y apellido', href: '/dashboard/person', Icon: IoAccessibilityOutline }, 19 | { title: 'Tareas', subTitle: 'Listado de tareas', href: '/dashboard/tasks', Icon: IoListOutline }, 20 | { title: 'Boda', subTitle: 'Invitados a la boda', href: '/dashboard/wedding-invitation', Icon: IoHeartOutline }, 21 | ]; 22 | 23 | 24 | 25 | 26 | export const SideMenu = () => { 27 | 28 | return ( 29 | 77 | ); 78 | }; -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pages/03-slices/WeddingInvitationPage.tsx: -------------------------------------------------------------------------------- 1 | import { WhiteCard } from '../../components'; 2 | 3 | 4 | 5 | export const WeddingInvitationPage = () => { 6 | return ( 7 | <> 8 |

Invitación de Boda

9 |

Zustand segmentado en slices

10 |
11 | 12 | 13 |
14 |
15 |
16 |
17 |
18 | 23 | 29 |
30 |
31 |
32 |
33 | 38 | 44 |
45 |
46 |
47 |
48 | 53 | 61 |
62 | 63 |
64 |
65 |
66 | 71 | 76 |
77 |
78 |
79 |
80 | 85 | 90 |
91 |
92 |
93 | 94 |
95 | 98 |
99 |
100 | 106 | 111 |
112 |
113 | 119 | 124 |
125 |
126 |
127 | 128 |
129 | 132 |
133 |
134 |
135 |
136 | 137 | ); 138 | }; --------------------------------------------------------------------------------