From 29bbc878caca9938f3836cce55d763b916b4090a Mon Sep 17 00:00:00 2001 From: Antoine Richard <56544866+arichard-info@users.noreply.github.com> Date: Thu, 5 Sep 2024 13:13:29 +0200 Subject: [PATCH] feat: init labs (#4) * feat: init labs * feat: rename demo application & implement first lab * feat: add lab 2 * fix: first labs scope * fix: add layout components to help * fix: textfield * fix: web modules * feat: implement 03.02 * feat: implement 04.01 * feat: add 04.02 --- package-lock.json | 332 ++++++++++++++++- steps/00-sfeir-people/README.md | 36 -- .../src/components/NavigationMenu/index.ts | 1 - .../.env.example | 0 .../.eslintrc.json | 0 .../.gitignore | 0 steps/00.00-sfeir-people-demo/README.md | 11 + .../next.config.mjs | 2 +- .../package.json | 0 .../postcss.config.js | 0 .../public/next.svg | 0 .../public/vercel.svg | 0 .../src/api/common.ts | 0 .../src/api/error.ts | 0 .../src/api/expenses.ts | 0 .../src/api/people.ts | 0 .../src/app/(auth)/actions.ts | 0 .../src/app/(auth)/layout.tsx | 0 .../src/app/(auth)/login/page.tsx | 0 .../(home)/@employeesSlot/error.tsx | 0 .../(home)/@employeesSlot/loading.tsx | 0 .../(home)/@employeesSlot/page.tsx | 0 .../(home)/@expensesSlot/error.tsx | 0 .../(home)/@expensesSlot/loading.tsx | 0 .../(dashboard)/(home)/@expensesSlot/page.tsx | 0 .../src/app/(dashboard)/(home)/layout.tsx | 0 .../@modal/(.)expenses/[id]/page.tsx | 0 .../src/app/(dashboard)/@modal/default.tsx | 0 .../(dashboard)/employees/[id]/edit/page.tsx | 0 .../app/(dashboard)/employees/[id]/page.tsx | 0 .../src/app/(dashboard)/employees/actions.ts | 0 .../src/app/(dashboard)/employees/error.tsx | 0 .../app/(dashboard)/employees/new/page.tsx | 0 .../src/app/(dashboard)/employees/page.tsx | 0 .../app/(dashboard)/expenses/[id]/page.tsx | 0 .../src/app/(dashboard)/expenses/page.tsx | 0 .../src/app/(dashboard)/layout.tsx | 0 .../src/app/api/revalidate/route.ts | 0 .../src/app/error.tsx | 0 .../src/app/layout.tsx | 0 .../src/assets/images/profile-placeholder.jpg | Bin .../src/assets/svg/logo.svg | 0 .../src/assets/svg/logoDark.svg | 0 .../src/components/Alert.tsx | 0 .../src/components/Button.tsx | 0 .../src/components/EmployeeExpenses.tsx | 0 .../src/components/EmployeeForm.tsx | 0 .../src/components/ExpenseDetails.tsx | 0 .../src/components/ExpenseDetailsLoading.tsx | 0 .../components/ExpensesDetailsStructure.tsx | 12 +- .../src/components/ExpensesTable.tsx | 0 .../src/components/FormSubmitButton.tsx | 0 .../src/components/Icons/ArrowLeft.tsx | 0 .../src/components/Icons/Eye.tsx | 0 .../src/components/Icons/Loader.tsx | 0 .../src/components/InterceptionModal.tsx | 0 .../src/components/Logo.tsx | 0 .../src/components}/Logout.tsx | 0 .../src/components/Modal.tsx | 0 .../src/components}/NavigationItem.tsx | 0 .../src/components}/NavigationMenu.tsx | 0 .../src/components/PageTitle.tsx | 0 .../src/components/Pagination.tsx | 0 .../src/components/Paper.tsx | 0 .../src/components/PersonCard.tsx | 0 .../src/components/Search.tsx | 0 .../src/components/Skeleton.tsx | 0 .../src/components/TableLoading.tsx | 0 .../src/components/TextField.tsx | 0 .../src/components/ThemeProvider.tsx | 0 .../src/functions/auth.ts | 0 .../src/functions/data.ts | 0 .../src/functions/timing.ts | 0 .../src/middleware.ts | 0 .../src/styles/global.css | 0 .../src/types.ts | 0 .../tailwind.config.js | 0 .../tsconfig.json | 0 .../02.01-pages-layout-solution/.env.example | 2 + .../.eslintrc.json | 3 + steps/02.01-pages-layout-solution/.gitignore | 36 ++ steps/02.01-pages-layout-solution/README.md | 1 + .../next.config.mjs | 15 + .../02.01-pages-layout-solution/package.json | 37 ++ .../postcss.config.js | 6 + .../public/next.svg | 1 + .../public/portraits/men/30.jpg | Bin 0 -> 4349 bytes .../public/portraits/men/34.jpg | Bin 0 -> 4305 bytes .../public/portraits/men/56.jpg | Bin 0 -> 3733 bytes .../public/portraits/men/78.jpg | Bin 0 -> 4643 bytes .../public/portraits/men/86.jpg | Bin 0 -> 5433 bytes .../public/portraits/women/24.jpg | Bin 0 -> 11329 bytes .../public/portraits/women/65.jpg | Bin 0 -> 5972 bytes .../public/portraits/women/8.jpg | Bin 0 -> 5810 bytes .../public/portraits/women/85.jpg | Bin 0 -> 3912 bytes .../public/portraits/women/93.jpg | Bin 0 -> 4871 bytes .../public/vercel.svg | 1 + .../src/app/(auth)/layout.tsx | 12 + .../src/app/(auth)/login/page.tsx | 30 ++ .../(dashboard)/employees/[id]/edit/page.tsx | 24 ++ .../app/(dashboard)/employees/[id]/page.tsx | 21 ++ .../app/(dashboard)/employees/new/page.tsx | 18 + .../src/app/(dashboard)/employees/page.tsx | 19 + .../app/(dashboard)/expenses/[id]/page.tsx | 18 + .../src/app/(dashboard)/expenses/page.tsx | 17 + .../src/app/(dashboard)/layout.tsx | 29 ++ .../src/app/(dashboard)/page.tsx | 11 + .../src/app/layout.tsx | 21 ++ .../src/assets/images/profile-placeholder.jpg | Bin 0 -> 11940 bytes .../src/assets/svg/logo.svg | 28 ++ .../src/assets/svg/logoDark.svg | 28 ++ .../src/components/Alert.tsx | 17 + .../src/components/Button.tsx | 34 ++ .../src/components/EmployeeForm.tsx | 113 ++++++ .../src/components/ExpensesDetails.tsx | 56 +++ .../src/components/ExpensesTable.tsx | 49 +++ .../src/components/Icons/ArrowLeft.tsx | 25 ++ .../src/components/Icons/Eye.tsx | 11 + .../src/components/Icons/Loader.tsx | 25 ++ .../src/components/NavigationMenu.tsx | 25 ++ .../src/components/PageTitle.tsx | 25 ++ .../src/components/Paper.tsx | 14 + .../src/components/PersonCard.tsx | 43 +++ .../src/components/TextField.tsx | 31 ++ .../src/data/employees.json | 152 ++++++++ .../src/data/expenses.json | 342 ++++++++++++++++++ .../src/styles/global.css | 41 +++ .../02.01-pages-layout-solution/src/types.ts | 39 ++ .../tailwind.config.js | 9 + .../02.01-pages-layout-solution/tsconfig.json | 26 ++ steps/02.01-pages-layout/.env.example | 2 + steps/02.01-pages-layout/.eslintrc.json | 3 + steps/02.01-pages-layout/.gitignore | 36 ++ steps/02.01-pages-layout/README.md | 1 + steps/02.01-pages-layout/data/employees.json | 152 ++++++++ steps/02.01-pages-layout/data/expenses.json | 342 ++++++++++++++++++ steps/02.01-pages-layout/next.config.mjs | 15 + steps/02.01-pages-layout/package.json | 37 ++ steps/02.01-pages-layout/postcss.config.js | 6 + steps/02.01-pages-layout/public/next.svg | 1 + .../public/portraits/men/30.jpg | Bin 0 -> 4349 bytes .../public/portraits/men/34.jpg | Bin 0 -> 4305 bytes .../public/portraits/men/56.jpg | Bin 0 -> 3733 bytes .../public/portraits/men/78.jpg | Bin 0 -> 4643 bytes .../public/portraits/men/86.jpg | Bin 0 -> 5433 bytes .../public/portraits/women/24.jpg | Bin 0 -> 11329 bytes .../public/portraits/women/65.jpg | Bin 0 -> 5972 bytes .../public/portraits/women/8.jpg | Bin 0 -> 5810 bytes .../public/portraits/women/85.jpg | Bin 0 -> 3912 bytes .../public/portraits/women/93.jpg | Bin 0 -> 4871 bytes steps/02.01-pages-layout/public/vercel.svg | 1 + steps/02.01-pages-layout/src/app/.gitkeep | 0 .../src/assets/images/profile-placeholder.jpg | Bin 0 -> 11940 bytes .../src/assets/svg/logo.svg | 28 ++ .../src/assets/svg/logoDark.svg | 28 ++ .../src/components/Alert.tsx | 17 + .../src/components/Button.tsx | 34 ++ .../src/components/EmployeeForm.tsx | 113 ++++++ .../src/components/ExpensesDetails.tsx | 54 +++ .../src/components/ExpensesTable.tsx | 61 ++++ .../src/components/Icons/ArrowLeft.tsx | 25 ++ .../src/components/Icons/Eye.tsx | 11 + .../src/components/Icons/Loader.tsx | 25 ++ .../src/components/NavigationMenu.tsx | 25 ++ .../src/components/PageTitle.tsx | 25 ++ .../src/components/Paper.tsx | 14 + .../src/components/PersonCard.tsx | 43 +++ .../src/components/TextField.tsx | 34 ++ .../src/components/layouts/AuthLayout.tsx | 12 + .../components/layouts/DashboardLayout.tsx | 24 ++ .../02.01-pages-layout/src/styles/global.css | 41 +++ steps/02.01-pages-layout/src/types.ts | 39 ++ steps/02.01-pages-layout/tailwind.config.js | 9 + steps/02.01-pages-layout/tsconfig.json | 26 ++ steps/02.02-navigation-solution/.env.example | 2 + .../02.02-navigation-solution/.eslintrc.json | 3 + steps/02.02-navigation-solution/.gitignore | 36 ++ steps/02.02-navigation-solution/README.md | 1 + .../02.02-navigation-solution/next.config.mjs | 15 + steps/02.02-navigation-solution/package.json | 37 ++ .../postcss.config.js | 6 + .../02.02-navigation-solution/public/next.svg | 1 + .../public/portraits/men/30.jpg | Bin 0 -> 4349 bytes .../public/portraits/men/34.jpg | Bin 0 -> 4305 bytes .../public/portraits/men/56.jpg | Bin 0 -> 3733 bytes .../public/portraits/men/78.jpg | Bin 0 -> 4643 bytes .../public/portraits/men/86.jpg | Bin 0 -> 5433 bytes .../public/portraits/women/24.jpg | Bin 0 -> 11329 bytes .../public/portraits/women/65.jpg | Bin 0 -> 5972 bytes .../public/portraits/women/8.jpg | Bin 0 -> 5810 bytes .../public/portraits/women/85.jpg | Bin 0 -> 3912 bytes .../public/portraits/women/93.jpg | Bin 0 -> 4871 bytes .../public/vercel.svg | 1 + .../src/app/(auth)/layout.tsx | 12 + .../src/app/(auth)/login/page.tsx | 30 ++ .../(dashboard)/employees/[id]/edit/page.tsx | 24 ++ .../app/(dashboard)/employees/[id]/page.tsx | 21 ++ .../app/(dashboard)/employees/new/page.tsx | 18 + .../src/app/(dashboard)/employees/page.tsx | 47 +++ .../app/(dashboard)/expenses/[id]/page.tsx | 18 + .../src/app/(dashboard)/expenses/page.tsx | 17 + .../src/app/(dashboard)/layout.tsx | 29 ++ .../src/app/(dashboard)/page.tsx | 11 + .../src/app/layout.tsx | 21 ++ .../src/assets/images/profile-placeholder.jpg | Bin 0 -> 11940 bytes .../src/assets/svg/logo.svg | 28 ++ .../src/assets/svg/logoDark.svg | 28 ++ .../src/components/Alert.tsx | 17 + .../src/components/Button.tsx | 34 ++ .../src/components/EmployeeForm.tsx | 113 ++++++ .../src/components/ExpensesDetails.tsx | 56 +++ .../src/components/ExpensesTable.tsx | 61 ++++ .../src/components/Icons/ArrowLeft.tsx | 25 ++ .../src/components/Icons/Eye.tsx | 11 + .../src/components/Icons/Loader.tsx | 25 ++ .../src/components/NavigationItem.tsx | 30 ++ .../src/components/NavigationMenu.tsx | 21 ++ .../src/components/PageTitle.tsx | 25 ++ .../src/components/Pagination.tsx | 95 +++++ .../src/components/Paper.tsx | 14 + .../src/components/PersonCard.tsx | 43 +++ .../src/components/Search.tsx | 43 +++ .../src/components/TextField.tsx | 31 ++ .../src/data/employees.json | 152 ++++++++ .../src/data/expenses.json | 342 ++++++++++++++++++ .../src/functions/timing.ts | 7 + .../src/styles/global.css | 41 +++ steps/02.02-navigation-solution/src/types.ts | 39 ++ .../tailwind.config.js | 9 + steps/02.02-navigation-solution/tsconfig.json | 26 ++ steps/02.02-navigation/.env.example | 2 + steps/02.02-navigation/.eslintrc.json | 3 + steps/02.02-navigation/.gitignore | 36 ++ steps/02.02-navigation/README.md | 1 + steps/02.02-navigation/next.config.mjs | 15 + steps/02.02-navigation/package.json | 37 ++ steps/02.02-navigation/postcss.config.js | 6 + steps/02.02-navigation/public/next.svg | 1 + .../public/portraits/men/30.jpg | Bin 0 -> 4349 bytes .../public/portraits/men/34.jpg | Bin 0 -> 4305 bytes .../public/portraits/men/56.jpg | Bin 0 -> 3733 bytes .../public/portraits/men/78.jpg | Bin 0 -> 4643 bytes .../public/portraits/men/86.jpg | Bin 0 -> 5433 bytes .../public/portraits/women/24.jpg | Bin 0 -> 11329 bytes .../public/portraits/women/65.jpg | Bin 0 -> 5972 bytes .../public/portraits/women/8.jpg | Bin 0 -> 5810 bytes .../public/portraits/women/85.jpg | Bin 0 -> 3912 bytes .../public/portraits/women/93.jpg | Bin 0 -> 4871 bytes steps/02.02-navigation/public/vercel.svg | 1 + .../src/app/(auth)/layout.tsx | 12 + .../src/app/(auth)/login/page.tsx | 30 ++ .../(dashboard)/employees/[id]/edit/page.tsx | 24 ++ .../app/(dashboard)/employees/[id]/page.tsx | 21 ++ .../app/(dashboard)/employees/new/page.tsx | 18 + .../src/app/(dashboard)/employees/page.tsx | 19 + .../app/(dashboard)/expenses/[id]/page.tsx | 18 + .../src/app/(dashboard)/expenses/page.tsx | 17 + .../src/app/(dashboard)/layout.tsx | 29 ++ .../src/app/(dashboard)/page.tsx | 11 + steps/02.02-navigation/src/app/layout.tsx | 21 ++ .../src/assets/images/profile-placeholder.jpg | Bin 0 -> 11940 bytes .../02.02-navigation/src/assets/svg/logo.svg | 28 ++ .../src/assets/svg/logoDark.svg | 28 ++ .../02.02-navigation/src/components/Alert.tsx | 17 + .../src/components/Button.tsx | 34 ++ .../src/components/EmployeeForm.tsx | 113 ++++++ .../src/components/ExpensesDetails.tsx | 56 +++ .../src/components/ExpensesTable.tsx | 49 +++ .../src/components/Icons/ArrowLeft.tsx | 25 ++ .../src/components/Icons/Eye.tsx | 11 + .../src/components/Icons/Loader.tsx | 25 ++ .../src/components/NavigationMenu.tsx | 25 ++ .../src/components/PageTitle.tsx | 25 ++ .../02.02-navigation/src/components/Paper.tsx | 14 + .../src/components/PersonCard.tsx | 43 +++ .../src/components/TextField.tsx | 31 ++ .../02.02-navigation/src/data/employees.json | 152 ++++++++ steps/02.02-navigation/src/data/expenses.json | 342 ++++++++++++++++++ .../02.02-navigation/src/functions/timing.ts | 7 + steps/02.02-navigation/src/styles/global.css | 41 +++ steps/02.02-navigation/src/types.ts | 39 ++ steps/02.02-navigation/tailwind.config.js | 9 + steps/02.02-navigation/tsconfig.json | 26 ++ .../.env.example | 2 + .../.eslintrc.json | 3 + .../.gitignore | 36 ++ .../03.01-server-components-solution/.gitkeep | 0 .../README.md | 1 + .../03.01-server-components-solution/logs.txt | 0 .../next.config.mjs | 15 + .../package.json | 38 ++ .../postcss.config.js | 6 + .../public/next.svg | 1 + .../public/portraits/men/30.jpg | Bin 0 -> 4349 bytes .../public/portraits/men/34.jpg | Bin 0 -> 4305 bytes .../public/portraits/men/56.jpg | Bin 0 -> 3733 bytes .../public/portraits/men/78.jpg | Bin 0 -> 4643 bytes .../public/portraits/men/86.jpg | Bin 0 -> 5433 bytes .../public/portraits/women/24.jpg | Bin 0 -> 11329 bytes .../public/portraits/women/65.jpg | Bin 0 -> 5972 bytes .../public/portraits/women/8.jpg | Bin 0 -> 5810 bytes .../public/portraits/women/85.jpg | Bin 0 -> 3912 bytes .../public/portraits/women/93.jpg | Bin 0 -> 4871 bytes .../public/vercel.svg | 1 + .../src/app/(auth)/layout.tsx | 12 + .../src/app/(auth)/login/page.tsx | 30 ++ .../(dashboard)/employees/[id]/edit/page.tsx | 24 ++ .../app/(dashboard)/employees/[id]/page.tsx | 21 ++ .../app/(dashboard)/employees/log/page.tsx | 16 + .../app/(dashboard)/employees/new/page.tsx | 18 + .../src/app/(dashboard)/employees/page.tsx | 59 +++ .../app/(dashboard)/expenses/[id]/page.tsx | 18 + .../src/app/(dashboard)/expenses/page.tsx | 17 + .../src/app/(dashboard)/layout.tsx | 37 ++ .../src/app/(dashboard)/page.tsx | 11 + .../src/app/layout.tsx | 21 ++ .../src/assets/images/profile-placeholder.jpg | Bin 0 -> 11940 bytes .../src/assets/svg/logo.svg | 28 ++ .../src/assets/svg/logoDark.svg | 28 ++ .../src/components/Alert.tsx | 17 + .../src/components/Button.tsx | 34 ++ .../src/components/EmployeeForm.tsx | 113 ++++++ .../src/components/ExpensesDetails.tsx | 56 +++ .../src/components/ExpensesTable.tsx | 61 ++++ .../src/components/Icons/ArrowLeft.tsx | 25 ++ .../src/components/Icons/Eye.tsx | 11 + .../src/components/Icons/Loader.tsx | 25 ++ .../src/components/NavigationItem.tsx | 30 ++ .../src/components/NavigationMenu.tsx | 21 ++ .../src/components/PageTitle.tsx | 25 ++ .../src/components/Pagination.tsx | 95 +++++ .../src/components/Paper.tsx | 14 + .../src/components/PersonCard.tsx | 43 +++ .../src/components/Search.tsx | 43 +++ .../src/components/TextField.tsx | 31 ++ .../src/data/employees.json | 152 ++++++++ .../src/data/expenses.json | 342 ++++++++++++++++++ .../src/functions/timing.ts | 7 + .../src/styles/global.css | 41 +++ .../src/types.ts | 39 ++ .../tailwind.config.js | 9 + .../tsconfig.json | 26 ++ steps/03.01-server-components/.env.example | 2 + steps/03.01-server-components/.eslintrc.json | 3 + steps/03.01-server-components/.gitignore | 36 ++ steps/03.01-server-components/.gitkeep | 0 steps/03.01-server-components/README.md | 1 + steps/03.01-server-components/next.config.mjs | 15 + steps/03.01-server-components/package.json | 38 ++ .../03.01-server-components/postcss.config.js | 6 + steps/03.01-server-components/public/next.svg | 1 + .../public/portraits/men/30.jpg | Bin 0 -> 4349 bytes .../public/portraits/men/34.jpg | Bin 0 -> 4305 bytes .../public/portraits/men/56.jpg | Bin 0 -> 3733 bytes .../public/portraits/men/78.jpg | Bin 0 -> 4643 bytes .../public/portraits/men/86.jpg | Bin 0 -> 5433 bytes .../public/portraits/women/24.jpg | Bin 0 -> 11329 bytes .../public/portraits/women/65.jpg | Bin 0 -> 5972 bytes .../public/portraits/women/8.jpg | Bin 0 -> 5810 bytes .../public/portraits/women/85.jpg | Bin 0 -> 3912 bytes .../public/portraits/women/93.jpg | Bin 0 -> 4871 bytes .../03.01-server-components/public/vercel.svg | 1 + .../src/app/(auth)/layout.tsx | 12 + .../src/app/(auth)/login/page.tsx | 30 ++ .../(dashboard)/employees/[id]/edit/page.tsx | 24 ++ .../app/(dashboard)/employees/[id]/page.tsx | 21 ++ .../app/(dashboard)/employees/new/page.tsx | 18 + .../src/app/(dashboard)/employees/page.tsx | 47 +++ .../app/(dashboard)/expenses/[id]/page.tsx | 18 + .../src/app/(dashboard)/expenses/page.tsx | 17 + .../src/app/(dashboard)/layout.tsx | 30 ++ .../src/app/(dashboard)/page.tsx | 11 + .../src/app/layout.tsx | 21 ++ .../src/assets/images/profile-placeholder.jpg | Bin 0 -> 11940 bytes .../src/assets/svg/logo.svg | 28 ++ .../src/assets/svg/logoDark.svg | 28 ++ .../src/components/Alert.tsx | 17 + .../src/components/Button.tsx | 34 ++ .../src/components/EmployeeForm.tsx | 113 ++++++ .../src/components/ExpensesDetails.tsx | 56 +++ .../src/components/ExpensesTable.tsx | 61 ++++ .../src/components/Icons/ArrowLeft.tsx | 25 ++ .../src/components/Icons/Eye.tsx | 11 + .../src/components/Icons/Loader.tsx | 25 ++ .../src/components/NavigationItem.tsx | 30 ++ .../src/components/NavigationMenu.tsx | 21 ++ .../src/components/PageTitle.tsx | 25 ++ .../src/components/Pagination.tsx | 95 +++++ .../src/components/Paper.tsx | 14 + .../src/components/PersonCard.tsx | 43 +++ .../src/components/Search.tsx | 43 +++ .../src/components/TextField.tsx | 31 ++ .../src/data/employees.json | 152 ++++++++ .../src/data/expenses.json | 342 ++++++++++++++++++ .../src/functions/timing.ts | 7 + .../src/styles/global.css | 41 +++ steps/03.01-server-components/src/types.ts | 39 ++ .../tailwind.config.js | 9 + steps/03.01-server-components/tsconfig.json | 26 ++ steps/03.02-composition-solution/.env.example | 2 + .../03.02-composition-solution/.eslintrc.json | 3 + steps/03.02-composition-solution/.gitignore | 36 ++ steps/03.02-composition-solution/README.md | 1 + .../next.config.mjs | 15 + steps/03.02-composition-solution/package.json | 38 ++ .../postcss.config.js | 6 + .../public/next.svg | 1 + .../public/portraits/men/30.jpg | Bin 0 -> 4349 bytes .../public/portraits/men/34.jpg | Bin 0 -> 4305 bytes .../public/portraits/men/56.jpg | Bin 0 -> 3733 bytes .../public/portraits/men/78.jpg | Bin 0 -> 4643 bytes .../public/portraits/men/86.jpg | Bin 0 -> 5433 bytes .../public/portraits/women/24.jpg | Bin 0 -> 11329 bytes .../public/portraits/women/65.jpg | Bin 0 -> 5972 bytes .../public/portraits/women/8.jpg | Bin 0 -> 5810 bytes .../public/portraits/women/85.jpg | Bin 0 -> 3912 bytes .../public/portraits/women/93.jpg | Bin 0 -> 4871 bytes .../public/vercel.svg | 1 + .../src/app/(auth)/layout.tsx | 12 + .../src/app/(auth)/login/page.tsx | 30 ++ .../(dashboard)/employees/[id]/edit/page.tsx | 24 ++ .../app/(dashboard)/employees/[id]/page.tsx | 21 ++ .../app/(dashboard)/employees/new/page.tsx | 18 + .../src/app/(dashboard)/employees/page.tsx | 47 +++ .../app/(dashboard)/expenses/[id]/page.tsx | 18 + .../src/app/(dashboard)/expenses/page.tsx | 17 + .../src/app/(dashboard)/layout.tsx | 36 ++ .../src/app/(dashboard)/page.tsx | 11 + .../src/app/layout.tsx | 24 ++ .../src/assets/images/profile-placeholder.jpg | Bin 0 -> 11940 bytes .../src/assets/svg/logo.svg | 28 ++ .../src/assets/svg/logoDark.svg | 28 ++ .../src/components/Alert.tsx | 17 + .../src/components/Button.tsx | 34 ++ .../src/components/EmployeeForm.tsx | 113 ++++++ .../src/components/ExpensesDetails.tsx | 56 +++ .../src/components/ExpensesTable.tsx | 51 +++ .../src/components/ExpensesTableRow.tsx | 31 ++ .../src/components/Icons/ArrowLeft.tsx | 25 ++ .../src/components/Icons/Eye.tsx | 11 + .../src/components/Icons/Loader.tsx | 25 ++ .../src/components/Logo.tsx | 17 + .../src/components/NavigationItem.tsx | 30 ++ .../src/components/NavigationMenu.tsx | 21 ++ .../src/components/PageTitle.tsx | 25 ++ .../src/components/Pagination.tsx | 95 +++++ .../src/components/Paper.tsx | 14 + .../src/components/PersonCard.tsx | 43 +++ .../src/components/Search.tsx | 43 +++ .../src/components/TextField.tsx | 31 ++ .../src/components/ThemeProvider.tsx | 31 ++ .../src/data/employees.json | 152 ++++++++ .../src/data/expenses.json | 342 ++++++++++++++++++ .../src/functions/timing.ts | 7 + .../src/styles/global.css | 41 +++ steps/03.02-composition-solution/src/types.ts | 39 ++ .../tailwind.config.js | 8 + .../03.02-composition-solution/tsconfig.json | 26 ++ steps/03.02-composition/.env.example | 2 + steps/03.02-composition/.eslintrc.json | 3 + steps/03.02-composition/.gitignore | 36 ++ steps/03.02-composition/README.md | 1 + steps/03.02-composition/next.config.mjs | 15 + steps/03.02-composition/package.json | 38 ++ steps/03.02-composition/postcss.config.js | 6 + steps/03.02-composition/public/next.svg | 1 + .../public/portraits/men/30.jpg | Bin 0 -> 4349 bytes .../public/portraits/men/34.jpg | Bin 0 -> 4305 bytes .../public/portraits/men/56.jpg | Bin 0 -> 3733 bytes .../public/portraits/men/78.jpg | Bin 0 -> 4643 bytes .../public/portraits/men/86.jpg | Bin 0 -> 5433 bytes .../public/portraits/women/24.jpg | Bin 0 -> 11329 bytes .../public/portraits/women/65.jpg | Bin 0 -> 5972 bytes .../public/portraits/women/8.jpg | Bin 0 -> 5810 bytes .../public/portraits/women/85.jpg | Bin 0 -> 3912 bytes .../public/portraits/women/93.jpg | Bin 0 -> 4871 bytes steps/03.02-composition/public/vercel.svg | 1 + .../src/app/(auth)/layout.tsx | 12 + .../src/app/(auth)/login/page.tsx | 30 ++ .../(dashboard)/employees/[id]/edit/page.tsx | 24 ++ .../app/(dashboard)/employees/[id]/page.tsx | 21 ++ .../app/(dashboard)/employees/new/page.tsx | 18 + .../src/app/(dashboard)/employees/page.tsx | 47 +++ .../app/(dashboard)/expenses/[id]/page.tsx | 18 + .../src/app/(dashboard)/expenses/page.tsx | 17 + .../src/app/(dashboard)/layout.tsx | 37 ++ .../src/app/(dashboard)/page.tsx | 11 + steps/03.02-composition/src/app/layout.tsx | 21 ++ .../src/assets/images/profile-placeholder.jpg | Bin 0 -> 11940 bytes .../03.02-composition/src/assets/svg/logo.svg | 28 ++ .../src/assets/svg/logoDark.svg | 28 ++ .../src/components/Alert.tsx | 17 + .../src/components/Button.tsx | 34 ++ .../src/components/EmployeeForm.tsx | 113 ++++++ .../src/components/ExpensesDetails.tsx | 56 +++ .../src/components/ExpensesTable.tsx | 61 ++++ .../src/components/Icons/ArrowLeft.tsx | 25 ++ .../src/components/Icons/Eye.tsx | 11 + .../src/components/Icons/Loader.tsx | 25 ++ .../src/components/NavigationItem.tsx | 30 ++ .../src/components/NavigationMenu.tsx | 21 ++ .../src/components/PageTitle.tsx | 25 ++ .../src/components/Pagination.tsx | 95 +++++ .../src/components/Paper.tsx | 14 + .../src/components/PersonCard.tsx | 43 +++ .../src/components/Search.tsx | 43 +++ .../src/components/TextField.tsx | 31 ++ .../03.02-composition/src/data/employees.json | 152 ++++++++ .../03.02-composition/src/data/expenses.json | 342 ++++++++++++++++++ .../03.02-composition/src/functions/timing.ts | 7 + steps/03.02-composition/src/styles/global.css | 41 +++ steps/03.02-composition/src/types.ts | 39 ++ steps/03.02-composition/tailwind.config.js | 9 + steps/03.02-composition/tsconfig.json | 26 ++ .../04.01-data-fetching-solution/.env.example | 2 + .../.eslintrc.json | 3 + steps/04.01-data-fetching-solution/.gitignore | 36 ++ steps/04.01-data-fetching-solution/README.md | 1 + .../next.config.mjs | 15 + .../04.01-data-fetching-solution/package.json | 38 ++ .../postcss.config.js | 6 + .../public/next.svg | 1 + .../public/portraits/men/30.jpg | Bin 0 -> 4349 bytes .../public/portraits/men/34.jpg | Bin 0 -> 4305 bytes .../public/portraits/men/56.jpg | Bin 0 -> 3733 bytes .../public/portraits/men/78.jpg | Bin 0 -> 4643 bytes .../public/portraits/men/86.jpg | Bin 0 -> 5433 bytes .../public/portraits/women/24.jpg | Bin 0 -> 11329 bytes .../public/portraits/women/65.jpg | Bin 0 -> 5972 bytes .../public/portraits/women/8.jpg | Bin 0 -> 5810 bytes .../public/portraits/women/85.jpg | Bin 0 -> 3912 bytes .../public/portraits/women/93.jpg | Bin 0 -> 4871 bytes .../public/vercel.svg | 1 + .../src/api/common.ts | 17 + .../src/api/error.ts | 8 + .../src/api/expenses.ts | 19 + .../src/api/people.ts | 19 + .../src/app/(auth)/layout.tsx | 12 + .../src/app/(auth)/login/page.tsx | 30 ++ .../(dashboard)/employees/[id]/edit/page.tsx | 24 ++ .../app/(dashboard)/employees/[id]/page.tsx | 22 ++ .../app/(dashboard)/employees/new/page.tsx | 18 + .../src/app/(dashboard)/employees/page.tsx | 45 +++ .../app/(dashboard)/expenses/[id]/page.tsx | 17 + .../src/app/(dashboard)/expenses/page.tsx | 16 + .../src/app/(dashboard)/layout.tsx | 37 ++ .../src/app/(dashboard)/page.tsx | 11 + .../src/app/layout.tsx | 21 ++ .../src/app/rest/expenses/route.ts | 9 + .../src/assets/images/profile-placeholder.jpg | Bin 0 -> 11940 bytes .../src/assets/svg/logo.svg | 28 ++ .../src/assets/svg/logoDark.svg | 28 ++ .../src/components/Alert.tsx | 17 + .../src/components/Button.tsx | 34 ++ .../src/components/EmployeeExpenses.tsx | 46 +++ .../src/components/EmployeeForm.tsx | 113 ++++++ .../src/components/ExpensesDetails.tsx | 56 +++ .../src/components/ExpensesTable.tsx | 61 ++++ .../src/components/Icons/ArrowLeft.tsx | 25 ++ .../src/components/Icons/Eye.tsx | 11 + .../src/components/Icons/Loader.tsx | 25 ++ .../src/components/NavigationItem.tsx | 30 ++ .../src/components/NavigationMenu.tsx | 21 ++ .../src/components/PageTitle.tsx | 25 ++ .../src/components/Pagination.tsx | 95 +++++ .../src/components/Paper.tsx | 14 + .../src/components/PersonCard.tsx | 43 +++ .../src/components/Search.tsx | 43 +++ .../src/components/TextField.tsx | 31 ++ .../src/functions/timing.ts | 7 + .../src/styles/global.css | 41 +++ .../04.01-data-fetching-solution/src/types.ts | 39 ++ .../tailwind.config.js | 9 + .../tsconfig.json | 26 ++ steps/04.01-data-fetching/.env.example | 2 + steps/04.01-data-fetching/.eslintrc.json | 3 + steps/04.01-data-fetching/.gitignore | 36 ++ steps/04.01-data-fetching/README.md | 1 + steps/04.01-data-fetching/next.config.mjs | 15 + steps/04.01-data-fetching/package.json | 38 ++ steps/04.01-data-fetching/postcss.config.js | 6 + steps/04.01-data-fetching/public/next.svg | 1 + .../public/portraits/men/30.jpg | Bin 0 -> 4349 bytes .../public/portraits/men/34.jpg | Bin 0 -> 4305 bytes .../public/portraits/men/56.jpg | Bin 0 -> 3733 bytes .../public/portraits/men/78.jpg | Bin 0 -> 4643 bytes .../public/portraits/men/86.jpg | Bin 0 -> 5433 bytes .../public/portraits/women/24.jpg | Bin 0 -> 11329 bytes .../public/portraits/women/65.jpg | Bin 0 -> 5972 bytes .../public/portraits/women/8.jpg | Bin 0 -> 5810 bytes .../public/portraits/women/85.jpg | Bin 0 -> 3912 bytes .../public/portraits/women/93.jpg | Bin 0 -> 4871 bytes steps/04.01-data-fetching/public/vercel.svg | 1 + steps/04.01-data-fetching/src/api/common.ts | 17 + steps/04.01-data-fetching/src/api/error.ts | 8 + .../src/app/(auth)/layout.tsx | 12 + .../src/app/(auth)/login/page.tsx | 30 ++ .../(dashboard)/employees/[id]/edit/page.tsx | 24 ++ .../app/(dashboard)/employees/[id]/page.tsx | 22 ++ .../app/(dashboard)/employees/new/page.tsx | 18 + .../src/app/(dashboard)/employees/page.tsx | 47 +++ .../app/(dashboard)/expenses/[id]/page.tsx | 18 + .../src/app/(dashboard)/expenses/page.tsx | 17 + .../src/app/(dashboard)/layout.tsx | 37 ++ .../src/app/(dashboard)/page.tsx | 11 + steps/04.01-data-fetching/src/app/layout.tsx | 21 ++ .../src/assets/images/profile-placeholder.jpg | Bin 0 -> 11940 bytes .../src/assets/svg/logo.svg | 28 ++ .../src/assets/svg/logoDark.svg | 28 ++ .../src/components/Alert.tsx | 17 + .../src/components/Button.tsx | 34 ++ .../src/components/EmployeeExpenses.tsx | 46 +++ .../src/components/EmployeeForm.tsx | 113 ++++++ .../src/components/ExpensesDetails.tsx | 56 +++ .../src/components/ExpensesTable.tsx | 61 ++++ .../src/components/Icons/ArrowLeft.tsx | 25 ++ .../src/components/Icons/Eye.tsx | 11 + .../src/components/Icons/Loader.tsx | 25 ++ .../src/components/NavigationItem.tsx | 30 ++ .../src/components/NavigationMenu.tsx | 21 ++ .../src/components/PageTitle.tsx | 25 ++ .../src/components/Pagination.tsx | 95 +++++ .../src/components/Paper.tsx | 14 + .../src/components/PersonCard.tsx | 43 +++ .../src/components/Search.tsx | 43 +++ .../src/components/TextField.tsx | 31 ++ .../src/data/employees.json | 152 ++++++++ .../src/data/expenses.json | 342 ++++++++++++++++++ .../src/functions/timing.ts | 7 + .../04.01-data-fetching/src/styles/global.css | 41 +++ steps/04.01-data-fetching/src/types.ts | 39 ++ steps/04.01-data-fetching/tailwind.config.js | 9 + steps/04.01-data-fetching/tsconfig.json | 26 ++ steps/04.02-caching-solution/.env.example | 2 + steps/04.02-caching-solution/.eslintrc.json | 3 + steps/04.02-caching-solution/.gitignore | 36 ++ steps/04.02-caching-solution/README.md | 1 + steps/04.02-caching-solution/next.config.mjs | 15 + steps/04.02-caching-solution/package.json | 38 ++ .../04.02-caching-solution/postcss.config.js | 6 + steps/04.02-caching-solution/public/next.svg | 1 + .../public/portraits/men/30.jpg | Bin 0 -> 4349 bytes .../public/portraits/men/34.jpg | Bin 0 -> 4305 bytes .../public/portraits/men/56.jpg | Bin 0 -> 3733 bytes .../public/portraits/men/78.jpg | Bin 0 -> 4643 bytes .../public/portraits/men/86.jpg | Bin 0 -> 5433 bytes .../public/portraits/women/24.jpg | Bin 0 -> 11329 bytes .../public/portraits/women/65.jpg | Bin 0 -> 5972 bytes .../public/portraits/women/8.jpg | Bin 0 -> 5810 bytes .../public/portraits/women/85.jpg | Bin 0 -> 3912 bytes .../public/portraits/women/93.jpg | Bin 0 -> 4871 bytes .../04.02-caching-solution/public/vercel.svg | 1 + .../04.02-caching-solution/src/api/common.ts | 17 + steps/04.02-caching-solution/src/api/error.ts | 8 + .../src/api/expenses.ts | 19 + .../04.02-caching-solution/src/api/people.ts | 19 + .../src/app/(auth)/layout.tsx | 12 + .../src/app/(auth)/login/page.tsx | 30 ++ .../(dashboard)/employees/[id]/edit/page.tsx | 24 ++ .../app/(dashboard)/employees/[id]/page.tsx | 22 ++ .../app/(dashboard)/employees/new/page.tsx | 18 + .../src/app/(dashboard)/employees/page.tsx | 45 +++ .../app/(dashboard)/expenses/[id]/page.tsx | 17 + .../src/app/(dashboard)/expenses/page.tsx | 16 + .../src/app/(dashboard)/layout.tsx | 37 ++ .../src/app/(dashboard)/page.tsx | 11 + .../src/app/api/revalidate/route.ts | 17 + .../04.02-caching-solution/src/app/layout.tsx | 21 ++ .../src/app/rest/expenses/route.ts | 9 + .../src/assets/images/profile-placeholder.jpg | Bin 0 -> 11940 bytes .../src/assets/svg/logo.svg | 28 ++ .../src/assets/svg/logoDark.svg | 28 ++ .../src/components/Alert.tsx | 17 + .../src/components/Button.tsx | 34 ++ .../src/components/EmployeeExpenses.tsx | 46 +++ .../src/components/EmployeeForm.tsx | 113 ++++++ .../src/components/ExpensesDetails.tsx | 56 +++ .../src/components/ExpensesTable.tsx | 61 ++++ .../src/components/Icons/ArrowLeft.tsx | 25 ++ .../src/components/Icons/Eye.tsx | 11 + .../src/components/Icons/Loader.tsx | 25 ++ .../src/components/NavigationItem.tsx | 30 ++ .../src/components/NavigationMenu.tsx | 21 ++ .../src/components/PageTitle.tsx | 25 ++ .../src/components/Pagination.tsx | 95 +++++ .../src/components/Paper.tsx | 14 + .../src/components/PersonCard.tsx | 43 +++ .../src/components/Search.tsx | 43 +++ .../src/components/TextField.tsx | 31 ++ .../src/functions/timing.ts | 7 + .../src/styles/global.css | 41 +++ steps/04.02-caching-solution/src/types.ts | 39 ++ .../04.02-caching-solution/tailwind.config.js | 9 + steps/04.02-caching-solution/tsconfig.json | 26 ++ steps/04.02-caching/.env.example | 2 + steps/04.02-caching/.eslintrc.json | 3 + steps/04.02-caching/.gitignore | 36 ++ steps/04.02-caching/README.md | 1 + steps/04.02-caching/next.config.mjs | 15 + steps/04.02-caching/package.json | 38 ++ steps/04.02-caching/postcss.config.js | 6 + steps/04.02-caching/public/next.svg | 1 + .../04.02-caching/public/portraits/men/30.jpg | Bin 0 -> 4349 bytes .../04.02-caching/public/portraits/men/34.jpg | Bin 0 -> 4305 bytes .../04.02-caching/public/portraits/men/56.jpg | Bin 0 -> 3733 bytes .../04.02-caching/public/portraits/men/78.jpg | Bin 0 -> 4643 bytes .../04.02-caching/public/portraits/men/86.jpg | Bin 0 -> 5433 bytes .../public/portraits/women/24.jpg | Bin 0 -> 11329 bytes .../public/portraits/women/65.jpg | Bin 0 -> 5972 bytes .../public/portraits/women/8.jpg | Bin 0 -> 5810 bytes .../public/portraits/women/85.jpg | Bin 0 -> 3912 bytes .../public/portraits/women/93.jpg | Bin 0 -> 4871 bytes steps/04.02-caching/public/vercel.svg | 1 + steps/04.02-caching/src/api/common.ts | 17 + steps/04.02-caching/src/api/error.ts | 8 + steps/04.02-caching/src/api/expenses.ts | 19 + steps/04.02-caching/src/api/people.ts | 19 + steps/04.02-caching/src/app/(auth)/layout.tsx | 12 + .../src/app/(auth)/login/page.tsx | 30 ++ .../(dashboard)/employees/[id]/edit/page.tsx | 24 ++ .../app/(dashboard)/employees/[id]/page.tsx | 22 ++ .../app/(dashboard)/employees/new/page.tsx | 18 + .../src/app/(dashboard)/employees/page.tsx | 45 +++ .../app/(dashboard)/expenses/[id]/page.tsx | 17 + .../src/app/(dashboard)/expenses/page.tsx | 16 + .../src/app/(dashboard)/layout.tsx | 37 ++ .../src/app/(dashboard)/page.tsx | 11 + steps/04.02-caching/src/app/layout.tsx | 21 ++ .../src/app/rest/expenses/route.ts | 9 + .../src/assets/images/profile-placeholder.jpg | Bin 0 -> 11940 bytes steps/04.02-caching/src/assets/svg/logo.svg | 28 ++ .../04.02-caching/src/assets/svg/logoDark.svg | 28 ++ steps/04.02-caching/src/components/Alert.tsx | 17 + steps/04.02-caching/src/components/Button.tsx | 34 ++ .../src/components/EmployeeExpenses.tsx | 46 +++ .../src/components/EmployeeForm.tsx | 113 ++++++ .../src/components/ExpensesDetails.tsx | 56 +++ .../src/components/ExpensesTable.tsx | 61 ++++ .../src/components/Icons/ArrowLeft.tsx | 25 ++ .../src/components/Icons/Eye.tsx | 11 + .../src/components/Icons/Loader.tsx | 25 ++ .../src/components/NavigationItem.tsx | 30 ++ .../src/components/NavigationMenu.tsx | 21 ++ .../src/components/PageTitle.tsx | 25 ++ .../src/components/Pagination.tsx | 95 +++++ steps/04.02-caching/src/components/Paper.tsx | 14 + .../src/components/PersonCard.tsx | 43 +++ steps/04.02-caching/src/components/Search.tsx | 43 +++ .../src/components/TextField.tsx | 31 ++ steps/04.02-caching/src/functions/timing.ts | 7 + steps/04.02-caching/src/styles/global.css | 41 +++ steps/04.02-caching/src/types.ts | 39 ++ steps/04.02-caching/tailwind.config.js | 9 + steps/04.02-caching/tsconfig.json | 26 ++ steps/05.01-server-actions-solution/.gitkeep | 0 steps/05.01-server-actions/.gitkeep | 0 steps/05.02-form-hooks-solution/.gitkeep | 0 steps/05.02-form-hooks/.gitkeep | 0 .../06.01-error-boundaries-solution/.gitkeep | 0 steps/06.01-error-boundaries/.gitkeep | 0 steps/06.02-expected-errors-solution/.gitkeep | 0 steps/06.02-expected-errors/.gitkeep | 0 steps/07.01-lifecycles-solution/.gitkeep | 0 steps/07.01-lifecycles/.gitkeep | 0 steps/07.02-middleware-solution/.gitkeep | 0 steps/07.02-middleware/.gitkeep | 0 .../08.01-rendering-methods-solution/.gitkeep | 0 steps/08.01-rendering-methods/.gitkeep | 0 steps/08.02-suspense-solution/.gitkeep | 0 steps/08.02-suspense/.gitkeep | 0 steps/api/controllers/employees.js | 2 +- steps/api/controllers/expenses.js | 2 +- 772 files changed, 18660 insertions(+), 48 deletions(-) delete mode 100644 steps/00-sfeir-people/README.md delete mode 100644 steps/00-sfeir-people/src/components/NavigationMenu/index.ts rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/.env.example (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/.eslintrc.json (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/.gitignore (100%) create mode 100644 steps/00.00-sfeir-people-demo/README.md rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/next.config.mjs (73%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/package.json (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/postcss.config.js (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/public/next.svg (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/public/vercel.svg (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/src/api/common.ts (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/src/api/error.ts (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/src/api/expenses.ts (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/src/api/people.ts (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/src/app/(auth)/actions.ts (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/src/app/(auth)/layout.tsx (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/src/app/(auth)/login/page.tsx (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/src/app/(dashboard)/(home)/@employeesSlot/error.tsx (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/src/app/(dashboard)/(home)/@employeesSlot/loading.tsx (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/src/app/(dashboard)/(home)/@employeesSlot/page.tsx (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/src/app/(dashboard)/(home)/@expensesSlot/error.tsx (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/src/app/(dashboard)/(home)/@expensesSlot/loading.tsx (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/src/app/(dashboard)/(home)/@expensesSlot/page.tsx (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/src/app/(dashboard)/(home)/layout.tsx (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/src/app/(dashboard)/@modal/(.)expenses/[id]/page.tsx (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/src/app/(dashboard)/@modal/default.tsx (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/src/app/(dashboard)/employees/[id]/edit/page.tsx (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/src/app/(dashboard)/employees/[id]/page.tsx (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/src/app/(dashboard)/employees/actions.ts (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/src/app/(dashboard)/employees/error.tsx (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/src/app/(dashboard)/employees/new/page.tsx (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/src/app/(dashboard)/employees/page.tsx (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/src/app/(dashboard)/expenses/[id]/page.tsx (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/src/app/(dashboard)/expenses/page.tsx (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/src/app/(dashboard)/layout.tsx (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/src/app/api/revalidate/route.ts (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/src/app/error.tsx (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/src/app/layout.tsx (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/src/assets/images/profile-placeholder.jpg (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/src/assets/svg/logo.svg (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/src/assets/svg/logoDark.svg (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/src/components/Alert.tsx (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/src/components/Button.tsx (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/src/components/EmployeeExpenses.tsx (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/src/components/EmployeeForm.tsx (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/src/components/ExpenseDetails.tsx (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/src/components/ExpenseDetailsLoading.tsx (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/src/components/ExpensesDetailsStructure.tsx (83%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/src/components/ExpensesTable.tsx (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/src/components/FormSubmitButton.tsx (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/src/components/Icons/ArrowLeft.tsx (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/src/components/Icons/Eye.tsx (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/src/components/Icons/Loader.tsx (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/src/components/InterceptionModal.tsx (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/src/components/Logo.tsx (100%) rename steps/{00-sfeir-people/src/components/NavigationMenu => 00.00-sfeir-people-demo/src/components}/Logout.tsx (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/src/components/Modal.tsx (100%) rename steps/{00-sfeir-people/src/components/NavigationMenu => 00.00-sfeir-people-demo/src/components}/NavigationItem.tsx (100%) rename steps/{00-sfeir-people/src/components/NavigationMenu => 00.00-sfeir-people-demo/src/components}/NavigationMenu.tsx (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/src/components/PageTitle.tsx (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/src/components/Pagination.tsx (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/src/components/Paper.tsx (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/src/components/PersonCard.tsx (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/src/components/Search.tsx (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/src/components/Skeleton.tsx (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/src/components/TableLoading.tsx (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/src/components/TextField.tsx (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/src/components/ThemeProvider.tsx (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/src/functions/auth.ts (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/src/functions/data.ts (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/src/functions/timing.ts (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/src/middleware.ts (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/src/styles/global.css (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/src/types.ts (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/tailwind.config.js (100%) rename steps/{00-sfeir-people => 00.00-sfeir-people-demo}/tsconfig.json (100%) create mode 100644 steps/02.01-pages-layout-solution/.env.example create mode 100644 steps/02.01-pages-layout-solution/.eslintrc.json create mode 100644 steps/02.01-pages-layout-solution/.gitignore create mode 100644 steps/02.01-pages-layout-solution/README.md create mode 100644 steps/02.01-pages-layout-solution/next.config.mjs create mode 100644 steps/02.01-pages-layout-solution/package.json create mode 100644 steps/02.01-pages-layout-solution/postcss.config.js create mode 100644 steps/02.01-pages-layout-solution/public/next.svg create mode 100644 steps/02.01-pages-layout-solution/public/portraits/men/30.jpg create mode 100644 steps/02.01-pages-layout-solution/public/portraits/men/34.jpg create mode 100644 steps/02.01-pages-layout-solution/public/portraits/men/56.jpg create mode 100644 steps/02.01-pages-layout-solution/public/portraits/men/78.jpg create mode 100644 steps/02.01-pages-layout-solution/public/portraits/men/86.jpg create mode 100644 steps/02.01-pages-layout-solution/public/portraits/women/24.jpg create mode 100644 steps/02.01-pages-layout-solution/public/portraits/women/65.jpg create mode 100644 steps/02.01-pages-layout-solution/public/portraits/women/8.jpg create mode 100644 steps/02.01-pages-layout-solution/public/portraits/women/85.jpg create mode 100644 steps/02.01-pages-layout-solution/public/portraits/women/93.jpg create mode 100644 steps/02.01-pages-layout-solution/public/vercel.svg create mode 100644 steps/02.01-pages-layout-solution/src/app/(auth)/layout.tsx create mode 100644 steps/02.01-pages-layout-solution/src/app/(auth)/login/page.tsx create mode 100644 steps/02.01-pages-layout-solution/src/app/(dashboard)/employees/[id]/edit/page.tsx create mode 100644 steps/02.01-pages-layout-solution/src/app/(dashboard)/employees/[id]/page.tsx create mode 100644 steps/02.01-pages-layout-solution/src/app/(dashboard)/employees/new/page.tsx create mode 100644 steps/02.01-pages-layout-solution/src/app/(dashboard)/employees/page.tsx create mode 100644 steps/02.01-pages-layout-solution/src/app/(dashboard)/expenses/[id]/page.tsx create mode 100644 steps/02.01-pages-layout-solution/src/app/(dashboard)/expenses/page.tsx create mode 100644 steps/02.01-pages-layout-solution/src/app/(dashboard)/layout.tsx create mode 100644 steps/02.01-pages-layout-solution/src/app/(dashboard)/page.tsx create mode 100644 steps/02.01-pages-layout-solution/src/app/layout.tsx create mode 100644 steps/02.01-pages-layout-solution/src/assets/images/profile-placeholder.jpg create mode 100644 steps/02.01-pages-layout-solution/src/assets/svg/logo.svg create mode 100644 steps/02.01-pages-layout-solution/src/assets/svg/logoDark.svg create mode 100644 steps/02.01-pages-layout-solution/src/components/Alert.tsx create mode 100644 steps/02.01-pages-layout-solution/src/components/Button.tsx create mode 100644 steps/02.01-pages-layout-solution/src/components/EmployeeForm.tsx create mode 100644 steps/02.01-pages-layout-solution/src/components/ExpensesDetails.tsx create mode 100644 steps/02.01-pages-layout-solution/src/components/ExpensesTable.tsx create mode 100644 steps/02.01-pages-layout-solution/src/components/Icons/ArrowLeft.tsx create mode 100644 steps/02.01-pages-layout-solution/src/components/Icons/Eye.tsx create mode 100644 steps/02.01-pages-layout-solution/src/components/Icons/Loader.tsx create mode 100644 steps/02.01-pages-layout-solution/src/components/NavigationMenu.tsx create mode 100644 steps/02.01-pages-layout-solution/src/components/PageTitle.tsx create mode 100644 steps/02.01-pages-layout-solution/src/components/Paper.tsx create mode 100644 steps/02.01-pages-layout-solution/src/components/PersonCard.tsx create mode 100644 steps/02.01-pages-layout-solution/src/components/TextField.tsx create mode 100644 steps/02.01-pages-layout-solution/src/data/employees.json create mode 100644 steps/02.01-pages-layout-solution/src/data/expenses.json create mode 100644 steps/02.01-pages-layout-solution/src/styles/global.css create mode 100644 steps/02.01-pages-layout-solution/src/types.ts create mode 100644 steps/02.01-pages-layout-solution/tailwind.config.js create mode 100644 steps/02.01-pages-layout-solution/tsconfig.json create mode 100644 steps/02.01-pages-layout/.env.example create mode 100644 steps/02.01-pages-layout/.eslintrc.json create mode 100644 steps/02.01-pages-layout/.gitignore create mode 100644 steps/02.01-pages-layout/README.md create mode 100644 steps/02.01-pages-layout/data/employees.json create mode 100644 steps/02.01-pages-layout/data/expenses.json create mode 100644 steps/02.01-pages-layout/next.config.mjs create mode 100644 steps/02.01-pages-layout/package.json create mode 100644 steps/02.01-pages-layout/postcss.config.js create mode 100644 steps/02.01-pages-layout/public/next.svg create mode 100644 steps/02.01-pages-layout/public/portraits/men/30.jpg create mode 100644 steps/02.01-pages-layout/public/portraits/men/34.jpg create mode 100644 steps/02.01-pages-layout/public/portraits/men/56.jpg create mode 100644 steps/02.01-pages-layout/public/portraits/men/78.jpg create mode 100644 steps/02.01-pages-layout/public/portraits/men/86.jpg create mode 100644 steps/02.01-pages-layout/public/portraits/women/24.jpg create mode 100644 steps/02.01-pages-layout/public/portraits/women/65.jpg create mode 100644 steps/02.01-pages-layout/public/portraits/women/8.jpg create mode 100644 steps/02.01-pages-layout/public/portraits/women/85.jpg create mode 100644 steps/02.01-pages-layout/public/portraits/women/93.jpg create mode 100644 steps/02.01-pages-layout/public/vercel.svg create mode 100644 steps/02.01-pages-layout/src/app/.gitkeep create mode 100644 steps/02.01-pages-layout/src/assets/images/profile-placeholder.jpg create mode 100644 steps/02.01-pages-layout/src/assets/svg/logo.svg create mode 100644 steps/02.01-pages-layout/src/assets/svg/logoDark.svg create mode 100644 steps/02.01-pages-layout/src/components/Alert.tsx create mode 100644 steps/02.01-pages-layout/src/components/Button.tsx create mode 100644 steps/02.01-pages-layout/src/components/EmployeeForm.tsx create mode 100644 steps/02.01-pages-layout/src/components/ExpensesDetails.tsx create mode 100644 steps/02.01-pages-layout/src/components/ExpensesTable.tsx create mode 100644 steps/02.01-pages-layout/src/components/Icons/ArrowLeft.tsx create mode 100644 steps/02.01-pages-layout/src/components/Icons/Eye.tsx create mode 100644 steps/02.01-pages-layout/src/components/Icons/Loader.tsx create mode 100644 steps/02.01-pages-layout/src/components/NavigationMenu.tsx create mode 100644 steps/02.01-pages-layout/src/components/PageTitle.tsx create mode 100644 steps/02.01-pages-layout/src/components/Paper.tsx create mode 100644 steps/02.01-pages-layout/src/components/PersonCard.tsx create mode 100644 steps/02.01-pages-layout/src/components/TextField.tsx create mode 100644 steps/02.01-pages-layout/src/components/layouts/AuthLayout.tsx create mode 100644 steps/02.01-pages-layout/src/components/layouts/DashboardLayout.tsx create mode 100644 steps/02.01-pages-layout/src/styles/global.css create mode 100644 steps/02.01-pages-layout/src/types.ts create mode 100644 steps/02.01-pages-layout/tailwind.config.js create mode 100644 steps/02.01-pages-layout/tsconfig.json create mode 100644 steps/02.02-navigation-solution/.env.example create mode 100644 steps/02.02-navigation-solution/.eslintrc.json create mode 100644 steps/02.02-navigation-solution/.gitignore create mode 100644 steps/02.02-navigation-solution/README.md create mode 100644 steps/02.02-navigation-solution/next.config.mjs create mode 100644 steps/02.02-navigation-solution/package.json create mode 100644 steps/02.02-navigation-solution/postcss.config.js create mode 100644 steps/02.02-navigation-solution/public/next.svg create mode 100644 steps/02.02-navigation-solution/public/portraits/men/30.jpg create mode 100644 steps/02.02-navigation-solution/public/portraits/men/34.jpg create mode 100644 steps/02.02-navigation-solution/public/portraits/men/56.jpg create mode 100644 steps/02.02-navigation-solution/public/portraits/men/78.jpg create mode 100644 steps/02.02-navigation-solution/public/portraits/men/86.jpg create mode 100644 steps/02.02-navigation-solution/public/portraits/women/24.jpg create mode 100644 steps/02.02-navigation-solution/public/portraits/women/65.jpg create mode 100644 steps/02.02-navigation-solution/public/portraits/women/8.jpg create mode 100644 steps/02.02-navigation-solution/public/portraits/women/85.jpg create mode 100644 steps/02.02-navigation-solution/public/portraits/women/93.jpg create mode 100644 steps/02.02-navigation-solution/public/vercel.svg create mode 100644 steps/02.02-navigation-solution/src/app/(auth)/layout.tsx create mode 100644 steps/02.02-navigation-solution/src/app/(auth)/login/page.tsx create mode 100644 steps/02.02-navigation-solution/src/app/(dashboard)/employees/[id]/edit/page.tsx create mode 100644 steps/02.02-navigation-solution/src/app/(dashboard)/employees/[id]/page.tsx create mode 100644 steps/02.02-navigation-solution/src/app/(dashboard)/employees/new/page.tsx create mode 100644 steps/02.02-navigation-solution/src/app/(dashboard)/employees/page.tsx create mode 100644 steps/02.02-navigation-solution/src/app/(dashboard)/expenses/[id]/page.tsx create mode 100644 steps/02.02-navigation-solution/src/app/(dashboard)/expenses/page.tsx create mode 100644 steps/02.02-navigation-solution/src/app/(dashboard)/layout.tsx create mode 100644 steps/02.02-navigation-solution/src/app/(dashboard)/page.tsx create mode 100644 steps/02.02-navigation-solution/src/app/layout.tsx create mode 100644 steps/02.02-navigation-solution/src/assets/images/profile-placeholder.jpg create mode 100644 steps/02.02-navigation-solution/src/assets/svg/logo.svg create mode 100644 steps/02.02-navigation-solution/src/assets/svg/logoDark.svg create mode 100644 steps/02.02-navigation-solution/src/components/Alert.tsx create mode 100644 steps/02.02-navigation-solution/src/components/Button.tsx create mode 100644 steps/02.02-navigation-solution/src/components/EmployeeForm.tsx create mode 100644 steps/02.02-navigation-solution/src/components/ExpensesDetails.tsx create mode 100644 steps/02.02-navigation-solution/src/components/ExpensesTable.tsx create mode 100644 steps/02.02-navigation-solution/src/components/Icons/ArrowLeft.tsx create mode 100644 steps/02.02-navigation-solution/src/components/Icons/Eye.tsx create mode 100644 steps/02.02-navigation-solution/src/components/Icons/Loader.tsx create mode 100644 steps/02.02-navigation-solution/src/components/NavigationItem.tsx create mode 100644 steps/02.02-navigation-solution/src/components/NavigationMenu.tsx create mode 100644 steps/02.02-navigation-solution/src/components/PageTitle.tsx create mode 100644 steps/02.02-navigation-solution/src/components/Pagination.tsx create mode 100644 steps/02.02-navigation-solution/src/components/Paper.tsx create mode 100644 steps/02.02-navigation-solution/src/components/PersonCard.tsx create mode 100644 steps/02.02-navigation-solution/src/components/Search.tsx create mode 100644 steps/02.02-navigation-solution/src/components/TextField.tsx create mode 100644 steps/02.02-navigation-solution/src/data/employees.json create mode 100644 steps/02.02-navigation-solution/src/data/expenses.json create mode 100644 steps/02.02-navigation-solution/src/functions/timing.ts create mode 100644 steps/02.02-navigation-solution/src/styles/global.css create mode 100644 steps/02.02-navigation-solution/src/types.ts create mode 100644 steps/02.02-navigation-solution/tailwind.config.js create mode 100644 steps/02.02-navigation-solution/tsconfig.json create mode 100644 steps/02.02-navigation/.env.example create mode 100644 steps/02.02-navigation/.eslintrc.json create mode 100644 steps/02.02-navigation/.gitignore create mode 100644 steps/02.02-navigation/README.md create mode 100644 steps/02.02-navigation/next.config.mjs create mode 100644 steps/02.02-navigation/package.json create mode 100644 steps/02.02-navigation/postcss.config.js create mode 100644 steps/02.02-navigation/public/next.svg create mode 100644 steps/02.02-navigation/public/portraits/men/30.jpg create mode 100644 steps/02.02-navigation/public/portraits/men/34.jpg create mode 100644 steps/02.02-navigation/public/portraits/men/56.jpg create mode 100644 steps/02.02-navigation/public/portraits/men/78.jpg create mode 100644 steps/02.02-navigation/public/portraits/men/86.jpg create mode 100644 steps/02.02-navigation/public/portraits/women/24.jpg create mode 100644 steps/02.02-navigation/public/portraits/women/65.jpg create mode 100644 steps/02.02-navigation/public/portraits/women/8.jpg create mode 100644 steps/02.02-navigation/public/portraits/women/85.jpg create mode 100644 steps/02.02-navigation/public/portraits/women/93.jpg create mode 100644 steps/02.02-navigation/public/vercel.svg create mode 100644 steps/02.02-navigation/src/app/(auth)/layout.tsx create mode 100644 steps/02.02-navigation/src/app/(auth)/login/page.tsx create mode 100644 steps/02.02-navigation/src/app/(dashboard)/employees/[id]/edit/page.tsx create mode 100644 steps/02.02-navigation/src/app/(dashboard)/employees/[id]/page.tsx create mode 100644 steps/02.02-navigation/src/app/(dashboard)/employees/new/page.tsx create mode 100644 steps/02.02-navigation/src/app/(dashboard)/employees/page.tsx create mode 100644 steps/02.02-navigation/src/app/(dashboard)/expenses/[id]/page.tsx create mode 100644 steps/02.02-navigation/src/app/(dashboard)/expenses/page.tsx create mode 100644 steps/02.02-navigation/src/app/(dashboard)/layout.tsx create mode 100644 steps/02.02-navigation/src/app/(dashboard)/page.tsx create mode 100644 steps/02.02-navigation/src/app/layout.tsx create mode 100644 steps/02.02-navigation/src/assets/images/profile-placeholder.jpg create mode 100644 steps/02.02-navigation/src/assets/svg/logo.svg create mode 100644 steps/02.02-navigation/src/assets/svg/logoDark.svg create mode 100644 steps/02.02-navigation/src/components/Alert.tsx create mode 100644 steps/02.02-navigation/src/components/Button.tsx create mode 100644 steps/02.02-navigation/src/components/EmployeeForm.tsx create mode 100644 steps/02.02-navigation/src/components/ExpensesDetails.tsx create mode 100644 steps/02.02-navigation/src/components/ExpensesTable.tsx create mode 100644 steps/02.02-navigation/src/components/Icons/ArrowLeft.tsx create mode 100644 steps/02.02-navigation/src/components/Icons/Eye.tsx create mode 100644 steps/02.02-navigation/src/components/Icons/Loader.tsx create mode 100644 steps/02.02-navigation/src/components/NavigationMenu.tsx create mode 100644 steps/02.02-navigation/src/components/PageTitle.tsx create mode 100644 steps/02.02-navigation/src/components/Paper.tsx create mode 100644 steps/02.02-navigation/src/components/PersonCard.tsx create mode 100644 steps/02.02-navigation/src/components/TextField.tsx create mode 100644 steps/02.02-navigation/src/data/employees.json create mode 100644 steps/02.02-navigation/src/data/expenses.json create mode 100644 steps/02.02-navigation/src/functions/timing.ts create mode 100644 steps/02.02-navigation/src/styles/global.css create mode 100644 steps/02.02-navigation/src/types.ts create mode 100644 steps/02.02-navigation/tailwind.config.js create mode 100644 steps/02.02-navigation/tsconfig.json create mode 100644 steps/03.01-server-components-solution/.env.example create mode 100644 steps/03.01-server-components-solution/.eslintrc.json create mode 100644 steps/03.01-server-components-solution/.gitignore create mode 100644 steps/03.01-server-components-solution/.gitkeep create mode 100644 steps/03.01-server-components-solution/README.md create mode 100644 steps/03.01-server-components-solution/logs.txt create mode 100644 steps/03.01-server-components-solution/next.config.mjs create mode 100644 steps/03.01-server-components-solution/package.json create mode 100644 steps/03.01-server-components-solution/postcss.config.js create mode 100644 steps/03.01-server-components-solution/public/next.svg create mode 100644 steps/03.01-server-components-solution/public/portraits/men/30.jpg create mode 100644 steps/03.01-server-components-solution/public/portraits/men/34.jpg create mode 100644 steps/03.01-server-components-solution/public/portraits/men/56.jpg create mode 100644 steps/03.01-server-components-solution/public/portraits/men/78.jpg create mode 100644 steps/03.01-server-components-solution/public/portraits/men/86.jpg create mode 100644 steps/03.01-server-components-solution/public/portraits/women/24.jpg create mode 100644 steps/03.01-server-components-solution/public/portraits/women/65.jpg create mode 100644 steps/03.01-server-components-solution/public/portraits/women/8.jpg create mode 100644 steps/03.01-server-components-solution/public/portraits/women/85.jpg create mode 100644 steps/03.01-server-components-solution/public/portraits/women/93.jpg create mode 100644 steps/03.01-server-components-solution/public/vercel.svg create mode 100644 steps/03.01-server-components-solution/src/app/(auth)/layout.tsx create mode 100644 steps/03.01-server-components-solution/src/app/(auth)/login/page.tsx create mode 100644 steps/03.01-server-components-solution/src/app/(dashboard)/employees/[id]/edit/page.tsx create mode 100644 steps/03.01-server-components-solution/src/app/(dashboard)/employees/[id]/page.tsx create mode 100644 steps/03.01-server-components-solution/src/app/(dashboard)/employees/log/page.tsx create mode 100644 steps/03.01-server-components-solution/src/app/(dashboard)/employees/new/page.tsx create mode 100644 steps/03.01-server-components-solution/src/app/(dashboard)/employees/page.tsx create mode 100644 steps/03.01-server-components-solution/src/app/(dashboard)/expenses/[id]/page.tsx create mode 100644 steps/03.01-server-components-solution/src/app/(dashboard)/expenses/page.tsx create mode 100644 steps/03.01-server-components-solution/src/app/(dashboard)/layout.tsx create mode 100644 steps/03.01-server-components-solution/src/app/(dashboard)/page.tsx create mode 100644 steps/03.01-server-components-solution/src/app/layout.tsx create mode 100644 steps/03.01-server-components-solution/src/assets/images/profile-placeholder.jpg create mode 100644 steps/03.01-server-components-solution/src/assets/svg/logo.svg create mode 100644 steps/03.01-server-components-solution/src/assets/svg/logoDark.svg create mode 100644 steps/03.01-server-components-solution/src/components/Alert.tsx create mode 100644 steps/03.01-server-components-solution/src/components/Button.tsx create mode 100644 steps/03.01-server-components-solution/src/components/EmployeeForm.tsx create mode 100644 steps/03.01-server-components-solution/src/components/ExpensesDetails.tsx create mode 100644 steps/03.01-server-components-solution/src/components/ExpensesTable.tsx create mode 100644 steps/03.01-server-components-solution/src/components/Icons/ArrowLeft.tsx create mode 100644 steps/03.01-server-components-solution/src/components/Icons/Eye.tsx create mode 100644 steps/03.01-server-components-solution/src/components/Icons/Loader.tsx create mode 100644 steps/03.01-server-components-solution/src/components/NavigationItem.tsx create mode 100644 steps/03.01-server-components-solution/src/components/NavigationMenu.tsx create mode 100644 steps/03.01-server-components-solution/src/components/PageTitle.tsx create mode 100644 steps/03.01-server-components-solution/src/components/Pagination.tsx create mode 100644 steps/03.01-server-components-solution/src/components/Paper.tsx create mode 100644 steps/03.01-server-components-solution/src/components/PersonCard.tsx create mode 100644 steps/03.01-server-components-solution/src/components/Search.tsx create mode 100644 steps/03.01-server-components-solution/src/components/TextField.tsx create mode 100644 steps/03.01-server-components-solution/src/data/employees.json create mode 100644 steps/03.01-server-components-solution/src/data/expenses.json create mode 100644 steps/03.01-server-components-solution/src/functions/timing.ts create mode 100644 steps/03.01-server-components-solution/src/styles/global.css create mode 100644 steps/03.01-server-components-solution/src/types.ts create mode 100644 steps/03.01-server-components-solution/tailwind.config.js create mode 100644 steps/03.01-server-components-solution/tsconfig.json create mode 100644 steps/03.01-server-components/.env.example create mode 100644 steps/03.01-server-components/.eslintrc.json create mode 100644 steps/03.01-server-components/.gitignore create mode 100644 steps/03.01-server-components/.gitkeep create mode 100644 steps/03.01-server-components/README.md create mode 100644 steps/03.01-server-components/next.config.mjs create mode 100644 steps/03.01-server-components/package.json create mode 100644 steps/03.01-server-components/postcss.config.js create mode 100644 steps/03.01-server-components/public/next.svg create mode 100644 steps/03.01-server-components/public/portraits/men/30.jpg create mode 100644 steps/03.01-server-components/public/portraits/men/34.jpg create mode 100644 steps/03.01-server-components/public/portraits/men/56.jpg create mode 100644 steps/03.01-server-components/public/portraits/men/78.jpg create mode 100644 steps/03.01-server-components/public/portraits/men/86.jpg create mode 100644 steps/03.01-server-components/public/portraits/women/24.jpg create mode 100644 steps/03.01-server-components/public/portraits/women/65.jpg create mode 100644 steps/03.01-server-components/public/portraits/women/8.jpg create mode 100644 steps/03.01-server-components/public/portraits/women/85.jpg create mode 100644 steps/03.01-server-components/public/portraits/women/93.jpg create mode 100644 steps/03.01-server-components/public/vercel.svg create mode 100644 steps/03.01-server-components/src/app/(auth)/layout.tsx create mode 100644 steps/03.01-server-components/src/app/(auth)/login/page.tsx create mode 100644 steps/03.01-server-components/src/app/(dashboard)/employees/[id]/edit/page.tsx create mode 100644 steps/03.01-server-components/src/app/(dashboard)/employees/[id]/page.tsx create mode 100644 steps/03.01-server-components/src/app/(dashboard)/employees/new/page.tsx create mode 100644 steps/03.01-server-components/src/app/(dashboard)/employees/page.tsx create mode 100644 steps/03.01-server-components/src/app/(dashboard)/expenses/[id]/page.tsx create mode 100644 steps/03.01-server-components/src/app/(dashboard)/expenses/page.tsx create mode 100644 steps/03.01-server-components/src/app/(dashboard)/layout.tsx create mode 100644 steps/03.01-server-components/src/app/(dashboard)/page.tsx create mode 100644 steps/03.01-server-components/src/app/layout.tsx create mode 100644 steps/03.01-server-components/src/assets/images/profile-placeholder.jpg create mode 100644 steps/03.01-server-components/src/assets/svg/logo.svg create mode 100644 steps/03.01-server-components/src/assets/svg/logoDark.svg create mode 100644 steps/03.01-server-components/src/components/Alert.tsx create mode 100644 steps/03.01-server-components/src/components/Button.tsx create mode 100644 steps/03.01-server-components/src/components/EmployeeForm.tsx create mode 100644 steps/03.01-server-components/src/components/ExpensesDetails.tsx create mode 100644 steps/03.01-server-components/src/components/ExpensesTable.tsx create mode 100644 steps/03.01-server-components/src/components/Icons/ArrowLeft.tsx create mode 100644 steps/03.01-server-components/src/components/Icons/Eye.tsx create mode 100644 steps/03.01-server-components/src/components/Icons/Loader.tsx create mode 100644 steps/03.01-server-components/src/components/NavigationItem.tsx create mode 100644 steps/03.01-server-components/src/components/NavigationMenu.tsx create mode 100644 steps/03.01-server-components/src/components/PageTitle.tsx create mode 100644 steps/03.01-server-components/src/components/Pagination.tsx create mode 100644 steps/03.01-server-components/src/components/Paper.tsx create mode 100644 steps/03.01-server-components/src/components/PersonCard.tsx create mode 100644 steps/03.01-server-components/src/components/Search.tsx create mode 100644 steps/03.01-server-components/src/components/TextField.tsx create mode 100644 steps/03.01-server-components/src/data/employees.json create mode 100644 steps/03.01-server-components/src/data/expenses.json create mode 100644 steps/03.01-server-components/src/functions/timing.ts create mode 100644 steps/03.01-server-components/src/styles/global.css create mode 100644 steps/03.01-server-components/src/types.ts create mode 100644 steps/03.01-server-components/tailwind.config.js create mode 100644 steps/03.01-server-components/tsconfig.json create mode 100644 steps/03.02-composition-solution/.env.example create mode 100644 steps/03.02-composition-solution/.eslintrc.json create mode 100644 steps/03.02-composition-solution/.gitignore create mode 100644 steps/03.02-composition-solution/README.md create mode 100644 steps/03.02-composition-solution/next.config.mjs create mode 100644 steps/03.02-composition-solution/package.json create mode 100644 steps/03.02-composition-solution/postcss.config.js create mode 100644 steps/03.02-composition-solution/public/next.svg create mode 100644 steps/03.02-composition-solution/public/portraits/men/30.jpg create mode 100644 steps/03.02-composition-solution/public/portraits/men/34.jpg create mode 100644 steps/03.02-composition-solution/public/portraits/men/56.jpg create mode 100644 steps/03.02-composition-solution/public/portraits/men/78.jpg create mode 100644 steps/03.02-composition-solution/public/portraits/men/86.jpg create mode 100644 steps/03.02-composition-solution/public/portraits/women/24.jpg create mode 100644 steps/03.02-composition-solution/public/portraits/women/65.jpg create mode 100644 steps/03.02-composition-solution/public/portraits/women/8.jpg create mode 100644 steps/03.02-composition-solution/public/portraits/women/85.jpg create mode 100644 steps/03.02-composition-solution/public/portraits/women/93.jpg create mode 100644 steps/03.02-composition-solution/public/vercel.svg create mode 100644 steps/03.02-composition-solution/src/app/(auth)/layout.tsx create mode 100644 steps/03.02-composition-solution/src/app/(auth)/login/page.tsx create mode 100644 steps/03.02-composition-solution/src/app/(dashboard)/employees/[id]/edit/page.tsx create mode 100644 steps/03.02-composition-solution/src/app/(dashboard)/employees/[id]/page.tsx create mode 100644 steps/03.02-composition-solution/src/app/(dashboard)/employees/new/page.tsx create mode 100644 steps/03.02-composition-solution/src/app/(dashboard)/employees/page.tsx create mode 100644 steps/03.02-composition-solution/src/app/(dashboard)/expenses/[id]/page.tsx create mode 100644 steps/03.02-composition-solution/src/app/(dashboard)/expenses/page.tsx create mode 100644 steps/03.02-composition-solution/src/app/(dashboard)/layout.tsx create mode 100644 steps/03.02-composition-solution/src/app/(dashboard)/page.tsx create mode 100644 steps/03.02-composition-solution/src/app/layout.tsx create mode 100644 steps/03.02-composition-solution/src/assets/images/profile-placeholder.jpg create mode 100644 steps/03.02-composition-solution/src/assets/svg/logo.svg create mode 100644 steps/03.02-composition-solution/src/assets/svg/logoDark.svg create mode 100644 steps/03.02-composition-solution/src/components/Alert.tsx create mode 100644 steps/03.02-composition-solution/src/components/Button.tsx create mode 100644 steps/03.02-composition-solution/src/components/EmployeeForm.tsx create mode 100644 steps/03.02-composition-solution/src/components/ExpensesDetails.tsx create mode 100644 steps/03.02-composition-solution/src/components/ExpensesTable.tsx create mode 100644 steps/03.02-composition-solution/src/components/ExpensesTableRow.tsx create mode 100644 steps/03.02-composition-solution/src/components/Icons/ArrowLeft.tsx create mode 100644 steps/03.02-composition-solution/src/components/Icons/Eye.tsx create mode 100644 steps/03.02-composition-solution/src/components/Icons/Loader.tsx create mode 100644 steps/03.02-composition-solution/src/components/Logo.tsx create mode 100644 steps/03.02-composition-solution/src/components/NavigationItem.tsx create mode 100644 steps/03.02-composition-solution/src/components/NavigationMenu.tsx create mode 100644 steps/03.02-composition-solution/src/components/PageTitle.tsx create mode 100644 steps/03.02-composition-solution/src/components/Pagination.tsx create mode 100644 steps/03.02-composition-solution/src/components/Paper.tsx create mode 100644 steps/03.02-composition-solution/src/components/PersonCard.tsx create mode 100644 steps/03.02-composition-solution/src/components/Search.tsx create mode 100644 steps/03.02-composition-solution/src/components/TextField.tsx create mode 100644 steps/03.02-composition-solution/src/components/ThemeProvider.tsx create mode 100644 steps/03.02-composition-solution/src/data/employees.json create mode 100644 steps/03.02-composition-solution/src/data/expenses.json create mode 100644 steps/03.02-composition-solution/src/functions/timing.ts create mode 100644 steps/03.02-composition-solution/src/styles/global.css create mode 100644 steps/03.02-composition-solution/src/types.ts create mode 100644 steps/03.02-composition-solution/tailwind.config.js create mode 100644 steps/03.02-composition-solution/tsconfig.json create mode 100644 steps/03.02-composition/.env.example create mode 100644 steps/03.02-composition/.eslintrc.json create mode 100644 steps/03.02-composition/.gitignore create mode 100644 steps/03.02-composition/README.md create mode 100644 steps/03.02-composition/next.config.mjs create mode 100644 steps/03.02-composition/package.json create mode 100644 steps/03.02-composition/postcss.config.js create mode 100644 steps/03.02-composition/public/next.svg create mode 100644 steps/03.02-composition/public/portraits/men/30.jpg create mode 100644 steps/03.02-composition/public/portraits/men/34.jpg create mode 100644 steps/03.02-composition/public/portraits/men/56.jpg create mode 100644 steps/03.02-composition/public/portraits/men/78.jpg create mode 100644 steps/03.02-composition/public/portraits/men/86.jpg create mode 100644 steps/03.02-composition/public/portraits/women/24.jpg create mode 100644 steps/03.02-composition/public/portraits/women/65.jpg create mode 100644 steps/03.02-composition/public/portraits/women/8.jpg create mode 100644 steps/03.02-composition/public/portraits/women/85.jpg create mode 100644 steps/03.02-composition/public/portraits/women/93.jpg create mode 100644 steps/03.02-composition/public/vercel.svg create mode 100644 steps/03.02-composition/src/app/(auth)/layout.tsx create mode 100644 steps/03.02-composition/src/app/(auth)/login/page.tsx create mode 100644 steps/03.02-composition/src/app/(dashboard)/employees/[id]/edit/page.tsx create mode 100644 steps/03.02-composition/src/app/(dashboard)/employees/[id]/page.tsx create mode 100644 steps/03.02-composition/src/app/(dashboard)/employees/new/page.tsx create mode 100644 steps/03.02-composition/src/app/(dashboard)/employees/page.tsx create mode 100644 steps/03.02-composition/src/app/(dashboard)/expenses/[id]/page.tsx create mode 100644 steps/03.02-composition/src/app/(dashboard)/expenses/page.tsx create mode 100644 steps/03.02-composition/src/app/(dashboard)/layout.tsx create mode 100644 steps/03.02-composition/src/app/(dashboard)/page.tsx create mode 100644 steps/03.02-composition/src/app/layout.tsx create mode 100644 steps/03.02-composition/src/assets/images/profile-placeholder.jpg create mode 100644 steps/03.02-composition/src/assets/svg/logo.svg create mode 100644 steps/03.02-composition/src/assets/svg/logoDark.svg create mode 100644 steps/03.02-composition/src/components/Alert.tsx create mode 100644 steps/03.02-composition/src/components/Button.tsx create mode 100644 steps/03.02-composition/src/components/EmployeeForm.tsx create mode 100644 steps/03.02-composition/src/components/ExpensesDetails.tsx create mode 100644 steps/03.02-composition/src/components/ExpensesTable.tsx create mode 100644 steps/03.02-composition/src/components/Icons/ArrowLeft.tsx create mode 100644 steps/03.02-composition/src/components/Icons/Eye.tsx create mode 100644 steps/03.02-composition/src/components/Icons/Loader.tsx create mode 100644 steps/03.02-composition/src/components/NavigationItem.tsx create mode 100644 steps/03.02-composition/src/components/NavigationMenu.tsx create mode 100644 steps/03.02-composition/src/components/PageTitle.tsx create mode 100644 steps/03.02-composition/src/components/Pagination.tsx create mode 100644 steps/03.02-composition/src/components/Paper.tsx create mode 100644 steps/03.02-composition/src/components/PersonCard.tsx create mode 100644 steps/03.02-composition/src/components/Search.tsx create mode 100644 steps/03.02-composition/src/components/TextField.tsx create mode 100644 steps/03.02-composition/src/data/employees.json create mode 100644 steps/03.02-composition/src/data/expenses.json create mode 100644 steps/03.02-composition/src/functions/timing.ts create mode 100644 steps/03.02-composition/src/styles/global.css create mode 100644 steps/03.02-composition/src/types.ts create mode 100644 steps/03.02-composition/tailwind.config.js create mode 100644 steps/03.02-composition/tsconfig.json create mode 100644 steps/04.01-data-fetching-solution/.env.example create mode 100644 steps/04.01-data-fetching-solution/.eslintrc.json create mode 100644 steps/04.01-data-fetching-solution/.gitignore create mode 100644 steps/04.01-data-fetching-solution/README.md create mode 100644 steps/04.01-data-fetching-solution/next.config.mjs create mode 100644 steps/04.01-data-fetching-solution/package.json create mode 100644 steps/04.01-data-fetching-solution/postcss.config.js create mode 100644 steps/04.01-data-fetching-solution/public/next.svg create mode 100644 steps/04.01-data-fetching-solution/public/portraits/men/30.jpg create mode 100644 steps/04.01-data-fetching-solution/public/portraits/men/34.jpg create mode 100644 steps/04.01-data-fetching-solution/public/portraits/men/56.jpg create mode 100644 steps/04.01-data-fetching-solution/public/portraits/men/78.jpg create mode 100644 steps/04.01-data-fetching-solution/public/portraits/men/86.jpg create mode 100644 steps/04.01-data-fetching-solution/public/portraits/women/24.jpg create mode 100644 steps/04.01-data-fetching-solution/public/portraits/women/65.jpg create mode 100644 steps/04.01-data-fetching-solution/public/portraits/women/8.jpg create mode 100644 steps/04.01-data-fetching-solution/public/portraits/women/85.jpg create mode 100644 steps/04.01-data-fetching-solution/public/portraits/women/93.jpg create mode 100644 steps/04.01-data-fetching-solution/public/vercel.svg create mode 100644 steps/04.01-data-fetching-solution/src/api/common.ts create mode 100644 steps/04.01-data-fetching-solution/src/api/error.ts create mode 100644 steps/04.01-data-fetching-solution/src/api/expenses.ts create mode 100644 steps/04.01-data-fetching-solution/src/api/people.ts create mode 100644 steps/04.01-data-fetching-solution/src/app/(auth)/layout.tsx create mode 100644 steps/04.01-data-fetching-solution/src/app/(auth)/login/page.tsx create mode 100644 steps/04.01-data-fetching-solution/src/app/(dashboard)/employees/[id]/edit/page.tsx create mode 100644 steps/04.01-data-fetching-solution/src/app/(dashboard)/employees/[id]/page.tsx create mode 100644 steps/04.01-data-fetching-solution/src/app/(dashboard)/employees/new/page.tsx create mode 100644 steps/04.01-data-fetching-solution/src/app/(dashboard)/employees/page.tsx create mode 100644 steps/04.01-data-fetching-solution/src/app/(dashboard)/expenses/[id]/page.tsx create mode 100644 steps/04.01-data-fetching-solution/src/app/(dashboard)/expenses/page.tsx create mode 100644 steps/04.01-data-fetching-solution/src/app/(dashboard)/layout.tsx create mode 100644 steps/04.01-data-fetching-solution/src/app/(dashboard)/page.tsx create mode 100644 steps/04.01-data-fetching-solution/src/app/layout.tsx create mode 100644 steps/04.01-data-fetching-solution/src/app/rest/expenses/route.ts create mode 100644 steps/04.01-data-fetching-solution/src/assets/images/profile-placeholder.jpg create mode 100644 steps/04.01-data-fetching-solution/src/assets/svg/logo.svg create mode 100644 steps/04.01-data-fetching-solution/src/assets/svg/logoDark.svg create mode 100644 steps/04.01-data-fetching-solution/src/components/Alert.tsx create mode 100644 steps/04.01-data-fetching-solution/src/components/Button.tsx create mode 100644 steps/04.01-data-fetching-solution/src/components/EmployeeExpenses.tsx create mode 100644 steps/04.01-data-fetching-solution/src/components/EmployeeForm.tsx create mode 100644 steps/04.01-data-fetching-solution/src/components/ExpensesDetails.tsx create mode 100644 steps/04.01-data-fetching-solution/src/components/ExpensesTable.tsx create mode 100644 steps/04.01-data-fetching-solution/src/components/Icons/ArrowLeft.tsx create mode 100644 steps/04.01-data-fetching-solution/src/components/Icons/Eye.tsx create mode 100644 steps/04.01-data-fetching-solution/src/components/Icons/Loader.tsx create mode 100644 steps/04.01-data-fetching-solution/src/components/NavigationItem.tsx create mode 100644 steps/04.01-data-fetching-solution/src/components/NavigationMenu.tsx create mode 100644 steps/04.01-data-fetching-solution/src/components/PageTitle.tsx create mode 100644 steps/04.01-data-fetching-solution/src/components/Pagination.tsx create mode 100644 steps/04.01-data-fetching-solution/src/components/Paper.tsx create mode 100644 steps/04.01-data-fetching-solution/src/components/PersonCard.tsx create mode 100644 steps/04.01-data-fetching-solution/src/components/Search.tsx create mode 100644 steps/04.01-data-fetching-solution/src/components/TextField.tsx create mode 100644 steps/04.01-data-fetching-solution/src/functions/timing.ts create mode 100644 steps/04.01-data-fetching-solution/src/styles/global.css create mode 100644 steps/04.01-data-fetching-solution/src/types.ts create mode 100644 steps/04.01-data-fetching-solution/tailwind.config.js create mode 100644 steps/04.01-data-fetching-solution/tsconfig.json create mode 100644 steps/04.01-data-fetching/.env.example create mode 100644 steps/04.01-data-fetching/.eslintrc.json create mode 100644 steps/04.01-data-fetching/.gitignore create mode 100644 steps/04.01-data-fetching/README.md create mode 100644 steps/04.01-data-fetching/next.config.mjs create mode 100644 steps/04.01-data-fetching/package.json create mode 100644 steps/04.01-data-fetching/postcss.config.js create mode 100644 steps/04.01-data-fetching/public/next.svg create mode 100644 steps/04.01-data-fetching/public/portraits/men/30.jpg create mode 100644 steps/04.01-data-fetching/public/portraits/men/34.jpg create mode 100644 steps/04.01-data-fetching/public/portraits/men/56.jpg create mode 100644 steps/04.01-data-fetching/public/portraits/men/78.jpg create mode 100644 steps/04.01-data-fetching/public/portraits/men/86.jpg create mode 100644 steps/04.01-data-fetching/public/portraits/women/24.jpg create mode 100644 steps/04.01-data-fetching/public/portraits/women/65.jpg create mode 100644 steps/04.01-data-fetching/public/portraits/women/8.jpg create mode 100644 steps/04.01-data-fetching/public/portraits/women/85.jpg create mode 100644 steps/04.01-data-fetching/public/portraits/women/93.jpg create mode 100644 steps/04.01-data-fetching/public/vercel.svg create mode 100644 steps/04.01-data-fetching/src/api/common.ts create mode 100644 steps/04.01-data-fetching/src/api/error.ts create mode 100644 steps/04.01-data-fetching/src/app/(auth)/layout.tsx create mode 100644 steps/04.01-data-fetching/src/app/(auth)/login/page.tsx create mode 100644 steps/04.01-data-fetching/src/app/(dashboard)/employees/[id]/edit/page.tsx create mode 100644 steps/04.01-data-fetching/src/app/(dashboard)/employees/[id]/page.tsx create mode 100644 steps/04.01-data-fetching/src/app/(dashboard)/employees/new/page.tsx create mode 100644 steps/04.01-data-fetching/src/app/(dashboard)/employees/page.tsx create mode 100644 steps/04.01-data-fetching/src/app/(dashboard)/expenses/[id]/page.tsx create mode 100644 steps/04.01-data-fetching/src/app/(dashboard)/expenses/page.tsx create mode 100644 steps/04.01-data-fetching/src/app/(dashboard)/layout.tsx create mode 100644 steps/04.01-data-fetching/src/app/(dashboard)/page.tsx create mode 100644 steps/04.01-data-fetching/src/app/layout.tsx create mode 100644 steps/04.01-data-fetching/src/assets/images/profile-placeholder.jpg create mode 100644 steps/04.01-data-fetching/src/assets/svg/logo.svg create mode 100644 steps/04.01-data-fetching/src/assets/svg/logoDark.svg create mode 100644 steps/04.01-data-fetching/src/components/Alert.tsx create mode 100644 steps/04.01-data-fetching/src/components/Button.tsx create mode 100644 steps/04.01-data-fetching/src/components/EmployeeExpenses.tsx create mode 100644 steps/04.01-data-fetching/src/components/EmployeeForm.tsx create mode 100644 steps/04.01-data-fetching/src/components/ExpensesDetails.tsx create mode 100644 steps/04.01-data-fetching/src/components/ExpensesTable.tsx create mode 100644 steps/04.01-data-fetching/src/components/Icons/ArrowLeft.tsx create mode 100644 steps/04.01-data-fetching/src/components/Icons/Eye.tsx create mode 100644 steps/04.01-data-fetching/src/components/Icons/Loader.tsx create mode 100644 steps/04.01-data-fetching/src/components/NavigationItem.tsx create mode 100644 steps/04.01-data-fetching/src/components/NavigationMenu.tsx create mode 100644 steps/04.01-data-fetching/src/components/PageTitle.tsx create mode 100644 steps/04.01-data-fetching/src/components/Pagination.tsx create mode 100644 steps/04.01-data-fetching/src/components/Paper.tsx create mode 100644 steps/04.01-data-fetching/src/components/PersonCard.tsx create mode 100644 steps/04.01-data-fetching/src/components/Search.tsx create mode 100644 steps/04.01-data-fetching/src/components/TextField.tsx create mode 100644 steps/04.01-data-fetching/src/data/employees.json create mode 100644 steps/04.01-data-fetching/src/data/expenses.json create mode 100644 steps/04.01-data-fetching/src/functions/timing.ts create mode 100644 steps/04.01-data-fetching/src/styles/global.css create mode 100644 steps/04.01-data-fetching/src/types.ts create mode 100644 steps/04.01-data-fetching/tailwind.config.js create mode 100644 steps/04.01-data-fetching/tsconfig.json create mode 100644 steps/04.02-caching-solution/.env.example create mode 100644 steps/04.02-caching-solution/.eslintrc.json create mode 100644 steps/04.02-caching-solution/.gitignore create mode 100644 steps/04.02-caching-solution/README.md create mode 100644 steps/04.02-caching-solution/next.config.mjs create mode 100644 steps/04.02-caching-solution/package.json create mode 100644 steps/04.02-caching-solution/postcss.config.js create mode 100644 steps/04.02-caching-solution/public/next.svg create mode 100644 steps/04.02-caching-solution/public/portraits/men/30.jpg create mode 100644 steps/04.02-caching-solution/public/portraits/men/34.jpg create mode 100644 steps/04.02-caching-solution/public/portraits/men/56.jpg create mode 100644 steps/04.02-caching-solution/public/portraits/men/78.jpg create mode 100644 steps/04.02-caching-solution/public/portraits/men/86.jpg create mode 100644 steps/04.02-caching-solution/public/portraits/women/24.jpg create mode 100644 steps/04.02-caching-solution/public/portraits/women/65.jpg create mode 100644 steps/04.02-caching-solution/public/portraits/women/8.jpg create mode 100644 steps/04.02-caching-solution/public/portraits/women/85.jpg create mode 100644 steps/04.02-caching-solution/public/portraits/women/93.jpg create mode 100644 steps/04.02-caching-solution/public/vercel.svg create mode 100644 steps/04.02-caching-solution/src/api/common.ts create mode 100644 steps/04.02-caching-solution/src/api/error.ts create mode 100644 steps/04.02-caching-solution/src/api/expenses.ts create mode 100644 steps/04.02-caching-solution/src/api/people.ts create mode 100644 steps/04.02-caching-solution/src/app/(auth)/layout.tsx create mode 100644 steps/04.02-caching-solution/src/app/(auth)/login/page.tsx create mode 100644 steps/04.02-caching-solution/src/app/(dashboard)/employees/[id]/edit/page.tsx create mode 100644 steps/04.02-caching-solution/src/app/(dashboard)/employees/[id]/page.tsx create mode 100644 steps/04.02-caching-solution/src/app/(dashboard)/employees/new/page.tsx create mode 100644 steps/04.02-caching-solution/src/app/(dashboard)/employees/page.tsx create mode 100644 steps/04.02-caching-solution/src/app/(dashboard)/expenses/[id]/page.tsx create mode 100644 steps/04.02-caching-solution/src/app/(dashboard)/expenses/page.tsx create mode 100644 steps/04.02-caching-solution/src/app/(dashboard)/layout.tsx create mode 100644 steps/04.02-caching-solution/src/app/(dashboard)/page.tsx create mode 100644 steps/04.02-caching-solution/src/app/api/revalidate/route.ts create mode 100644 steps/04.02-caching-solution/src/app/layout.tsx create mode 100644 steps/04.02-caching-solution/src/app/rest/expenses/route.ts create mode 100644 steps/04.02-caching-solution/src/assets/images/profile-placeholder.jpg create mode 100644 steps/04.02-caching-solution/src/assets/svg/logo.svg create mode 100644 steps/04.02-caching-solution/src/assets/svg/logoDark.svg create mode 100644 steps/04.02-caching-solution/src/components/Alert.tsx create mode 100644 steps/04.02-caching-solution/src/components/Button.tsx create mode 100644 steps/04.02-caching-solution/src/components/EmployeeExpenses.tsx create mode 100644 steps/04.02-caching-solution/src/components/EmployeeForm.tsx create mode 100644 steps/04.02-caching-solution/src/components/ExpensesDetails.tsx create mode 100644 steps/04.02-caching-solution/src/components/ExpensesTable.tsx create mode 100644 steps/04.02-caching-solution/src/components/Icons/ArrowLeft.tsx create mode 100644 steps/04.02-caching-solution/src/components/Icons/Eye.tsx create mode 100644 steps/04.02-caching-solution/src/components/Icons/Loader.tsx create mode 100644 steps/04.02-caching-solution/src/components/NavigationItem.tsx create mode 100644 steps/04.02-caching-solution/src/components/NavigationMenu.tsx create mode 100644 steps/04.02-caching-solution/src/components/PageTitle.tsx create mode 100644 steps/04.02-caching-solution/src/components/Pagination.tsx create mode 100644 steps/04.02-caching-solution/src/components/Paper.tsx create mode 100644 steps/04.02-caching-solution/src/components/PersonCard.tsx create mode 100644 steps/04.02-caching-solution/src/components/Search.tsx create mode 100644 steps/04.02-caching-solution/src/components/TextField.tsx create mode 100644 steps/04.02-caching-solution/src/functions/timing.ts create mode 100644 steps/04.02-caching-solution/src/styles/global.css create mode 100644 steps/04.02-caching-solution/src/types.ts create mode 100644 steps/04.02-caching-solution/tailwind.config.js create mode 100644 steps/04.02-caching-solution/tsconfig.json create mode 100644 steps/04.02-caching/.env.example create mode 100644 steps/04.02-caching/.eslintrc.json create mode 100644 steps/04.02-caching/.gitignore create mode 100644 steps/04.02-caching/README.md create mode 100644 steps/04.02-caching/next.config.mjs create mode 100644 steps/04.02-caching/package.json create mode 100644 steps/04.02-caching/postcss.config.js create mode 100644 steps/04.02-caching/public/next.svg create mode 100644 steps/04.02-caching/public/portraits/men/30.jpg create mode 100644 steps/04.02-caching/public/portraits/men/34.jpg create mode 100644 steps/04.02-caching/public/portraits/men/56.jpg create mode 100644 steps/04.02-caching/public/portraits/men/78.jpg create mode 100644 steps/04.02-caching/public/portraits/men/86.jpg create mode 100644 steps/04.02-caching/public/portraits/women/24.jpg create mode 100644 steps/04.02-caching/public/portraits/women/65.jpg create mode 100644 steps/04.02-caching/public/portraits/women/8.jpg create mode 100644 steps/04.02-caching/public/portraits/women/85.jpg create mode 100644 steps/04.02-caching/public/portraits/women/93.jpg create mode 100644 steps/04.02-caching/public/vercel.svg create mode 100644 steps/04.02-caching/src/api/common.ts create mode 100644 steps/04.02-caching/src/api/error.ts create mode 100644 steps/04.02-caching/src/api/expenses.ts create mode 100644 steps/04.02-caching/src/api/people.ts create mode 100644 steps/04.02-caching/src/app/(auth)/layout.tsx create mode 100644 steps/04.02-caching/src/app/(auth)/login/page.tsx create mode 100644 steps/04.02-caching/src/app/(dashboard)/employees/[id]/edit/page.tsx create mode 100644 steps/04.02-caching/src/app/(dashboard)/employees/[id]/page.tsx create mode 100644 steps/04.02-caching/src/app/(dashboard)/employees/new/page.tsx create mode 100644 steps/04.02-caching/src/app/(dashboard)/employees/page.tsx create mode 100644 steps/04.02-caching/src/app/(dashboard)/expenses/[id]/page.tsx create mode 100644 steps/04.02-caching/src/app/(dashboard)/expenses/page.tsx create mode 100644 steps/04.02-caching/src/app/(dashboard)/layout.tsx create mode 100644 steps/04.02-caching/src/app/(dashboard)/page.tsx create mode 100644 steps/04.02-caching/src/app/layout.tsx create mode 100644 steps/04.02-caching/src/app/rest/expenses/route.ts create mode 100644 steps/04.02-caching/src/assets/images/profile-placeholder.jpg create mode 100644 steps/04.02-caching/src/assets/svg/logo.svg create mode 100644 steps/04.02-caching/src/assets/svg/logoDark.svg create mode 100644 steps/04.02-caching/src/components/Alert.tsx create mode 100644 steps/04.02-caching/src/components/Button.tsx create mode 100644 steps/04.02-caching/src/components/EmployeeExpenses.tsx create mode 100644 steps/04.02-caching/src/components/EmployeeForm.tsx create mode 100644 steps/04.02-caching/src/components/ExpensesDetails.tsx create mode 100644 steps/04.02-caching/src/components/ExpensesTable.tsx create mode 100644 steps/04.02-caching/src/components/Icons/ArrowLeft.tsx create mode 100644 steps/04.02-caching/src/components/Icons/Eye.tsx create mode 100644 steps/04.02-caching/src/components/Icons/Loader.tsx create mode 100644 steps/04.02-caching/src/components/NavigationItem.tsx create mode 100644 steps/04.02-caching/src/components/NavigationMenu.tsx create mode 100644 steps/04.02-caching/src/components/PageTitle.tsx create mode 100644 steps/04.02-caching/src/components/Pagination.tsx create mode 100644 steps/04.02-caching/src/components/Paper.tsx create mode 100644 steps/04.02-caching/src/components/PersonCard.tsx create mode 100644 steps/04.02-caching/src/components/Search.tsx create mode 100644 steps/04.02-caching/src/components/TextField.tsx create mode 100644 steps/04.02-caching/src/functions/timing.ts create mode 100644 steps/04.02-caching/src/styles/global.css create mode 100644 steps/04.02-caching/src/types.ts create mode 100644 steps/04.02-caching/tailwind.config.js create mode 100644 steps/04.02-caching/tsconfig.json create mode 100644 steps/05.01-server-actions-solution/.gitkeep create mode 100644 steps/05.01-server-actions/.gitkeep create mode 100644 steps/05.02-form-hooks-solution/.gitkeep create mode 100644 steps/05.02-form-hooks/.gitkeep create mode 100644 steps/06.01-error-boundaries-solution/.gitkeep create mode 100644 steps/06.01-error-boundaries/.gitkeep create mode 100644 steps/06.02-expected-errors-solution/.gitkeep create mode 100644 steps/06.02-expected-errors/.gitkeep create mode 100644 steps/07.01-lifecycles-solution/.gitkeep create mode 100644 steps/07.01-lifecycles/.gitkeep create mode 100644 steps/07.02-middleware-solution/.gitkeep create mode 100644 steps/07.02-middleware/.gitkeep create mode 100644 steps/08.01-rendering-methods-solution/.gitkeep create mode 100644 steps/08.01-rendering-methods/.gitkeep create mode 100644 steps/08.02-suspense-solution/.gitkeep create mode 100644 steps/08.02-suspense/.gitkeep diff --git a/package-lock.json b/package-lock.json index 3d4528b..bb66550 100644 --- a/package-lock.json +++ b/package-lock.json @@ -59,6 +59,14 @@ "node": ">=6.9.0" } }, + "node_modules/@code-hike/lighter": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@code-hike/lighter/-/lighter-0.8.1.tgz", + "integrity": "sha512-St4rPmB7C2EWmAK1sAbvD3lZeM7UDInVDMjQDzEDsu4Q3B3AqF25vXedQK51U0UO0MCOASgBBdTiNwvJAfIqMQ==", + "funding": { + "url": "https://github.com/code-hike/lighter?sponsor=1" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -876,8 +884,36 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/00-sfeir-people": { - "resolved": "steps/00-sfeir-people", + "node_modules/02.01": { + "resolved": "steps/02.01-pages-layout", + "link": true + }, + "node_modules/02.01-solution": { + "resolved": "steps/02.01-pages-layout-solution", + "link": true + }, + "node_modules/02.02": { + "resolved": "steps/02.02-navigation", + "link": true + }, + "node_modules/02.02-solution": { + "resolved": "steps/02.02-navigation-solution", + "link": true + }, + "node_modules/03.01": { + "resolved": "steps/03.01-server-components", + "link": true + }, + "node_modules/03.01-solution": { + "resolved": "steps/03.01-server-components-solution", + "link": true + }, + "node_modules/03.02": { + "resolved": "steps/03.02-composition", + "link": true + }, + "node_modules/03.02-solution": { + "resolved": "steps/03.02-composition-solution", "link": true }, "node_modules/abort-controller": { @@ -1539,6 +1575,21 @@ "node": ">=8" } }, + "node_modules/bright": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/bright/-/bright-0.8.5.tgz", + "integrity": "sha512-LOhh3jk8KLFMqhX67TSGP1kCb3qGXbiRLbyBToVOfrrrEa3omXHT44r0/L4/OOlKluaFcO7+11KLOM5xI50XvA==", + "dependencies": { + "@code-hike/lighter": "0.8.1", + "server-only": "^0.0.1" + }, + "funding": { + "url": "https://github.com/code-hike/bright?sponsor=1" + }, + "peerDependencies": { + "react": "^18" + } + }, "node_modules/browserslist": { "version": "4.23.2", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.2.tgz", @@ -2343,6 +2394,10 @@ "node": ">=0.10.0" } }, + "node_modules/demo": { + "resolved": "steps/00.00-sfeir-people-demo", + "link": true + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -10013,8 +10068,281 @@ } }, "steps/00-sfeir-people": { + "version": "0.1.0", + "extraneous": true, + "dependencies": { + "clsx": "^2.1.1", + "jose": "^5.6.3", + "jsonwebtoken": "^9.0.2", + "next": "14.2.5", + "react": "^18", + "react-dom": "^18", + "react-error-boundary": "^4.0.13", + "rehype-sanitize": "^6.0.0", + "rehype-stringify": "^10.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.0", + "server-only": "^0.0.1", + "showdown": "^2.1.0", + "unified": "^11.0.5" + }, + "devDependencies": { + "@types/jsonwebtoken": "^9.0.6", + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "@types/showdown": "^2.0.6", + "eslint": "^8", + "eslint-config-next": "14.2.5", + "typescript": "^5" + } + }, + "steps/00.00-sfeir-people-demo": { + "name": "demo", + "version": "0.1.0", + "dependencies": { + "clsx": "^2.1.1", + "jose": "^5.6.3", + "jsonwebtoken": "^9.0.2", + "next": "14.2.5", + "react": "^18", + "react-dom": "^18", + "react-error-boundary": "^4.0.13", + "rehype-sanitize": "^6.0.0", + "rehype-stringify": "^10.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.0", + "server-only": "^0.0.1", + "showdown": "^2.1.0", + "unified": "^11.0.5" + }, + "devDependencies": { + "@types/jsonwebtoken": "^9.0.6", + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "@types/showdown": "^2.0.6", + "eslint": "^8", + "eslint-config-next": "14.2.5", + "typescript": "^5" + } + }, + "steps/02.01-pages-layout": { + "name": "02.01", + "version": "0.1.0", + "dependencies": { + "clsx": "^2.1.1", + "jose": "^5.6.3", + "jsonwebtoken": "^9.0.2", + "next": "14.2.5", + "react": "^18", + "react-dom": "^18", + "react-error-boundary": "^4.0.13", + "rehype-sanitize": "^6.0.0", + "rehype-stringify": "^10.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.0", + "server-only": "^0.0.1", + "showdown": "^2.1.0", + "unified": "^11.0.5" + }, + "devDependencies": { + "@types/jsonwebtoken": "^9.0.6", + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "@types/showdown": "^2.0.6", + "eslint": "^8", + "eslint-config-next": "14.2.5", + "typescript": "^5" + } + }, + "steps/02.01-pages-layout-solution": { + "name": "02.01-solution", + "version": "0.1.0", + "dependencies": { + "clsx": "^2.1.1", + "jose": "^5.6.3", + "jsonwebtoken": "^9.0.2", + "next": "14.2.5", + "react": "^18", + "react-dom": "^18", + "react-error-boundary": "^4.0.13", + "rehype-sanitize": "^6.0.0", + "rehype-stringify": "^10.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.0", + "server-only": "^0.0.1", + "showdown": "^2.1.0", + "unified": "^11.0.5" + }, + "devDependencies": { + "@types/jsonwebtoken": "^9.0.6", + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "@types/showdown": "^2.0.6", + "eslint": "^8", + "eslint-config-next": "14.2.5", + "typescript": "^5" + } + }, + "steps/02.02-navigation": { + "name": "02.02", + "version": "0.1.0", + "dependencies": { + "clsx": "^2.1.1", + "jose": "^5.6.3", + "jsonwebtoken": "^9.0.2", + "next": "14.2.5", + "react": "^18", + "react-dom": "^18", + "react-error-boundary": "^4.0.13", + "rehype-sanitize": "^6.0.0", + "rehype-stringify": "^10.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.0", + "server-only": "^0.0.1", + "showdown": "^2.1.0", + "unified": "^11.0.5" + }, + "devDependencies": { + "@types/jsonwebtoken": "^9.0.6", + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "@types/showdown": "^2.0.6", + "eslint": "^8", + "eslint-config-next": "14.2.5", + "typescript": "^5" + } + }, + "steps/02.02-navigation-solution": { + "name": "02.02-solution", + "version": "0.1.0", + "dependencies": { + "clsx": "^2.1.1", + "jose": "^5.6.3", + "jsonwebtoken": "^9.0.2", + "next": "14.2.5", + "react": "^18", + "react-dom": "^18", + "react-error-boundary": "^4.0.13", + "rehype-sanitize": "^6.0.0", + "rehype-stringify": "^10.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.0", + "server-only": "^0.0.1", + "showdown": "^2.1.0", + "unified": "^11.0.5" + }, + "devDependencies": { + "@types/jsonwebtoken": "^9.0.6", + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "@types/showdown": "^2.0.6", + "eslint": "^8", + "eslint-config-next": "14.2.5", + "typescript": "^5" + } + }, + "steps/03.01-server-components": { + "name": "03.01", + "version": "0.1.0", + "dependencies": { + "bright": "^0.8.5", + "clsx": "^2.1.1", + "jose": "^5.6.3", + "jsonwebtoken": "^9.0.2", + "next": "14.2.5", + "react": "^18", + "react-dom": "^18", + "react-error-boundary": "^4.0.13", + "rehype-sanitize": "^6.0.0", + "rehype-stringify": "^10.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.0", + "server-only": "^0.0.1", + "showdown": "^2.1.0", + "unified": "^11.0.5" + }, + "devDependencies": { + "@types/jsonwebtoken": "^9.0.6", + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "@types/showdown": "^2.0.6", + "eslint": "^8", + "eslint-config-next": "14.2.5", + "typescript": "^5" + } + }, + "steps/03.01-server-components-solution": { + "name": "03.01-solution", + "version": "0.1.0", + "dependencies": { + "bright": "^0.8.5", + "clsx": "^2.1.1", + "jose": "^5.6.3", + "jsonwebtoken": "^9.0.2", + "next": "14.2.5", + "react": "^18", + "react-dom": "^18", + "react-error-boundary": "^4.0.13", + "rehype-sanitize": "^6.0.0", + "rehype-stringify": "^10.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.0", + "server-only": "^0.0.1", + "showdown": "^2.1.0", + "unified": "^11.0.5" + }, + "devDependencies": { + "@types/jsonwebtoken": "^9.0.6", + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "@types/showdown": "^2.0.6", + "eslint": "^8", + "eslint-config-next": "14.2.5", + "typescript": "^5" + } + }, + "steps/03.02-composition": { + "version": "0.1.0", + "dependencies": { + "bright": "^0.8.5", + "clsx": "^2.1.1", + "jose": "^5.6.3", + "jsonwebtoken": "^9.0.2", + "next": "14.2.5", + "react": "^18", + "react-dom": "^18", + "react-error-boundary": "^4.0.13", + "rehype-sanitize": "^6.0.0", + "rehype-stringify": "^10.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.0", + "server-only": "^0.0.1", + "showdown": "^2.1.0", + "unified": "^11.0.5" + }, + "devDependencies": { + "@types/jsonwebtoken": "^9.0.6", + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "@types/showdown": "^2.0.6", + "eslint": "^8", + "eslint-config-next": "14.2.5", + "typescript": "^5" + } + }, + "steps/03.02-composition-solution": { "version": "0.1.0", "dependencies": { + "bright": "^0.8.5", "clsx": "^2.1.1", "jose": "^5.6.3", "jsonwebtoken": "^9.0.2", diff --git a/steps/00-sfeir-people/README.md b/steps/00-sfeir-people/README.md deleted file mode 100644 index c403366..0000000 --- a/steps/00-sfeir-people/README.md +++ /dev/null @@ -1,36 +0,0 @@ -This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). - -## Getting Started - -First, run the development server: - -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev -``` - -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. - -This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. - -## Learn More - -To learn more about Next.js, take a look at the following resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! - -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. diff --git a/steps/00-sfeir-people/src/components/NavigationMenu/index.ts b/steps/00-sfeir-people/src/components/NavigationMenu/index.ts deleted file mode 100644 index 97b3c97..0000000 --- a/steps/00-sfeir-people/src/components/NavigationMenu/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './NavigationMenu'; diff --git a/steps/00-sfeir-people/.env.example b/steps/00.00-sfeir-people-demo/.env.example similarity index 100% rename from steps/00-sfeir-people/.env.example rename to steps/00.00-sfeir-people-demo/.env.example diff --git a/steps/00-sfeir-people/.eslintrc.json b/steps/00.00-sfeir-people-demo/.eslintrc.json similarity index 100% rename from steps/00-sfeir-people/.eslintrc.json rename to steps/00.00-sfeir-people-demo/.eslintrc.json diff --git a/steps/00-sfeir-people/.gitignore b/steps/00.00-sfeir-people-demo/.gitignore similarity index 100% rename from steps/00-sfeir-people/.gitignore rename to steps/00.00-sfeir-people-demo/.gitignore diff --git a/steps/00.00-sfeir-people-demo/README.md b/steps/00.00-sfeir-people-demo/README.md new file mode 100644 index 0000000..cfa89f4 --- /dev/null +++ b/steps/00.00-sfeir-people-demo/README.md @@ -0,0 +1,11 @@ +This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. diff --git a/steps/00-sfeir-people/next.config.mjs b/steps/00.00-sfeir-people-demo/next.config.mjs similarity index 73% rename from steps/00-sfeir-people/next.config.mjs rename to steps/00.00-sfeir-people-demo/next.config.mjs index adda551..16343f6 100644 --- a/steps/00-sfeir-people/next.config.mjs +++ b/steps/00.00-sfeir-people-demo/next.config.mjs @@ -1,4 +1,4 @@ -const apiUrl = new URL(process.env.API_BASE_URL); +const apiUrl = new URL(process.env.API_BASE_URL || 'http://localhost:3001'); /** @type {import('next').NextConfig} */ const nextConfig = { diff --git a/steps/00-sfeir-people/package.json b/steps/00.00-sfeir-people-demo/package.json similarity index 100% rename from steps/00-sfeir-people/package.json rename to steps/00.00-sfeir-people-demo/package.json diff --git a/steps/00-sfeir-people/postcss.config.js b/steps/00.00-sfeir-people-demo/postcss.config.js similarity index 100% rename from steps/00-sfeir-people/postcss.config.js rename to steps/00.00-sfeir-people-demo/postcss.config.js diff --git a/steps/00-sfeir-people/public/next.svg b/steps/00.00-sfeir-people-demo/public/next.svg similarity index 100% rename from steps/00-sfeir-people/public/next.svg rename to steps/00.00-sfeir-people-demo/public/next.svg diff --git a/steps/00-sfeir-people/public/vercel.svg b/steps/00.00-sfeir-people-demo/public/vercel.svg similarity index 100% rename from steps/00-sfeir-people/public/vercel.svg rename to steps/00.00-sfeir-people-demo/public/vercel.svg diff --git a/steps/00-sfeir-people/src/api/common.ts b/steps/00.00-sfeir-people-demo/src/api/common.ts similarity index 100% rename from steps/00-sfeir-people/src/api/common.ts rename to steps/00.00-sfeir-people-demo/src/api/common.ts diff --git a/steps/00-sfeir-people/src/api/error.ts b/steps/00.00-sfeir-people-demo/src/api/error.ts similarity index 100% rename from steps/00-sfeir-people/src/api/error.ts rename to steps/00.00-sfeir-people-demo/src/api/error.ts diff --git a/steps/00-sfeir-people/src/api/expenses.ts b/steps/00.00-sfeir-people-demo/src/api/expenses.ts similarity index 100% rename from steps/00-sfeir-people/src/api/expenses.ts rename to steps/00.00-sfeir-people-demo/src/api/expenses.ts diff --git a/steps/00-sfeir-people/src/api/people.ts b/steps/00.00-sfeir-people-demo/src/api/people.ts similarity index 100% rename from steps/00-sfeir-people/src/api/people.ts rename to steps/00.00-sfeir-people-demo/src/api/people.ts diff --git a/steps/00-sfeir-people/src/app/(auth)/actions.ts b/steps/00.00-sfeir-people-demo/src/app/(auth)/actions.ts similarity index 100% rename from steps/00-sfeir-people/src/app/(auth)/actions.ts rename to steps/00.00-sfeir-people-demo/src/app/(auth)/actions.ts diff --git a/steps/00-sfeir-people/src/app/(auth)/layout.tsx b/steps/00.00-sfeir-people-demo/src/app/(auth)/layout.tsx similarity index 100% rename from steps/00-sfeir-people/src/app/(auth)/layout.tsx rename to steps/00.00-sfeir-people-demo/src/app/(auth)/layout.tsx diff --git a/steps/00-sfeir-people/src/app/(auth)/login/page.tsx b/steps/00.00-sfeir-people-demo/src/app/(auth)/login/page.tsx similarity index 100% rename from steps/00-sfeir-people/src/app/(auth)/login/page.tsx rename to steps/00.00-sfeir-people-demo/src/app/(auth)/login/page.tsx diff --git a/steps/00-sfeir-people/src/app/(dashboard)/(home)/@employeesSlot/error.tsx b/steps/00.00-sfeir-people-demo/src/app/(dashboard)/(home)/@employeesSlot/error.tsx similarity index 100% rename from steps/00-sfeir-people/src/app/(dashboard)/(home)/@employeesSlot/error.tsx rename to steps/00.00-sfeir-people-demo/src/app/(dashboard)/(home)/@employeesSlot/error.tsx diff --git a/steps/00-sfeir-people/src/app/(dashboard)/(home)/@employeesSlot/loading.tsx b/steps/00.00-sfeir-people-demo/src/app/(dashboard)/(home)/@employeesSlot/loading.tsx similarity index 100% rename from steps/00-sfeir-people/src/app/(dashboard)/(home)/@employeesSlot/loading.tsx rename to steps/00.00-sfeir-people-demo/src/app/(dashboard)/(home)/@employeesSlot/loading.tsx diff --git a/steps/00-sfeir-people/src/app/(dashboard)/(home)/@employeesSlot/page.tsx b/steps/00.00-sfeir-people-demo/src/app/(dashboard)/(home)/@employeesSlot/page.tsx similarity index 100% rename from steps/00-sfeir-people/src/app/(dashboard)/(home)/@employeesSlot/page.tsx rename to steps/00.00-sfeir-people-demo/src/app/(dashboard)/(home)/@employeesSlot/page.tsx diff --git a/steps/00-sfeir-people/src/app/(dashboard)/(home)/@expensesSlot/error.tsx b/steps/00.00-sfeir-people-demo/src/app/(dashboard)/(home)/@expensesSlot/error.tsx similarity index 100% rename from steps/00-sfeir-people/src/app/(dashboard)/(home)/@expensesSlot/error.tsx rename to steps/00.00-sfeir-people-demo/src/app/(dashboard)/(home)/@expensesSlot/error.tsx diff --git a/steps/00-sfeir-people/src/app/(dashboard)/(home)/@expensesSlot/loading.tsx b/steps/00.00-sfeir-people-demo/src/app/(dashboard)/(home)/@expensesSlot/loading.tsx similarity index 100% rename from steps/00-sfeir-people/src/app/(dashboard)/(home)/@expensesSlot/loading.tsx rename to steps/00.00-sfeir-people-demo/src/app/(dashboard)/(home)/@expensesSlot/loading.tsx diff --git a/steps/00-sfeir-people/src/app/(dashboard)/(home)/@expensesSlot/page.tsx b/steps/00.00-sfeir-people-demo/src/app/(dashboard)/(home)/@expensesSlot/page.tsx similarity index 100% rename from steps/00-sfeir-people/src/app/(dashboard)/(home)/@expensesSlot/page.tsx rename to steps/00.00-sfeir-people-demo/src/app/(dashboard)/(home)/@expensesSlot/page.tsx diff --git a/steps/00-sfeir-people/src/app/(dashboard)/(home)/layout.tsx b/steps/00.00-sfeir-people-demo/src/app/(dashboard)/(home)/layout.tsx similarity index 100% rename from steps/00-sfeir-people/src/app/(dashboard)/(home)/layout.tsx rename to steps/00.00-sfeir-people-demo/src/app/(dashboard)/(home)/layout.tsx diff --git a/steps/00-sfeir-people/src/app/(dashboard)/@modal/(.)expenses/[id]/page.tsx b/steps/00.00-sfeir-people-demo/src/app/(dashboard)/@modal/(.)expenses/[id]/page.tsx similarity index 100% rename from steps/00-sfeir-people/src/app/(dashboard)/@modal/(.)expenses/[id]/page.tsx rename to steps/00.00-sfeir-people-demo/src/app/(dashboard)/@modal/(.)expenses/[id]/page.tsx diff --git a/steps/00-sfeir-people/src/app/(dashboard)/@modal/default.tsx b/steps/00.00-sfeir-people-demo/src/app/(dashboard)/@modal/default.tsx similarity index 100% rename from steps/00-sfeir-people/src/app/(dashboard)/@modal/default.tsx rename to steps/00.00-sfeir-people-demo/src/app/(dashboard)/@modal/default.tsx diff --git a/steps/00-sfeir-people/src/app/(dashboard)/employees/[id]/edit/page.tsx b/steps/00.00-sfeir-people-demo/src/app/(dashboard)/employees/[id]/edit/page.tsx similarity index 100% rename from steps/00-sfeir-people/src/app/(dashboard)/employees/[id]/edit/page.tsx rename to steps/00.00-sfeir-people-demo/src/app/(dashboard)/employees/[id]/edit/page.tsx diff --git a/steps/00-sfeir-people/src/app/(dashboard)/employees/[id]/page.tsx b/steps/00.00-sfeir-people-demo/src/app/(dashboard)/employees/[id]/page.tsx similarity index 100% rename from steps/00-sfeir-people/src/app/(dashboard)/employees/[id]/page.tsx rename to steps/00.00-sfeir-people-demo/src/app/(dashboard)/employees/[id]/page.tsx diff --git a/steps/00-sfeir-people/src/app/(dashboard)/employees/actions.ts b/steps/00.00-sfeir-people-demo/src/app/(dashboard)/employees/actions.ts similarity index 100% rename from steps/00-sfeir-people/src/app/(dashboard)/employees/actions.ts rename to steps/00.00-sfeir-people-demo/src/app/(dashboard)/employees/actions.ts diff --git a/steps/00-sfeir-people/src/app/(dashboard)/employees/error.tsx b/steps/00.00-sfeir-people-demo/src/app/(dashboard)/employees/error.tsx similarity index 100% rename from steps/00-sfeir-people/src/app/(dashboard)/employees/error.tsx rename to steps/00.00-sfeir-people-demo/src/app/(dashboard)/employees/error.tsx diff --git a/steps/00-sfeir-people/src/app/(dashboard)/employees/new/page.tsx b/steps/00.00-sfeir-people-demo/src/app/(dashboard)/employees/new/page.tsx similarity index 100% rename from steps/00-sfeir-people/src/app/(dashboard)/employees/new/page.tsx rename to steps/00.00-sfeir-people-demo/src/app/(dashboard)/employees/new/page.tsx diff --git a/steps/00-sfeir-people/src/app/(dashboard)/employees/page.tsx b/steps/00.00-sfeir-people-demo/src/app/(dashboard)/employees/page.tsx similarity index 100% rename from steps/00-sfeir-people/src/app/(dashboard)/employees/page.tsx rename to steps/00.00-sfeir-people-demo/src/app/(dashboard)/employees/page.tsx diff --git a/steps/00-sfeir-people/src/app/(dashboard)/expenses/[id]/page.tsx b/steps/00.00-sfeir-people-demo/src/app/(dashboard)/expenses/[id]/page.tsx similarity index 100% rename from steps/00-sfeir-people/src/app/(dashboard)/expenses/[id]/page.tsx rename to steps/00.00-sfeir-people-demo/src/app/(dashboard)/expenses/[id]/page.tsx diff --git a/steps/00-sfeir-people/src/app/(dashboard)/expenses/page.tsx b/steps/00.00-sfeir-people-demo/src/app/(dashboard)/expenses/page.tsx similarity index 100% rename from steps/00-sfeir-people/src/app/(dashboard)/expenses/page.tsx rename to steps/00.00-sfeir-people-demo/src/app/(dashboard)/expenses/page.tsx diff --git a/steps/00-sfeir-people/src/app/(dashboard)/layout.tsx b/steps/00.00-sfeir-people-demo/src/app/(dashboard)/layout.tsx similarity index 100% rename from steps/00-sfeir-people/src/app/(dashboard)/layout.tsx rename to steps/00.00-sfeir-people-demo/src/app/(dashboard)/layout.tsx diff --git a/steps/00-sfeir-people/src/app/api/revalidate/route.ts b/steps/00.00-sfeir-people-demo/src/app/api/revalidate/route.ts similarity index 100% rename from steps/00-sfeir-people/src/app/api/revalidate/route.ts rename to steps/00.00-sfeir-people-demo/src/app/api/revalidate/route.ts diff --git a/steps/00-sfeir-people/src/app/error.tsx b/steps/00.00-sfeir-people-demo/src/app/error.tsx similarity index 100% rename from steps/00-sfeir-people/src/app/error.tsx rename to steps/00.00-sfeir-people-demo/src/app/error.tsx diff --git a/steps/00-sfeir-people/src/app/layout.tsx b/steps/00.00-sfeir-people-demo/src/app/layout.tsx similarity index 100% rename from steps/00-sfeir-people/src/app/layout.tsx rename to steps/00.00-sfeir-people-demo/src/app/layout.tsx diff --git a/steps/00-sfeir-people/src/assets/images/profile-placeholder.jpg b/steps/00.00-sfeir-people-demo/src/assets/images/profile-placeholder.jpg similarity index 100% rename from steps/00-sfeir-people/src/assets/images/profile-placeholder.jpg rename to steps/00.00-sfeir-people-demo/src/assets/images/profile-placeholder.jpg diff --git a/steps/00-sfeir-people/src/assets/svg/logo.svg b/steps/00.00-sfeir-people-demo/src/assets/svg/logo.svg similarity index 100% rename from steps/00-sfeir-people/src/assets/svg/logo.svg rename to steps/00.00-sfeir-people-demo/src/assets/svg/logo.svg diff --git a/steps/00-sfeir-people/src/assets/svg/logoDark.svg b/steps/00.00-sfeir-people-demo/src/assets/svg/logoDark.svg similarity index 100% rename from steps/00-sfeir-people/src/assets/svg/logoDark.svg rename to steps/00.00-sfeir-people-demo/src/assets/svg/logoDark.svg diff --git a/steps/00-sfeir-people/src/components/Alert.tsx b/steps/00.00-sfeir-people-demo/src/components/Alert.tsx similarity index 100% rename from steps/00-sfeir-people/src/components/Alert.tsx rename to steps/00.00-sfeir-people-demo/src/components/Alert.tsx diff --git a/steps/00-sfeir-people/src/components/Button.tsx b/steps/00.00-sfeir-people-demo/src/components/Button.tsx similarity index 100% rename from steps/00-sfeir-people/src/components/Button.tsx rename to steps/00.00-sfeir-people-demo/src/components/Button.tsx diff --git a/steps/00-sfeir-people/src/components/EmployeeExpenses.tsx b/steps/00.00-sfeir-people-demo/src/components/EmployeeExpenses.tsx similarity index 100% rename from steps/00-sfeir-people/src/components/EmployeeExpenses.tsx rename to steps/00.00-sfeir-people-demo/src/components/EmployeeExpenses.tsx diff --git a/steps/00-sfeir-people/src/components/EmployeeForm.tsx b/steps/00.00-sfeir-people-demo/src/components/EmployeeForm.tsx similarity index 100% rename from steps/00-sfeir-people/src/components/EmployeeForm.tsx rename to steps/00.00-sfeir-people-demo/src/components/EmployeeForm.tsx diff --git a/steps/00-sfeir-people/src/components/ExpenseDetails.tsx b/steps/00.00-sfeir-people-demo/src/components/ExpenseDetails.tsx similarity index 100% rename from steps/00-sfeir-people/src/components/ExpenseDetails.tsx rename to steps/00.00-sfeir-people-demo/src/components/ExpenseDetails.tsx diff --git a/steps/00-sfeir-people/src/components/ExpenseDetailsLoading.tsx b/steps/00.00-sfeir-people-demo/src/components/ExpenseDetailsLoading.tsx similarity index 100% rename from steps/00-sfeir-people/src/components/ExpenseDetailsLoading.tsx rename to steps/00.00-sfeir-people-demo/src/components/ExpenseDetailsLoading.tsx diff --git a/steps/00-sfeir-people/src/components/ExpensesDetailsStructure.tsx b/steps/00.00-sfeir-people-demo/src/components/ExpensesDetailsStructure.tsx similarity index 83% rename from steps/00-sfeir-people/src/components/ExpensesDetailsStructure.tsx rename to steps/00.00-sfeir-people-demo/src/components/ExpensesDetailsStructure.tsx index 2cce01a..7c3aae6 100644 --- a/steps/00-sfeir-people/src/components/ExpensesDetailsStructure.tsx +++ b/steps/00.00-sfeir-people-demo/src/components/ExpensesDetailsStructure.tsx @@ -1,5 +1,3 @@ -import Skeleton from './Skeleton'; - type ExpenseDetailsStructureProps = { information: React.ReactNode; workflow: React.ReactNode; @@ -29,10 +27,12 @@ const ExpenseDetailsStructure: React.FC = ({

Amount

{amount} -
-

Employee

- {employee} -
+ {employee && ( +
+

Employee

+ {employee} +
+ )} ); diff --git a/steps/00-sfeir-people/src/components/ExpensesTable.tsx b/steps/00.00-sfeir-people-demo/src/components/ExpensesTable.tsx similarity index 100% rename from steps/00-sfeir-people/src/components/ExpensesTable.tsx rename to steps/00.00-sfeir-people-demo/src/components/ExpensesTable.tsx diff --git a/steps/00-sfeir-people/src/components/FormSubmitButton.tsx b/steps/00.00-sfeir-people-demo/src/components/FormSubmitButton.tsx similarity index 100% rename from steps/00-sfeir-people/src/components/FormSubmitButton.tsx rename to steps/00.00-sfeir-people-demo/src/components/FormSubmitButton.tsx diff --git a/steps/00-sfeir-people/src/components/Icons/ArrowLeft.tsx b/steps/00.00-sfeir-people-demo/src/components/Icons/ArrowLeft.tsx similarity index 100% rename from steps/00-sfeir-people/src/components/Icons/ArrowLeft.tsx rename to steps/00.00-sfeir-people-demo/src/components/Icons/ArrowLeft.tsx diff --git a/steps/00-sfeir-people/src/components/Icons/Eye.tsx b/steps/00.00-sfeir-people-demo/src/components/Icons/Eye.tsx similarity index 100% rename from steps/00-sfeir-people/src/components/Icons/Eye.tsx rename to steps/00.00-sfeir-people-demo/src/components/Icons/Eye.tsx diff --git a/steps/00-sfeir-people/src/components/Icons/Loader.tsx b/steps/00.00-sfeir-people-demo/src/components/Icons/Loader.tsx similarity index 100% rename from steps/00-sfeir-people/src/components/Icons/Loader.tsx rename to steps/00.00-sfeir-people-demo/src/components/Icons/Loader.tsx diff --git a/steps/00-sfeir-people/src/components/InterceptionModal.tsx b/steps/00.00-sfeir-people-demo/src/components/InterceptionModal.tsx similarity index 100% rename from steps/00-sfeir-people/src/components/InterceptionModal.tsx rename to steps/00.00-sfeir-people-demo/src/components/InterceptionModal.tsx diff --git a/steps/00-sfeir-people/src/components/Logo.tsx b/steps/00.00-sfeir-people-demo/src/components/Logo.tsx similarity index 100% rename from steps/00-sfeir-people/src/components/Logo.tsx rename to steps/00.00-sfeir-people-demo/src/components/Logo.tsx diff --git a/steps/00-sfeir-people/src/components/NavigationMenu/Logout.tsx b/steps/00.00-sfeir-people-demo/src/components/Logout.tsx similarity index 100% rename from steps/00-sfeir-people/src/components/NavigationMenu/Logout.tsx rename to steps/00.00-sfeir-people-demo/src/components/Logout.tsx diff --git a/steps/00-sfeir-people/src/components/Modal.tsx b/steps/00.00-sfeir-people-demo/src/components/Modal.tsx similarity index 100% rename from steps/00-sfeir-people/src/components/Modal.tsx rename to steps/00.00-sfeir-people-demo/src/components/Modal.tsx diff --git a/steps/00-sfeir-people/src/components/NavigationMenu/NavigationItem.tsx b/steps/00.00-sfeir-people-demo/src/components/NavigationItem.tsx similarity index 100% rename from steps/00-sfeir-people/src/components/NavigationMenu/NavigationItem.tsx rename to steps/00.00-sfeir-people-demo/src/components/NavigationItem.tsx diff --git a/steps/00-sfeir-people/src/components/NavigationMenu/NavigationMenu.tsx b/steps/00.00-sfeir-people-demo/src/components/NavigationMenu.tsx similarity index 100% rename from steps/00-sfeir-people/src/components/NavigationMenu/NavigationMenu.tsx rename to steps/00.00-sfeir-people-demo/src/components/NavigationMenu.tsx diff --git a/steps/00-sfeir-people/src/components/PageTitle.tsx b/steps/00.00-sfeir-people-demo/src/components/PageTitle.tsx similarity index 100% rename from steps/00-sfeir-people/src/components/PageTitle.tsx rename to steps/00.00-sfeir-people-demo/src/components/PageTitle.tsx diff --git a/steps/00-sfeir-people/src/components/Pagination.tsx b/steps/00.00-sfeir-people-demo/src/components/Pagination.tsx similarity index 100% rename from steps/00-sfeir-people/src/components/Pagination.tsx rename to steps/00.00-sfeir-people-demo/src/components/Pagination.tsx diff --git a/steps/00-sfeir-people/src/components/Paper.tsx b/steps/00.00-sfeir-people-demo/src/components/Paper.tsx similarity index 100% rename from steps/00-sfeir-people/src/components/Paper.tsx rename to steps/00.00-sfeir-people-demo/src/components/Paper.tsx diff --git a/steps/00-sfeir-people/src/components/PersonCard.tsx b/steps/00.00-sfeir-people-demo/src/components/PersonCard.tsx similarity index 100% rename from steps/00-sfeir-people/src/components/PersonCard.tsx rename to steps/00.00-sfeir-people-demo/src/components/PersonCard.tsx diff --git a/steps/00-sfeir-people/src/components/Search.tsx b/steps/00.00-sfeir-people-demo/src/components/Search.tsx similarity index 100% rename from steps/00-sfeir-people/src/components/Search.tsx rename to steps/00.00-sfeir-people-demo/src/components/Search.tsx diff --git a/steps/00-sfeir-people/src/components/Skeleton.tsx b/steps/00.00-sfeir-people-demo/src/components/Skeleton.tsx similarity index 100% rename from steps/00-sfeir-people/src/components/Skeleton.tsx rename to steps/00.00-sfeir-people-demo/src/components/Skeleton.tsx diff --git a/steps/00-sfeir-people/src/components/TableLoading.tsx b/steps/00.00-sfeir-people-demo/src/components/TableLoading.tsx similarity index 100% rename from steps/00-sfeir-people/src/components/TableLoading.tsx rename to steps/00.00-sfeir-people-demo/src/components/TableLoading.tsx diff --git a/steps/00-sfeir-people/src/components/TextField.tsx b/steps/00.00-sfeir-people-demo/src/components/TextField.tsx similarity index 100% rename from steps/00-sfeir-people/src/components/TextField.tsx rename to steps/00.00-sfeir-people-demo/src/components/TextField.tsx diff --git a/steps/00-sfeir-people/src/components/ThemeProvider.tsx b/steps/00.00-sfeir-people-demo/src/components/ThemeProvider.tsx similarity index 100% rename from steps/00-sfeir-people/src/components/ThemeProvider.tsx rename to steps/00.00-sfeir-people-demo/src/components/ThemeProvider.tsx diff --git a/steps/00-sfeir-people/src/functions/auth.ts b/steps/00.00-sfeir-people-demo/src/functions/auth.ts similarity index 100% rename from steps/00-sfeir-people/src/functions/auth.ts rename to steps/00.00-sfeir-people-demo/src/functions/auth.ts diff --git a/steps/00-sfeir-people/src/functions/data.ts b/steps/00.00-sfeir-people-demo/src/functions/data.ts similarity index 100% rename from steps/00-sfeir-people/src/functions/data.ts rename to steps/00.00-sfeir-people-demo/src/functions/data.ts diff --git a/steps/00-sfeir-people/src/functions/timing.ts b/steps/00.00-sfeir-people-demo/src/functions/timing.ts similarity index 100% rename from steps/00-sfeir-people/src/functions/timing.ts rename to steps/00.00-sfeir-people-demo/src/functions/timing.ts diff --git a/steps/00-sfeir-people/src/middleware.ts b/steps/00.00-sfeir-people-demo/src/middleware.ts similarity index 100% rename from steps/00-sfeir-people/src/middleware.ts rename to steps/00.00-sfeir-people-demo/src/middleware.ts diff --git a/steps/00-sfeir-people/src/styles/global.css b/steps/00.00-sfeir-people-demo/src/styles/global.css similarity index 100% rename from steps/00-sfeir-people/src/styles/global.css rename to steps/00.00-sfeir-people-demo/src/styles/global.css diff --git a/steps/00-sfeir-people/src/types.ts b/steps/00.00-sfeir-people-demo/src/types.ts similarity index 100% rename from steps/00-sfeir-people/src/types.ts rename to steps/00.00-sfeir-people-demo/src/types.ts diff --git a/steps/00-sfeir-people/tailwind.config.js b/steps/00.00-sfeir-people-demo/tailwind.config.js similarity index 100% rename from steps/00-sfeir-people/tailwind.config.js rename to steps/00.00-sfeir-people-demo/tailwind.config.js diff --git a/steps/00-sfeir-people/tsconfig.json b/steps/00.00-sfeir-people-demo/tsconfig.json similarity index 100% rename from steps/00-sfeir-people/tsconfig.json rename to steps/00.00-sfeir-people-demo/tsconfig.json diff --git a/steps/02.01-pages-layout-solution/.env.example b/steps/02.01-pages-layout-solution/.env.example new file mode 100644 index 0000000..1ebabff --- /dev/null +++ b/steps/02.01-pages-layout-solution/.env.example @@ -0,0 +1,2 @@ +API_BASE_URL=http://localhost:3001 +API_KEY=XXXX diff --git a/steps/02.01-pages-layout-solution/.eslintrc.json b/steps/02.01-pages-layout-solution/.eslintrc.json new file mode 100644 index 0000000..bffb357 --- /dev/null +++ b/steps/02.01-pages-layout-solution/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/steps/02.01-pages-layout-solution/.gitignore b/steps/02.01-pages-layout-solution/.gitignore new file mode 100644 index 0000000..fd3dbb5 --- /dev/null +++ b/steps/02.01-pages-layout-solution/.gitignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/steps/02.01-pages-layout-solution/README.md b/steps/02.01-pages-layout-solution/README.md new file mode 100644 index 0000000..3eeac38 --- /dev/null +++ b/steps/02.01-pages-layout-solution/README.md @@ -0,0 +1 @@ +# 02.01 - Pages and Layouts diff --git a/steps/02.01-pages-layout-solution/next.config.mjs b/steps/02.01-pages-layout-solution/next.config.mjs new file mode 100644 index 0000000..16343f6 --- /dev/null +++ b/steps/02.01-pages-layout-solution/next.config.mjs @@ -0,0 +1,15 @@ +const apiUrl = new URL(process.env.API_BASE_URL || 'http://localhost:3001'); + +/** @type {import('next').NextConfig} */ +const nextConfig = { + images: { + remotePatterns: [ + { + hostname: apiUrl.hostname, + port: apiUrl.port, + }, + ], + }, +}; + +export default nextConfig; diff --git a/steps/02.01-pages-layout-solution/package.json b/steps/02.01-pages-layout-solution/package.json new file mode 100644 index 0000000..bc3482d --- /dev/null +++ b/steps/02.01-pages-layout-solution/package.json @@ -0,0 +1,37 @@ +{ + "name": "02.01-solution", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "clsx": "^2.1.1", + "jose": "^5.6.3", + "jsonwebtoken": "^9.0.2", + "next": "14.2.5", + "react": "^18", + "react-dom": "^18", + "react-error-boundary": "^4.0.13", + "rehype-sanitize": "^6.0.0", + "rehype-stringify": "^10.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.0", + "server-only": "^0.0.1", + "showdown": "^2.1.0", + "unified": "^11.0.5" + }, + "devDependencies": { + "@types/jsonwebtoken": "^9.0.6", + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "@types/showdown": "^2.0.6", + "eslint": "^8", + "eslint-config-next": "14.2.5", + "typescript": "^5" + } +} diff --git a/steps/02.01-pages-layout-solution/postcss.config.js b/steps/02.01-pages-layout-solution/postcss.config.js new file mode 100644 index 0000000..33ad091 --- /dev/null +++ b/steps/02.01-pages-layout-solution/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/steps/02.01-pages-layout-solution/public/next.svg b/steps/02.01-pages-layout-solution/public/next.svg new file mode 100644 index 0000000..5174b28 --- /dev/null +++ b/steps/02.01-pages-layout-solution/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/steps/02.01-pages-layout-solution/public/portraits/men/30.jpg b/steps/02.01-pages-layout-solution/public/portraits/men/30.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d04b7a2669245212620be0cbb17922fd1cb3cbfe GIT binary patch literal 4349 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 w3$0jQtjS=6rPW;hG!!51^p@HCR)|Jncu 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 literal 0 HcmV?d00001 diff --git a/steps/02.01-pages-layout-solution/public/portraits/men/78.jpg b/steps/02.01-pages-layout-solution/public/portraits/men/78.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6438e80b9b56fcf7e6e0e9a02eb8cc54a53c91aa GIT binary patch literal 4643 zcmbu8XH=70v&VM`RRTy;=_QDYfDn3>-a!aOns6|5sR>m?K|mCd5;_LyVCW!Sy3$2D z(hmYhI!F-|!Q7y0-Sg#rKiqX^uV>G1&417AXJ$PQVUn-_&g*DsYXArY09zJNKrV6*Yg(Ww|-+&U30|p=fIP6duFJ(hR zJ@8-cZ~_o30Wd0bR_nhW`_BTky#odX0ECh#OQXEdK15a`vVp&k*BQqVnF-}=XHVoj zA`7C4FG%E}v-sUVynMz^fB5?uqfL;i#NJ>;=63qSf@gg951;kIjdDi26VJF2na|zL zm-r69?W_}+gNLax(X;=4FaQZOfePRTcY!Z(0dBwt2ob#pac2KH5Ai$C0C*B}P{iE} z1OhZM!wEPOa|MY}Uw{D)MDIw9I}n!}@dVM%W`E`Z_;;olN3pYd#Fk+?0FW&a2>Sv6 zP`m`-G?GC0nL{9)<^lkn1fVVP-+a$R;yAa7@wk6ud>H`Hg#l38@^9>JJ^*#Z8DEd;KuJzcK~6?VK|w)9MR^X!L<6IyhOsa((lK$ca&dC7va@sZ318vn6@;_1UzNHh zC?YB@F3xpDMnM`OFDxdGI4c67qN0LP!!FU#Tte`$^C14uMrZ@{lpq9zKq0(WDGo_Tvse0Do?0H$k7qyP>EZXWmdzuBY4MmBuAv*A~9yI+yi^ z-}}U*FfCA^)TsyC7)J}R#qBM)clrd0zS)pwaA;>A-pDVemCh3CeQbJ=6VrG%vmIh@ z+^gvLJo@EuGIlb!)v3^~MQ};t@~rJU)Bb__Tz77|n|eQnE}Ygk$4K6OVf(HE=KHJu zq_}lS|NiM=z8eyfP;)Wi%Ok~W8?1-#8(Ni7<|1k%jauBw{am&@`(NJHZ0U!`F!x4h zO0gqpP-gC?^bD6nrBdiu&b6rSaWxB=t2y^=yoJjsQQb8DX(n$Z^-_W?oi}PJ(JrnJ zO+(!Xzt1LbN^LMv=F>If(U~Lt0f{a=RZMcL`c$%$$X7cETj@a;F1sMQ-%IyaedN5t z0=vGI6+@Z5R6zzosw-pIlTUWU6BAZZ?qrw9(du-U^W1T-%ZwL$W@*hDf}TI^o#RyZ z6)R<`$lj5{7r5s3jNN=k`HvNOljQAdM^@q!4ovoL5WH zU0M=2gXM_hl2ChdUfYYAr2#%`E6LQ9DruP>lly}T+CPlqCv|-v4t9N|=9F)y!0#PT zgr27P>9&mNpRNkd(Fapxn8w`DGRdsv$~k0sr}mgBd5XW`DoegMZe?`Hm^y>A!)UIi z=~0s9w_4LN+KV9~S%DDK28&ktgjj`vX6BAT$<8iU- z{NVgMg(#67MtJ-+zEc|AB7IJh7=6P7Jp zNh7aVV^XFj0)tsM^`f!PGrC56_FJFNSGHwyPJFJccVb#nlHf(uV2pHmE>=UQX-+pVUW`vX;0Y7TXhC}GW^V!u)324TEkj9YHt1A zuawrhjrB`{?m2lPjyjxk75MDs?&V&y8A2MBikZ+mjy8CTT0A-o%NrD&NU(~Q-O=by zT_6Bk+154It(fxj7mm(tk_SkbZ@n#Y8J1&L+^=4V@02v?9nSQ7IKs3=dh~?n68&=M zkj%n(-I5&z+nSWqBHivt0oc?W~m%W%&eUX3outNwRbZ$zx3&5s1!2-)9qdOs5~b$wn-F{nk|732QiK5_xvf z0}6i>SqWNKKIjWNuL2oBz1H?I`1CbB#+Rx6656?0W&XiC3`y@Uqe^^bbU?BwF8<`r zmh$x{!?&VxWh5@Ijs{WZPIYH%dw;F;z*I=bk~Q^yJ1AOlmb+S)l3>f*^}#$T%El0f zQvADry7~^QTtJYJeeHL8ae+PzZhM9Ac(FLWX0c{7p6sXiCFn$1zp(VEeE_^)FsD&$ zDplQ0uw49L7N;>PlE7u{#01%#!~3Lu#P;5JES)V+XV44^ElAh72fc}C|| z{`Yc$tm;mh*oo__%wg@_%OB^LSY{XNq9XB3ySU7RTvQ`b8xXt)fWJ>R^>V>C$}l26Pc`{6z$ym8|3n4>~A|sadsA z+i1o*Gs>r%q)^fSG_DSo{Z+l!x5g884^N&yu)zR4Jc*CBXLLk%>g)7-yT}z}c+X-ohrsqV$)kdN zE8XwEHXJKEn#1!?IYSXFBmPWa7$6aamYjp&|E5LZaQ*k6n&UNTSX}nB`fA%?} z&Lmlk*kGoutpI6xY0h&O>uSVi&sYt{J&5C|LlcTUJe?039Q=q~ml9hGE^9!D{B(ik zMw9pUGP}0QW;$#Mmm3?_w!W!V+2&}{k0wzJ9Q?<`{ZX}9b6!oINLpl?a;j>~oo_=W+?#tJ_F4A(*N}SjD%kyY-YbEmysdb&Zfj5o-YAj3VX+PV*1nA?$Mv`ae)S++T zm`p~9$?~jA)vew=w=nZDhl=UsvSRtc=GdIjkelT?x9Mcp>WQVFCVM9Y`o8cy4P&O%-D*+i_q1!P%6gRp^Zt2Zrg z=vQkd1{Qs?tnyEf_O9S(q|#tnDm8|=+FsC2fk#ypOQzz5fq?6kQQ8Lx+?Zuq!{v-u zFJXN0BfYWJ))`s*W^B&`vTLL+(#@TR2q>&NEDk&@YqwDgE)tMBKila&M@6fz_SLov zP3ux}Xly;#iSg*=a}#^_*DsCeG(A&;kr-=5xY6%~?mxq<(Q1mAN1}e^Z<5iR0pPzD;iusUp5a)rv^9~tMFgn{C zaHLf)-@V!X5Ah6znL06EwF(6ZT%X^NA4?DJ+$!H>`diZnSF&4NVMGAca)2+l9q z&KyyW4t8B}no(fw81!ijXX#dxi(R`z*{hIMW&hmLG1Q6AY~8@rR7025r0(%vU>QkV zI1c&=8oKwbsBL^(!k1Cp)BUczE@SWt0W@^S9oXX6H2=Os0IlRS-^tq0DOvUKoL_|Wmt;RA)FE%q6%o7;e(gve zd38D1-#TjSQUCkdV=hbawMSVJ9xcypULaiV>owNq%*!B#b05-gX&m}t@K$r{$Hkk~ zDIM_%Ar$2w@v@yi{laX+w5oe*UOfIQFm!D6XV9eY9W|vGM$J(Z>?3OT_3pz`g9DUl zmDL>iA?2$teP*x5?3bfOI1e(Lny7JK%8wYn*cK5Mz{E>uBYn2t-l--G3 Y-IZTg@->$i4XtycyzDV!v4pAr0qF|a9RL6T literal 0 HcmV?d00001 diff --git a/steps/02.01-pages-layout-solution/public/portraits/men/86.jpg b/steps/02.01-pages-layout-solution/public/portraits/men/86.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9358491105b401a359ef698035ab9b05b84e0457 GIT binary patch literal 5433 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 Kdoq>(yYN42Z*!dh literal 0 HcmV?d00001 diff --git a/steps/02.01-pages-layout-solution/public/portraits/women/65.jpg b/steps/02.01-pages-layout-solution/public/portraits/women/65.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3cab57987a6ff10c5639fad656fb0fc77ba569c9 GIT binary patch literal 5972 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 C^!eWa literal 0 HcmV?d00001 diff --git a/steps/02.01-pages-layout-solution/public/portraits/women/85.jpg b/steps/02.01-pages-layout-solution/public/portraits/women/85.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0a900f9e8874eddf5978f08e6de03815f23bb1ea GIT binary patch literal 3912 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 literal 0 HcmV?d00001 diff --git a/steps/02.01-pages-layout-solution/public/portraits/women/93.jpg b/steps/02.01-pages-layout-solution/public/portraits/women/93.jpg new file mode 100644 index 0000000000000000000000000000000000000000..81ea0613898f679ad77f8e4563a93be893f38a07 GIT binary patch literal 4871 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 \ No newline at end of file diff --git a/steps/02.01-pages-layout-solution/src/app/(auth)/layout.tsx b/steps/02.01-pages-layout-solution/src/app/(auth)/layout.tsx new file mode 100644 index 0000000..cac31a7 --- /dev/null +++ b/steps/02.01-pages-layout-solution/src/app/(auth)/layout.tsx @@ -0,0 +1,12 @@ +type AuthLayoutProps = { + children: React.ReactNode; +}; + +const AuthLayout: React.FC = ({ children }) => ( +
+
+
{children}
+
+); + +export default AuthLayout; diff --git a/steps/02.01-pages-layout-solution/src/app/(auth)/login/page.tsx b/steps/02.01-pages-layout-solution/src/app/(auth)/login/page.tsx new file mode 100644 index 0000000..3ae0596 --- /dev/null +++ b/steps/02.01-pages-layout-solution/src/app/(auth)/login/page.tsx @@ -0,0 +1,30 @@ +import { Metadata } from 'next'; + +import TextField from '@/components/TextField'; +import Button from '@/components/Button'; + +export const metadata: Metadata = { + title: 'SFEIR People | Login', +}; + +const LoginPage = () => { + return ( +
+

Welcome !

+ + + + + ); +}; + +export default LoginPage; diff --git a/steps/02.01-pages-layout-solution/src/app/(dashboard)/employees/[id]/edit/page.tsx b/steps/02.01-pages-layout-solution/src/app/(dashboard)/employees/[id]/edit/page.tsx new file mode 100644 index 0000000..9d29dc0 --- /dev/null +++ b/steps/02.01-pages-layout-solution/src/app/(dashboard)/employees/[id]/edit/page.tsx @@ -0,0 +1,24 @@ +import EmployeeForm from '@/components/EmployeeForm'; +import PageTitle from '@/components/PageTitle'; + +import employeesData from '@/data/employees.json'; + +const EmployeeDetail = async ({ params }: { params: { id: string } }) => { + const employee = employeesData.find((employee) => employee.id === params.id); + + if (!employee) return Single Employee - Not found; + + return ( + <> + + Single Employee - {employee.firstname} {employee.lastname} | Edit + + +
+ +
+ + ); +}; + +export default EmployeeDetail; diff --git a/steps/02.01-pages-layout-solution/src/app/(dashboard)/employees/[id]/page.tsx b/steps/02.01-pages-layout-solution/src/app/(dashboard)/employees/[id]/page.tsx new file mode 100644 index 0000000..a498c63 --- /dev/null +++ b/steps/02.01-pages-layout-solution/src/app/(dashboard)/employees/[id]/page.tsx @@ -0,0 +1,21 @@ +import PageTitle from '@/components/PageTitle'; +import PersonCard from '@/components/PersonCard'; + +import employeesData from '@/data/employees.json'; + +const EmployeeDetail = async ({ params }: { params: { id: string } }) => { + const employee = employeesData.find((employee) => employee.id === params.id); + + if (!employee) return Single Employee - Not found; + + return ( + <> + + Single Employee - {employee.firstname} {employee.lastname} + + + + ); +}; + +export default EmployeeDetail; diff --git a/steps/02.01-pages-layout-solution/src/app/(dashboard)/employees/new/page.tsx b/steps/02.01-pages-layout-solution/src/app/(dashboard)/employees/new/page.tsx new file mode 100644 index 0000000..f1e5157 --- /dev/null +++ b/steps/02.01-pages-layout-solution/src/app/(dashboard)/employees/new/page.tsx @@ -0,0 +1,18 @@ +import EmployeeForm from '@/components/EmployeeForm'; +import PageTitle from '@/components/PageTitle'; + +const EmployeeDetail = async () => { + return ( + <> + + Employees | Create + + +
+ +
+ + ); +}; + +export default EmployeeDetail; diff --git a/steps/02.01-pages-layout-solution/src/app/(dashboard)/employees/page.tsx b/steps/02.01-pages-layout-solution/src/app/(dashboard)/employees/page.tsx new file mode 100644 index 0000000..55c3cd8 --- /dev/null +++ b/steps/02.01-pages-layout-solution/src/app/(dashboard)/employees/page.tsx @@ -0,0 +1,19 @@ +import PageTitle from '@/components/PageTitle'; +import PersonCard from '@/components/PersonCard'; + +import employeesData from '@/data/employees.json'; + +const Employees = async () => { + return ( +
+ Employees +
+ {employeesData?.map((employee) => ( + + ))} +
+
+ ); +}; + +export default Employees; diff --git a/steps/02.01-pages-layout-solution/src/app/(dashboard)/expenses/[id]/page.tsx b/steps/02.01-pages-layout-solution/src/app/(dashboard)/expenses/[id]/page.tsx new file mode 100644 index 0000000..bda58e2 --- /dev/null +++ b/steps/02.01-pages-layout-solution/src/app/(dashboard)/expenses/[id]/page.tsx @@ -0,0 +1,18 @@ +import ExpenseDetails from '@/components/ExpensesDetails'; +import PageTitle from '@/components/PageTitle'; + +import expensesData from '@/data/expenses.json'; +import { Expense } from '@/types'; + +const SingleExpense = ({ params }: { params: { id: string } }) => { + const expense = expensesData.find((expense) => expense.id === params.id); + + return ( + <> + Single Expense - {expense?.label || 'Not found'} + {expense && } + + ); +}; + +export default SingleExpense; diff --git a/steps/02.01-pages-layout-solution/src/app/(dashboard)/expenses/page.tsx b/steps/02.01-pages-layout-solution/src/app/(dashboard)/expenses/page.tsx new file mode 100644 index 0000000..55d1d65 --- /dev/null +++ b/steps/02.01-pages-layout-solution/src/app/(dashboard)/expenses/page.tsx @@ -0,0 +1,17 @@ +import ExpensesTable from '@/components/ExpensesTable'; +import PageTitle from '@/components/PageTitle'; + +import { Expense } from '@/types'; + +import expensesData from '@/data/expenses.json'; + +const Expenses = async () => { + return ( + <> + Expenses + } /> + + ); +}; + +export default Expenses; diff --git a/steps/02.01-pages-layout-solution/src/app/(dashboard)/layout.tsx b/steps/02.01-pages-layout-solution/src/app/(dashboard)/layout.tsx new file mode 100644 index 0000000..07b02c8 --- /dev/null +++ b/steps/02.01-pages-layout-solution/src/app/(dashboard)/layout.tsx @@ -0,0 +1,29 @@ +import { Metadata } from 'next'; +import Link from 'next/link'; +import Image from 'next/image'; + +import NavigationMenu from '@/components/NavigationMenu'; + +import logo from '@/assets/svg/logo.svg'; + +type DashboardLayoutProps = { children: React.ReactNode }; + +export const metadata: Metadata = { + title: 'SFEIR People | Dashboard', +}; + +const DashboardLayout: React.FC = async ({ children }) => { + return ( +
+
+ + People logo + + +
+
{children}
+
+ ); +}; + +export default DashboardLayout; diff --git a/steps/02.01-pages-layout-solution/src/app/(dashboard)/page.tsx b/steps/02.01-pages-layout-solution/src/app/(dashboard)/page.tsx new file mode 100644 index 0000000..f581ebb --- /dev/null +++ b/steps/02.01-pages-layout-solution/src/app/(dashboard)/page.tsx @@ -0,0 +1,11 @@ +import PageTitle from '@/components/PageTitle'; + +const HomePage = () => { + return ( + <> + SFEIR People + + ); +}; + +export default HomePage; diff --git a/steps/02.01-pages-layout-solution/src/app/layout.tsx b/steps/02.01-pages-layout-solution/src/app/layout.tsx new file mode 100644 index 0000000..e7d90e9 --- /dev/null +++ b/steps/02.01-pages-layout-solution/src/app/layout.tsx @@ -0,0 +1,21 @@ +import type { Metadata } from 'next'; +import { Inter } from 'next/font/google'; + +const inter = Inter({ subsets: ['latin'] }); + +import '@/styles/global.css'; + +export const metadata: Metadata = { + title: 'SFEIR People', + description: 'SFEIR People dashboard application', +}; + +const RootLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => { + return ( + + {children} + + ); +}; + +export default RootLayout; diff --git a/steps/02.01-pages-layout-solution/src/assets/images/profile-placeholder.jpg b/steps/02.01-pages-layout-solution/src/assets/images/profile-placeholder.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6fa00ea6c9e371e542006bb4bd69ae5e6922a324 GIT binary patch literal 11940 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 m>aIBzX~|65y0*1u%UVegt+JNI)Z4`imH;jI + + + + + + + + diff --git a/steps/02.01-pages-layout-solution/src/assets/svg/logoDark.svg b/steps/02.01-pages-layout-solution/src/assets/svg/logoDark.svg new file mode 100644 index 0000000..06eb6ed --- /dev/null +++ b/steps/02.01-pages-layout-solution/src/assets/svg/logoDark.svg @@ -0,0 +1,28 @@ + + + + + + + + + diff --git a/steps/02.01-pages-layout-solution/src/components/Alert.tsx b/steps/02.01-pages-layout-solution/src/components/Alert.tsx new file mode 100644 index 0000000..1c90895 --- /dev/null +++ b/steps/02.01-pages-layout-solution/src/components/Alert.tsx @@ -0,0 +1,17 @@ +import clsx from 'clsx'; + +type AlertProps = { + children: React.ReactNode; + className?: string; +}; + +const Alert: React.FC = ({ children, className }) => ( +
+ {children} +
+); + +export default Alert; diff --git a/steps/02.01-pages-layout-solution/src/components/Button.tsx b/steps/02.01-pages-layout-solution/src/components/Button.tsx new file mode 100644 index 0000000..2cee623 --- /dev/null +++ b/steps/02.01-pages-layout-solution/src/components/Button.tsx @@ -0,0 +1,34 @@ +import clsx from 'clsx'; + +export type ButtonProps = { + children: React.ReactNode; + className?: string; + variant?: 'primary' | 'secondary'; + component?: C; +} & Omit, 'className' | 'variant'>; + +const classNames = { + primary: 'inline-block text-white bg-blue-700 hover:bg-blue-800 font-medium rounded-lg text-sm px-5 py-2.5', + secondary: [ + 'inline-block py-2.5 px-5 text-sm font-medium text-slate-900 bg-white rounded-lg border border-gray-200', + 'hover:bg-gray-100 hover:text-blue-700', + 'dark:bg-slate-900 dark:text-white dark:hover:bg-slate-950 dark:hover:text-blue-200 dark:hover:border-blue-200', + ].join(' '), +}; + +const Button = ({ + children, + className, + variant = 'secondary', + component, + ...restProps +}: ButtonProps) => { + const Component = component || 'button'; + return ( + + {children} + + ); +}; + +export default Button; diff --git a/steps/02.01-pages-layout-solution/src/components/EmployeeForm.tsx b/steps/02.01-pages-layout-solution/src/components/EmployeeForm.tsx new file mode 100644 index 0000000..dc15055 --- /dev/null +++ b/steps/02.01-pages-layout-solution/src/components/EmployeeForm.tsx @@ -0,0 +1,113 @@ +'use client'; + +import Image from 'next/image'; +import TextField from '@/components/TextField'; +import { Person } from '@/types'; +import { useFormState } from 'react-dom'; + +import placeholderImage from '@/assets/images/profile-placeholder.jpg'; +import Button from './Button'; + +type ActionState = { + validationErrors?: { [key: string]: Array }; +}; + +type Action = (id: string, formData: FormData) => Promise; + +type EmployeeFormProps = { + employee?: Person; + action?: Action; + className?: string; +}; + +const initialState = { + validationErrors: {}, +} as ActionState; + +const EmployeeForm: React.FC = ({ employee, action, className }) => { + // @ts-ignore + const [state, formAction] = useFormState(action, initialState as unknown as void); + + return ( +
+
+ {employee +
+
+
+ + + + + +
+
+ + + +
+
+
+ +
+
+ ); +}; + +export default EmployeeForm; diff --git a/steps/02.01-pages-layout-solution/src/components/ExpensesDetails.tsx b/steps/02.01-pages-layout-solution/src/components/ExpensesDetails.tsx new file mode 100644 index 0000000..f59b51a --- /dev/null +++ b/steps/02.01-pages-layout-solution/src/components/ExpensesDetails.tsx @@ -0,0 +1,56 @@ +import { Expense } from '@/types'; +import Paper from './Paper'; + +type ExpenseDetailsRowProps = { + label: string; + value: string; +}; + +const ExpenseDetailsRow: React.FC = ({ label, value }) => ( +
+ {label} + {value} +
+); + +type ExpenseDetailsProps = { + expense: Expense; +}; + +const ExpenseDetails: React.FC = ({ expense }) => ( + <> +
+
+

Information

+ + + + + +
+
+

Workflow

+ + + + + +
+
+
+
+

Amount

+ + + + + +
+
+ +); + +export default ExpenseDetails; diff --git a/steps/02.01-pages-layout-solution/src/components/ExpensesTable.tsx b/steps/02.01-pages-layout-solution/src/components/ExpensesTable.tsx new file mode 100644 index 0000000..d124cab --- /dev/null +++ b/steps/02.01-pages-layout-solution/src/components/ExpensesTable.tsx @@ -0,0 +1,49 @@ +import { Expense } from '@/types'; +import clsx from 'clsx'; + +type ExpensesTableProps = { + expenses: Array; +}; + +const ExpensesTable: React.FC = ({ expenses }) => { + return ( + + + + + + + + + + + {expenses.map((expense, index) => ( + + + + + + + ))} + +
+ Label + + Creation date + + Category + + Price +
{expense.label}{new Date(expense.creationDate).toLocaleDateString()}{expense.category} + {expense.price.priceIncludingTax} {expense.price.currency} +
+ ); +}; + +export default ExpensesTable; diff --git a/steps/02.01-pages-layout-solution/src/components/Icons/ArrowLeft.tsx b/steps/02.01-pages-layout-solution/src/components/Icons/ArrowLeft.tsx new file mode 100644 index 0000000..1cc10c7 --- /dev/null +++ b/steps/02.01-pages-layout-solution/src/components/Icons/ArrowLeft.tsx @@ -0,0 +1,25 @@ +type ArrowLeftProps = { + className?: string; +}; + +const ArrowLeft: React.FC = ({ className }) => ( + +); + +export default ArrowLeft; diff --git a/steps/02.01-pages-layout-solution/src/components/Icons/Eye.tsx b/steps/02.01-pages-layout-solution/src/components/Icons/Eye.tsx new file mode 100644 index 0000000..04beb91 --- /dev/null +++ b/steps/02.01-pages-layout-solution/src/components/Icons/Eye.tsx @@ -0,0 +1,11 @@ +type EyeProps = { + className?: string; +}; + +const Eye: React.FC = ({ className }) => ( + + + +); + +export default Eye; diff --git a/steps/02.01-pages-layout-solution/src/components/Icons/Loader.tsx b/steps/02.01-pages-layout-solution/src/components/Icons/Loader.tsx new file mode 100644 index 0000000..9c81994 --- /dev/null +++ b/steps/02.01-pages-layout-solution/src/components/Icons/Loader.tsx @@ -0,0 +1,25 @@ +type LoaderProps = { + className?: string; +}; + +const Loader: React.FC = ({ className }) => ( + +); + +export default Loader; diff --git a/steps/02.01-pages-layout-solution/src/components/NavigationMenu.tsx b/steps/02.01-pages-layout-solution/src/components/NavigationMenu.tsx new file mode 100644 index 0000000..e3bf528 --- /dev/null +++ b/steps/02.01-pages-layout-solution/src/components/NavigationMenu.tsx @@ -0,0 +1,25 @@ +const NavigationMenu = () => { + return ( + + ); +}; + +export default NavigationMenu; diff --git a/steps/02.01-pages-layout-solution/src/components/PageTitle.tsx b/steps/02.01-pages-layout-solution/src/components/PageTitle.tsx new file mode 100644 index 0000000..217fe69 --- /dev/null +++ b/steps/02.01-pages-layout-solution/src/components/PageTitle.tsx @@ -0,0 +1,25 @@ +import Link from 'next/link'; + +import ArrowLeft from './Icons/ArrowLeft'; + +type PageTitleProps = { + children: React.ReactNode; + backHref?: string; +}; + +const PageTitle: React.FC = ({ children, backHref }) => ( +
+ {backHref && ( + + + Go back + + )} +

{children}

+
+); + +export default PageTitle; diff --git a/steps/02.01-pages-layout-solution/src/components/Paper.tsx b/steps/02.01-pages-layout-solution/src/components/Paper.tsx new file mode 100644 index 0000000..8ba656e --- /dev/null +++ b/steps/02.01-pages-layout-solution/src/components/Paper.tsx @@ -0,0 +1,14 @@ +import clsx from 'clsx'; + +type PaperProps = React.HTMLAttributes & { + children: React.ReactNode; + rounded?: boolean; +}; + +const Paper: React.FC = ({ children, rounded = true, ...restProps }) => ( +
+ {children} +
+); + +export default Paper; diff --git a/steps/02.01-pages-layout-solution/src/components/PersonCard.tsx b/steps/02.01-pages-layout-solution/src/components/PersonCard.tsx new file mode 100644 index 0000000..b38ee53 --- /dev/null +++ b/steps/02.01-pages-layout-solution/src/components/PersonCard.tsx @@ -0,0 +1,43 @@ +import Image from 'next/image'; + +import { Person } from '@/types'; + +import placeholderImage from '@/assets/images/profile-placeholder.jpg'; + +type PersonCardProps = React.HTMLAttributes & { + person: Person; + actions?: React.ReactNode; + compact?: boolean; +}; + +const PersonCard: React.FC = ({ person, actions, className, compact = false }) => { + return ( +
+
+ {`Picture + + {person.firstname} {person.lastname} + + {person.position} +
+ + {!compact && ( +
+ {person.phone} + {person.email} + {person.manager && {person.manager}} +
+ )} + + {actions &&
{actions}
} +
+ ); +}; + +export default PersonCard; diff --git a/steps/02.01-pages-layout-solution/src/components/TextField.tsx b/steps/02.01-pages-layout-solution/src/components/TextField.tsx new file mode 100644 index 0000000..d8061e9 --- /dev/null +++ b/steps/02.01-pages-layout-solution/src/components/TextField.tsx @@ -0,0 +1,31 @@ +import clsx from 'clsx'; + +type TextFieldProps = React.InputHTMLAttributes & { + label: string; + id: string; + type?: string; + className?: string; + errorMessages?: Array; +}; + +const TextField: React.FC = ({ label, id, type = 'text', className, errorMessages, ...restProps }) => { + return ( +
+ + + {errorMessages?.length &&

{errorMessages[0]}

} +
+ ); +}; + +export default TextField; diff --git a/steps/02.01-pages-layout-solution/src/data/employees.json b/steps/02.01-pages-layout-solution/src/data/employees.json new file mode 100644 index 0000000..5f5342e --- /dev/null +++ b/steps/02.01-pages-layout-solution/src/data/employees.json @@ -0,0 +1,152 @@ +[ + { + "id": "5763cd4d9d2a4f259b53c901", + "photo": "/portraits/women/85.jpg", + "firstname": "Leanne", + "lastname": "Woodard", + "position": "Developer", + "entryDate": "27/10/2015", + "birthDate": "02/01/1974", + "gender": "f", + "email": "woodard.l@acme.com", + "phone": "0784112248", + "isManager": false, + "manager": "Erika", + "managerId": "5763cd4d3b57c672861bfa1f" + }, + { + "id": "5763cd4d51fdb6588742f99e", + "photo": "/portraits/men/56.jpg", + "firstname": "Castaneda", + "lastname": "Salinas", + "position": "Developer", + "entryDate": "04/10/2015", + "birthDate": "22/01/1963", + "gender": "m", + "email": "salinas.c@acme.com", + "phone": "0145652522", + "isManager": false, + "manager": "Erika", + "managerId": "5763cd4d3b57c672861bfa1f" + }, + { + "id": "5763cd4dba6362a3f92c954e", + "photo": "/portraits/women/24.jpg", + "firstname": "Phyllis", + "lastname": "Donovan", + "position": "Sales", + "entryDate": "30/03/2015", + "birthDate": "30/11/1951", + "gender": "f", + "email": "donovan.p@acme.com", + "phone": "0685230125", + "isManager": false, + "manager": "Erika", + "managerId": "5763cd4d3b57c672861bfa1f" + }, + { + "id": "5763cd4d3b57c672861bfa1f", + "photo": "/portraits/women/65.jpg", + "firstname": "Erika", + "lastname": "Guzman", + "position": "Product Owner", + "entryDate": "13/05/2016", + "birthDate": "19/03/1962", + "gender": "f", + "email": "guzman.e@acme.com", + "phone": "0678412587", + "isManager": true, + "manager": "Mercedes", + "managerId": "5763cd4d979b62a209809160" + }, + { + "id": "5763cd4d5fc36e4f842ca5a9", + "photo": "/portraits/men/30.jpg", + "firstname": "Moody", + "lastname": "Prince", + "position": "Developer", + "entryDate": "28/09/2015", + "birthDate": "15/04/1971", + "gender": "m", + "email": "prince.m@acme.com", + "phone": "0662589632", + "isManager": false, + "manager": "Mercedes", + "managerId": "5763cd4d979b62a209809160" + }, + { + "id": "5763cd4d979b62a209809160", + "photo": "/portraits/women/8.jpg", + "firstname": "Mercedes", + "lastname": "Hebert", + "position": "Product Owner", + "entryDate": "02/01/2016", + "birthDate": "20/07/1947", + "gender": "f", + "email": "hebert.m@acme.com", + "phone": "0125878522", + "isManager": true, + "manager": "Mclaughlin", + "managerId": "5763cd4dfa6f96cd26c65787" + }, + { + "id": "5763cd4d15e6c2c28b70f2e8", + "photo": "/portraits/men/86.jpg", + "firstname": "Howell", + "lastname": "Mcknight", + "position": "Sales", + "entryDate": "26/09/2015", + "birthDate": "18/07/1979", + "gender": "m", + "email": "mcknight.h@acme.com", + "phone": "0456987425", + "isManager": false, + "manager": "Mclaughlin", + "managerId": "5763cd4dfa6f96cd26c65787" + }, + { + "id": "5763cd4d5d6ad8dfc6c34883", + "photo": "/portraits/women/93.jpg", + "firstname": "Lizzie", + "lastname": "Morris", + "position": "Human Resources", + "entryDate": "03/05/2016", + "birthDate": "15/11/1981", + "gender": "f", + "email": "morris.l@acme.com", + "phone": "0662259988", + "isManager": false, + "manager": "Mclaughlin", + "managerId": "5763cd4dfa6f96cd26c65787" + }, + { + "id": "5763cd4dc378a38ecd387737", + "photo": "/portraits/men/34.jpg", + "firstname": "Roy", + "lastname": "Nielsen", + "position": "Sales", + "entryDate": "17/05/2016", + "birthDate": "21/10/1951", + "gender": "m", + "email": "nielsen.r@acme.com", + "phone": "0755669551", + "isManager": false, + "manager": "Mclaughlin", + "managerId": "5763cd4dfa6f96cd26c65787" + }, + { + "id": "5763cd4dfa6f96cd26c65787", + "photo": "/portraits/men/78.jpg", + "firstname": "Mclaughlin", + "lastname": "Cochran", + "position": "Director", + "entryDate": "11/04/2016", + "birthDate": "19/03/1973", + "gender": "m", + "email": "cochran.m@acme.com", + "phone": "0266334856", + "isManager": true, + "manager": "", + "managerId": "" + } +] diff --git a/steps/02.01-pages-layout-solution/src/data/expenses.json b/steps/02.01-pages-layout-solution/src/data/expenses.json new file mode 100644 index 0000000..0d096dc --- /dev/null +++ b/steps/02.01-pages-layout-solution/src/data/expenses.json @@ -0,0 +1,342 @@ +[ + { + "id": "0475830f-a563-44e0-8c5c-6d829c11a132", + "employeeId": "5763cd4d51fdb6588742f99e", + "price": { + "priceIncludingTax": 120.50, + "taxAmount": 20.50, + "priceExcludingTax": 100, + "currency": "EUR" + }, + "label": "Business Lunch", + "description": "Lunch with a client to discuss a new project.", + "category": "Meals", + "receiptLink": "https://example.com/receipt1.pdf", + "status": "approved", + "creationDate": "2024-03-15T09:30:00Z", + "updateDate": "2024-03-18T14:45:00Z" + }, + { + "id": "a2e8b2c4-99d8-4c13-9a9c-0a8a68623260", + "employeeId": "5763cd4d51fdb6588742f99e", + "price": { + "priceIncludingTax": 55.20, + "taxAmount": 7.20, + "priceExcludingTax": 48, + "currency": "USD" + }, + "label": "Office Supplies", + "description": "Purchase of paper, pens, etc.", + "category": "Supplies", + "receiptLink": "https://example.com/receipt2.jpg", + "status": "created", + "creationDate": "2024-05-02T11:20:00Z", + "updateDate": "2024-05-02T11:20:00Z" + }, + { + "id": "3d3fb561-0d9c-4021-8285-d2f49c40c47d", + "employeeId": "5763cd4d51fdb6588742f99e", + "price": { + "priceIncludingTax": 250, + "taxAmount": 0, + "priceExcludingTax": 250, + "currency": "EUR" + }, + "label": "Plane Ticket", + "description": "Business trip to Berlin.", + "category": "Travel", + "receiptLink": "https://example.com/receipt3.png", + "status": "declined", + "creationDate": "2024-04-10T16:45:00Z", + "updateDate": "2024-04-12T09:30:00Z" + }, + { + "id": "4c41689c-e5f5-4029-a181-38c498e7a82b", + "employeeId": "5763cd4d5fc36e4f842ca5a9", + "price": { + "priceIncludingTax": 80.00, + "taxAmount": 13.33, + "priceExcludingTax": 66.67, + "currency": "USD" + }, + "label": "Hotel", + "description": "Hotel night in London.", + "category": "Accommodation", + "receiptLink": "https://example.com/receipt4.pdf", + "status": "approved", + "creationDate": "2024-06-20T08:15:00Z", + "updateDate": "2024-06-22T10:30:00Z" + }, + { + "id": "871030e0-e485-41d5-882c-30201429a23f", + "employeeId": "5763cd4d5fc36e4f842ca5a9", + "price": { + "priceIncludingTax": 35.75, + "taxAmount": 5.75, + "priceExcludingTax": 30, + "currency": "EUR" + }, + "label": "Taxi Fare", + "description": "Ride from the airport to the office.", + "category": "Transportation", + "receiptLink": "https://example.com/receipt5.jpg", + "status": "submitted", + "creationDate": "2024-07-05T19:00:00Z", + "updateDate": "2024-07-05T19:00:00Z" + }, + { + "id": "e38e7368-8686-4211-a6a2-844806839a4c", + "employeeId": "5763cd4d979b62a209809160", + "price": { + "priceIncludingTax": 180.00, + "taxAmount": 30.00, + "priceExcludingTax": 150, + "currency": "USD" + }, + "label": "Car Rental", + "description": "Car rental for a week.", + "category": "Transportation", + "receiptLink": "https://example.com/receipt6.png", + "status": "approved", + "creationDate": "2024-02-28T13:40:00Z", + "updateDate": "2024-03-02T11:15:00Z" + }, + { + "id": "90432a7b-3009-491a-832b-a4622721a490", + "employeeId": "5763cd4d15e6c2c28b70f2e8", + "price": { + "priceIncludingTax": 65.00, + "taxAmount": 10.83, + "priceExcludingTax": 54.17, + "currency": "USD" + }, + "label": "Client Meal", + "description": "Dinner with a potential client.", + "category": "Meals", + "receiptLink": "https://example.com/receipt7.pdf", + "status": "in_review", + "creationDate": "2024-07-18T20:30:00Z", + "updateDate": "2024-07-19T09:00:00Z" + }, + { + "id": "a88a026a-e928-48b6-8a1d-90d980e7a423", + "employeeId": "5763cd4d5d6ad8dfc6c34883", + "price": { + "priceIncludingTax": 95.99, + "taxAmount": 15.99, + "priceExcludingTax": 80, + "currency": "EUR" + }, + "label": "Software", + "description": "Monthly subscription to project management software.", + "category": "Software", + "receiptLink": "https://example.com/receipt8.jpg", + "status": "approved", + "creationDate": "2024-05-10T10:00:00Z", + "updateDate": "2024-05-11T14:20:00Z" + }, + { + "id": "21e5615a-9c2a-40f2-a489-0c2f4a866098", + "employeeId": "5763cd4dc378a38ecd387737", + "price": { + "priceIncludingTax": 25.50, + "taxAmount": 4.25, + "priceExcludingTax": 21.25, + "currency": "USD" + }, + "label": "Parking Fees", + "description": "Parking fees at the airport.", + "category": "Transportation", + "receiptLink": "https://example.com/receipt9.png", + "status": "created", + "creationDate": "2024-07-30T17:45:00Z", + "updateDate": "2024-07-30T17:45:00Z" + }, + { + "id": "5200c69a-e387-4292-9282-98e66878524c", + "employeeId": "5763cd4dfa6f96cd26c65787", + "price": { + "priceIncludingTax": 150.00, + "taxAmount": 25.00, + "priceExcludingTax": 125, + "currency": "USD" + }, + "label": "Training", + "description": "Participation in professional training.", + "category": "Training", + "receiptLink": "https://example.com/receipt10.pdf", + "status": "approved", + "creationDate": "2024-04-05T09:00:00Z", + "updateDate": "2024-04-07T11:30:00Z" + }, + { + "id": "9b60a0a2-d2cf-41ab-a8a6-690080e77898", + "employeeId": "5763cd4d9d2a4f259b53c901", + "price": { + "priceIncludingTax": 75.20, + "taxAmount": 12.53, + "priceExcludingTax": 62.67, + "currency": "EUR" + }, + "label": "Office Supplies", + "description": "Purchase of office supplies.", + "category": "Supplies", + "receiptLink": "https://example.com/receipt11.jpg", + "status": "approved", + "creationDate": "2024-06-12T14:20:00Z", + "updateDate": "2024-06-14T10:15:00Z" + }, + { + "id": "8478041f-7a41-46f0-9158-34759c409873", + "employeeId": "5763cd4d51fdb6588742f99e", + "price": { + "priceIncludingTax": 280.00, + "taxAmount": 46.67, + "priceExcludingTax": 233.33, + "currency": "USD" + }, + "label": "Plane Ticket", + "description": "Business trip to New York.", + "category": "Travel", + "receiptLink": "https://example.com/receipt12.png", + "status": "submitted", + "creationDate": "2024-07-25T11:30:00Z", + "updateDate": "2024-07-25T11:30:00Z" + }, + { + "id": "4902854f-2049-4498-9597-74823829501f", + "employeeId": "5763cd4dba6362a3f92c954e", + "price": { + "priceIncludingTax": 95.00, + "taxAmount": 15.83, + "priceExcludingTax": 79.17, + "currency": "EUR" + }, + "label": "Hotel", + "description": "Hotel night in Paris.", + "category": "Accommodation", + "receiptLink": "https://example.com/receipt13.pdf", + "status": "in_review", + "creationDate": "2024-08-01T08:45:00Z", + "updateDate": "2024-08-02T10:00:00Z" + }, + { + "id": "4309573f-8903-4a42-a095-839529490582", + "employeeId": "5763cd4d3b57c672861bfa1f", + "price": { + "priceIncludingTax": 42.50, + "taxAmount": 7.08, + "priceExcludingTax": 35.42, + "currency": "EUR" + }, + "label": "Taxi Fare", + "description": "Ride from the station to the conference venue.", + "category": "Transportation", + "receiptLink": "https://example.com/receipt14.jpg", + "status": "approved", + "creationDate": "2024-05-20T18:30:00Z", + "updateDate": "2024-05-22T09:15:00Z" + }, + { + "id": "3028759f-9023-4a83-a785-928375928375", + "employeeId": "5763cd4d5fc36e4f842ca5a9", + "price": { + "priceIncludingTax": 190.00, + "taxAmount": 31.67, + "priceExcludingTax": 158.33, + "currency": "USD" + }, + "label": "Car Rental", + "description": "Weekend car rental.", + "category": "Transportation", + "receiptLink": "https://example.com/receipt15.png", + "status": "created", + "creationDate": "2024-07-28T12:00:00Z", + "updateDate": "2024-07-28T12:00:00Z" + }, + { + "id": "92837592-8375-9283-7592-837592837592", + "employeeId": "5763cd4d979b62a209809160", + "price": { + "priceIncludingTax": 70.00, + "taxAmount": 11.67, + "priceExcludingTax": 58.33, + "currency": "USD" + }, + "label": "Client Meal", + "description": "Lunch with a client to discuss a project.", + "category": "Meals", + "receiptLink": "https://example.com/receipt16.pdf", + "status": "declined", + "creationDate": "2024-03-08T13:15:00Z", + "updateDate": "2024-03-10T09:30:00Z" + }, + { + "id": "85930583-0385-9305-8303-859305830385", + "employeeId": "5763cd4d15e6c2c28b70f2e8", + "price": { + "priceIncludingTax": 110.50, + "taxAmount": 18.42, + "priceExcludingTax": 92.08, + "currency": "EUR" + }, + "label": "Software", + "description": "Purchase of a license for design software.", + "category": "Software", + "receiptLink": "https://example.com/receipt17.jpg", + "status": "approved", + "creationDate": "2024-06-05T10:45:00Z", + "updateDate": "2024-06-07T14:30:00Z" + }, + { + "id": "03958305-8305-9385-0385-930583059385", + "employeeId": "5763cd4d5d6ad8dfc6c34883", + "price": { + "priceIncludingTax": 35.00, + "taxAmount": 5.83, + "priceExcludingTax": 29.17, + "currency": "USD" + }, + "label": "Parking Fees", + "description": "Parking fees at the conference center.", + "category": "Transportation", + "receiptLink": "https://example.com/receipt18.png", + "status": "submitted", + "creationDate": "2024-07-15T16:20:00Z", + "updateDate": "2024-07-15T16:20:00Z" + }, + { + "id": "93850395-8503-9585-0395-850395850395", + "employeeId": "5763cd4dc378a38ecd387737", + "price": { + "priceIncludingTax": 165.00, + "taxAmount": 27.50, + "priceExcludingTax": 137.50, + "currency": "USD" + }, + "label": "Training", + "description": "Registration for a webinar on digital marketing.", + "category": "Training", + "receiptLink": "https://example.com/receipt19.pdf", + "status": "in_review", + "creationDate": "2024-07-10T09:00:00Z", + "updateDate": "2024-07-11T14:30:00Z" + }, + { + "id": "85039585-0395-8503-9585-039585039585", + "employeeId": "5763cd4dfa6f96cd26c65787", + "price": { + "priceIncludingTax": 82.75, + "taxAmount": 13.79, + "priceExcludingTax": 68.96, + "currency": "EUR" + }, + "label": "Office Supplies", + "description": "Purchase of ink cartridges for the printer.", + "category": "Supplies", + "receiptLink": "https://example.com/receipt20.jpg", + "status": "approved", + "creationDate": "2024-06-28T11:45:00Z", + "updateDate": "2024-06-30T09:15:00Z" + } +] \ No newline at end of file diff --git a/steps/02.01-pages-layout-solution/src/styles/global.css b/steps/02.01-pages-layout-solution/src/styles/global.css new file mode 100644 index 0000000..f77ed90 --- /dev/null +++ b/steps/02.01-pages-layout-solution/src/styles/global.css @@ -0,0 +1,41 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --color-bg-global: #e9effc; + --color-bg-primary: #ffffff; + --color-bg-secondary: #f5f5f5; + --color-text-primary: #000000; + + --spacing-sm: 0.5rem; + --spacing-md: 0.75rem; + --spacing-lg: 1rem; + --spacing-xl: 1.5rem; +} + +/* Headings */ + +.heading1 { + font-size: 2rem; + font-weight: bold; + margin-bottom: var(--spacing-lg); +} + +.heading2 { + font-size: 1.5rem; + font-weight: bold; + margin-bottom: var(--spacing-lg); +} + +.heading3 { + font-size: 1.125rem; + font-weight: bold; + margin-bottom: var(--spacing-lg); +} + +.heading4 { + font-size: 1rem; + font-weight: bold; + margin-bottom: var(--spacing-lg); +} diff --git a/steps/02.01-pages-layout-solution/src/types.ts b/steps/02.01-pages-layout-solution/src/types.ts new file mode 100644 index 0000000..82cffb5 --- /dev/null +++ b/steps/02.01-pages-layout-solution/src/types.ts @@ -0,0 +1,39 @@ +export type Person = { + id: string; + photo?: string; + firstname: string; + lastname: string; + position: string; + entryDate: string; + birthDate: string; + gender: string; + email: string; + phone: string; + isManager: boolean; + manager?: string; + managerId?: string; +}; + +export type Expense = { + id: string; + employeeId: string; + price: { + priceIncludingTax: number; + taxAmount: number; + priceExcludingTax: number; + currency: string; + }; + label: string; + description: string; + category: string; + receiptLink: string; + status: 'approved' | 'created' | 'declined'; + creationDate: string; + updateDate: string; +}; + +export type PaginationAttributes = { + per_page?: number; + page: number; + total_pages: number; +}; diff --git a/steps/02.01-pages-layout-solution/tailwind.config.js b/steps/02.01-pages-layout-solution/tailwind.config.js new file mode 100644 index 0000000..eaa361c --- /dev/null +++ b/steps/02.01-pages-layout-solution/tailwind.config.js @@ -0,0 +1,9 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ['./src/**/*.{js,ts,jsx,tsx,mdx}'], + darkMode: 'selector', + theme: { + extend: {}, + }, + plugins: [], +}; diff --git a/steps/02.01-pages-layout-solution/tsconfig.json b/steps/02.01-pages-layout-solution/tsconfig.json new file mode 100644 index 0000000..7b28589 --- /dev/null +++ b/steps/02.01-pages-layout-solution/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/steps/02.01-pages-layout/.env.example b/steps/02.01-pages-layout/.env.example new file mode 100644 index 0000000..1ebabff --- /dev/null +++ b/steps/02.01-pages-layout/.env.example @@ -0,0 +1,2 @@ +API_BASE_URL=http://localhost:3001 +API_KEY=XXXX diff --git a/steps/02.01-pages-layout/.eslintrc.json b/steps/02.01-pages-layout/.eslintrc.json new file mode 100644 index 0000000..bffb357 --- /dev/null +++ b/steps/02.01-pages-layout/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/steps/02.01-pages-layout/.gitignore b/steps/02.01-pages-layout/.gitignore new file mode 100644 index 0000000..fd3dbb5 --- /dev/null +++ b/steps/02.01-pages-layout/.gitignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/steps/02.01-pages-layout/README.md b/steps/02.01-pages-layout/README.md new file mode 100644 index 0000000..3eeac38 --- /dev/null +++ b/steps/02.01-pages-layout/README.md @@ -0,0 +1 @@ +# 02.01 - Pages and Layouts diff --git a/steps/02.01-pages-layout/data/employees.json b/steps/02.01-pages-layout/data/employees.json new file mode 100644 index 0000000..5f5342e --- /dev/null +++ b/steps/02.01-pages-layout/data/employees.json @@ -0,0 +1,152 @@ +[ + { + "id": "5763cd4d9d2a4f259b53c901", + "photo": "/portraits/women/85.jpg", + "firstname": "Leanne", + "lastname": "Woodard", + "position": "Developer", + "entryDate": "27/10/2015", + "birthDate": "02/01/1974", + "gender": "f", + "email": "woodard.l@acme.com", + "phone": "0784112248", + "isManager": false, + "manager": "Erika", + "managerId": "5763cd4d3b57c672861bfa1f" + }, + { + "id": "5763cd4d51fdb6588742f99e", + "photo": "/portraits/men/56.jpg", + "firstname": "Castaneda", + "lastname": "Salinas", + "position": "Developer", + "entryDate": "04/10/2015", + "birthDate": "22/01/1963", + "gender": "m", + "email": "salinas.c@acme.com", + "phone": "0145652522", + "isManager": false, + "manager": "Erika", + "managerId": "5763cd4d3b57c672861bfa1f" + }, + { + "id": "5763cd4dba6362a3f92c954e", + "photo": "/portraits/women/24.jpg", + "firstname": "Phyllis", + "lastname": "Donovan", + "position": "Sales", + "entryDate": "30/03/2015", + "birthDate": "30/11/1951", + "gender": "f", + "email": "donovan.p@acme.com", + "phone": "0685230125", + "isManager": false, + "manager": "Erika", + "managerId": "5763cd4d3b57c672861bfa1f" + }, + { + "id": "5763cd4d3b57c672861bfa1f", + "photo": "/portraits/women/65.jpg", + "firstname": "Erika", + "lastname": "Guzman", + "position": "Product Owner", + "entryDate": "13/05/2016", + "birthDate": "19/03/1962", + "gender": "f", + "email": "guzman.e@acme.com", + "phone": "0678412587", + "isManager": true, + "manager": "Mercedes", + "managerId": "5763cd4d979b62a209809160" + }, + { + "id": "5763cd4d5fc36e4f842ca5a9", + "photo": "/portraits/men/30.jpg", + "firstname": "Moody", + "lastname": "Prince", + "position": "Developer", + "entryDate": "28/09/2015", + "birthDate": "15/04/1971", + "gender": "m", + "email": "prince.m@acme.com", + "phone": "0662589632", + "isManager": false, + "manager": "Mercedes", + "managerId": "5763cd4d979b62a209809160" + }, + { + "id": "5763cd4d979b62a209809160", + "photo": "/portraits/women/8.jpg", + "firstname": "Mercedes", + "lastname": "Hebert", + "position": "Product Owner", + "entryDate": "02/01/2016", + "birthDate": "20/07/1947", + "gender": "f", + "email": "hebert.m@acme.com", + "phone": "0125878522", + "isManager": true, + "manager": "Mclaughlin", + "managerId": "5763cd4dfa6f96cd26c65787" + }, + { + "id": "5763cd4d15e6c2c28b70f2e8", + "photo": "/portraits/men/86.jpg", + "firstname": "Howell", + "lastname": "Mcknight", + "position": "Sales", + "entryDate": "26/09/2015", + "birthDate": "18/07/1979", + "gender": "m", + "email": "mcknight.h@acme.com", + "phone": "0456987425", + "isManager": false, + "manager": "Mclaughlin", + "managerId": "5763cd4dfa6f96cd26c65787" + }, + { + "id": "5763cd4d5d6ad8dfc6c34883", + "photo": "/portraits/women/93.jpg", + "firstname": "Lizzie", + "lastname": "Morris", + "position": "Human Resources", + "entryDate": "03/05/2016", + "birthDate": "15/11/1981", + "gender": "f", + "email": "morris.l@acme.com", + "phone": "0662259988", + "isManager": false, + "manager": "Mclaughlin", + "managerId": "5763cd4dfa6f96cd26c65787" + }, + { + "id": "5763cd4dc378a38ecd387737", + "photo": "/portraits/men/34.jpg", + "firstname": "Roy", + "lastname": "Nielsen", + "position": "Sales", + "entryDate": "17/05/2016", + "birthDate": "21/10/1951", + "gender": "m", + "email": "nielsen.r@acme.com", + "phone": "0755669551", + "isManager": false, + "manager": "Mclaughlin", + "managerId": "5763cd4dfa6f96cd26c65787" + }, + { + "id": "5763cd4dfa6f96cd26c65787", + "photo": "/portraits/men/78.jpg", + "firstname": "Mclaughlin", + "lastname": "Cochran", + "position": "Director", + "entryDate": "11/04/2016", + "birthDate": "19/03/1973", + "gender": "m", + "email": "cochran.m@acme.com", + "phone": "0266334856", + "isManager": true, + "manager": "", + "managerId": "" + } +] diff --git a/steps/02.01-pages-layout/data/expenses.json b/steps/02.01-pages-layout/data/expenses.json new file mode 100644 index 0000000..0d096dc --- /dev/null +++ b/steps/02.01-pages-layout/data/expenses.json @@ -0,0 +1,342 @@ +[ + { + "id": "0475830f-a563-44e0-8c5c-6d829c11a132", + "employeeId": "5763cd4d51fdb6588742f99e", + "price": { + "priceIncludingTax": 120.50, + "taxAmount": 20.50, + "priceExcludingTax": 100, + "currency": "EUR" + }, + "label": "Business Lunch", + "description": "Lunch with a client to discuss a new project.", + "category": "Meals", + "receiptLink": "https://example.com/receipt1.pdf", + "status": "approved", + "creationDate": "2024-03-15T09:30:00Z", + "updateDate": "2024-03-18T14:45:00Z" + }, + { + "id": "a2e8b2c4-99d8-4c13-9a9c-0a8a68623260", + "employeeId": "5763cd4d51fdb6588742f99e", + "price": { + "priceIncludingTax": 55.20, + "taxAmount": 7.20, + "priceExcludingTax": 48, + "currency": "USD" + }, + "label": "Office Supplies", + "description": "Purchase of paper, pens, etc.", + "category": "Supplies", + "receiptLink": "https://example.com/receipt2.jpg", + "status": "created", + "creationDate": "2024-05-02T11:20:00Z", + "updateDate": "2024-05-02T11:20:00Z" + }, + { + "id": "3d3fb561-0d9c-4021-8285-d2f49c40c47d", + "employeeId": "5763cd4d51fdb6588742f99e", + "price": { + "priceIncludingTax": 250, + "taxAmount": 0, + "priceExcludingTax": 250, + "currency": "EUR" + }, + "label": "Plane Ticket", + "description": "Business trip to Berlin.", + "category": "Travel", + "receiptLink": "https://example.com/receipt3.png", + "status": "declined", + "creationDate": "2024-04-10T16:45:00Z", + "updateDate": "2024-04-12T09:30:00Z" + }, + { + "id": "4c41689c-e5f5-4029-a181-38c498e7a82b", + "employeeId": "5763cd4d5fc36e4f842ca5a9", + "price": { + "priceIncludingTax": 80.00, + "taxAmount": 13.33, + "priceExcludingTax": 66.67, + "currency": "USD" + }, + "label": "Hotel", + "description": "Hotel night in London.", + "category": "Accommodation", + "receiptLink": "https://example.com/receipt4.pdf", + "status": "approved", + "creationDate": "2024-06-20T08:15:00Z", + "updateDate": "2024-06-22T10:30:00Z" + }, + { + "id": "871030e0-e485-41d5-882c-30201429a23f", + "employeeId": "5763cd4d5fc36e4f842ca5a9", + "price": { + "priceIncludingTax": 35.75, + "taxAmount": 5.75, + "priceExcludingTax": 30, + "currency": "EUR" + }, + "label": "Taxi Fare", + "description": "Ride from the airport to the office.", + "category": "Transportation", + "receiptLink": "https://example.com/receipt5.jpg", + "status": "submitted", + "creationDate": "2024-07-05T19:00:00Z", + "updateDate": "2024-07-05T19:00:00Z" + }, + { + "id": "e38e7368-8686-4211-a6a2-844806839a4c", + "employeeId": "5763cd4d979b62a209809160", + "price": { + "priceIncludingTax": 180.00, + "taxAmount": 30.00, + "priceExcludingTax": 150, + "currency": "USD" + }, + "label": "Car Rental", + "description": "Car rental for a week.", + "category": "Transportation", + "receiptLink": "https://example.com/receipt6.png", + "status": "approved", + "creationDate": "2024-02-28T13:40:00Z", + "updateDate": "2024-03-02T11:15:00Z" + }, + { + "id": "90432a7b-3009-491a-832b-a4622721a490", + "employeeId": "5763cd4d15e6c2c28b70f2e8", + "price": { + "priceIncludingTax": 65.00, + "taxAmount": 10.83, + "priceExcludingTax": 54.17, + "currency": "USD" + }, + "label": "Client Meal", + "description": "Dinner with a potential client.", + "category": "Meals", + "receiptLink": "https://example.com/receipt7.pdf", + "status": "in_review", + "creationDate": "2024-07-18T20:30:00Z", + "updateDate": "2024-07-19T09:00:00Z" + }, + { + "id": "a88a026a-e928-48b6-8a1d-90d980e7a423", + "employeeId": "5763cd4d5d6ad8dfc6c34883", + "price": { + "priceIncludingTax": 95.99, + "taxAmount": 15.99, + "priceExcludingTax": 80, + "currency": "EUR" + }, + "label": "Software", + "description": "Monthly subscription to project management software.", + "category": "Software", + "receiptLink": "https://example.com/receipt8.jpg", + "status": "approved", + "creationDate": "2024-05-10T10:00:00Z", + "updateDate": "2024-05-11T14:20:00Z" + }, + { + "id": "21e5615a-9c2a-40f2-a489-0c2f4a866098", + "employeeId": "5763cd4dc378a38ecd387737", + "price": { + "priceIncludingTax": 25.50, + "taxAmount": 4.25, + "priceExcludingTax": 21.25, + "currency": "USD" + }, + "label": "Parking Fees", + "description": "Parking fees at the airport.", + "category": "Transportation", + "receiptLink": "https://example.com/receipt9.png", + "status": "created", + "creationDate": "2024-07-30T17:45:00Z", + "updateDate": "2024-07-30T17:45:00Z" + }, + { + "id": "5200c69a-e387-4292-9282-98e66878524c", + "employeeId": "5763cd4dfa6f96cd26c65787", + "price": { + "priceIncludingTax": 150.00, + "taxAmount": 25.00, + "priceExcludingTax": 125, + "currency": "USD" + }, + "label": "Training", + "description": "Participation in professional training.", + "category": "Training", + "receiptLink": "https://example.com/receipt10.pdf", + "status": "approved", + "creationDate": "2024-04-05T09:00:00Z", + "updateDate": "2024-04-07T11:30:00Z" + }, + { + "id": "9b60a0a2-d2cf-41ab-a8a6-690080e77898", + "employeeId": "5763cd4d9d2a4f259b53c901", + "price": { + "priceIncludingTax": 75.20, + "taxAmount": 12.53, + "priceExcludingTax": 62.67, + "currency": "EUR" + }, + "label": "Office Supplies", + "description": "Purchase of office supplies.", + "category": "Supplies", + "receiptLink": "https://example.com/receipt11.jpg", + "status": "approved", + "creationDate": "2024-06-12T14:20:00Z", + "updateDate": "2024-06-14T10:15:00Z" + }, + { + "id": "8478041f-7a41-46f0-9158-34759c409873", + "employeeId": "5763cd4d51fdb6588742f99e", + "price": { + "priceIncludingTax": 280.00, + "taxAmount": 46.67, + "priceExcludingTax": 233.33, + "currency": "USD" + }, + "label": "Plane Ticket", + "description": "Business trip to New York.", + "category": "Travel", + "receiptLink": "https://example.com/receipt12.png", + "status": "submitted", + "creationDate": "2024-07-25T11:30:00Z", + "updateDate": "2024-07-25T11:30:00Z" + }, + { + "id": "4902854f-2049-4498-9597-74823829501f", + "employeeId": "5763cd4dba6362a3f92c954e", + "price": { + "priceIncludingTax": 95.00, + "taxAmount": 15.83, + "priceExcludingTax": 79.17, + "currency": "EUR" + }, + "label": "Hotel", + "description": "Hotel night in Paris.", + "category": "Accommodation", + "receiptLink": "https://example.com/receipt13.pdf", + "status": "in_review", + "creationDate": "2024-08-01T08:45:00Z", + "updateDate": "2024-08-02T10:00:00Z" + }, + { + "id": "4309573f-8903-4a42-a095-839529490582", + "employeeId": "5763cd4d3b57c672861bfa1f", + "price": { + "priceIncludingTax": 42.50, + "taxAmount": 7.08, + "priceExcludingTax": 35.42, + "currency": "EUR" + }, + "label": "Taxi Fare", + "description": "Ride from the station to the conference venue.", + "category": "Transportation", + "receiptLink": "https://example.com/receipt14.jpg", + "status": "approved", + "creationDate": "2024-05-20T18:30:00Z", + "updateDate": "2024-05-22T09:15:00Z" + }, + { + "id": "3028759f-9023-4a83-a785-928375928375", + "employeeId": "5763cd4d5fc36e4f842ca5a9", + "price": { + "priceIncludingTax": 190.00, + "taxAmount": 31.67, + "priceExcludingTax": 158.33, + "currency": "USD" + }, + "label": "Car Rental", + "description": "Weekend car rental.", + "category": "Transportation", + "receiptLink": "https://example.com/receipt15.png", + "status": "created", + "creationDate": "2024-07-28T12:00:00Z", + "updateDate": "2024-07-28T12:00:00Z" + }, + { + "id": "92837592-8375-9283-7592-837592837592", + "employeeId": "5763cd4d979b62a209809160", + "price": { + "priceIncludingTax": 70.00, + "taxAmount": 11.67, + "priceExcludingTax": 58.33, + "currency": "USD" + }, + "label": "Client Meal", + "description": "Lunch with a client to discuss a project.", + "category": "Meals", + "receiptLink": "https://example.com/receipt16.pdf", + "status": "declined", + "creationDate": "2024-03-08T13:15:00Z", + "updateDate": "2024-03-10T09:30:00Z" + }, + { + "id": "85930583-0385-9305-8303-859305830385", + "employeeId": "5763cd4d15e6c2c28b70f2e8", + "price": { + "priceIncludingTax": 110.50, + "taxAmount": 18.42, + "priceExcludingTax": 92.08, + "currency": "EUR" + }, + "label": "Software", + "description": "Purchase of a license for design software.", + "category": "Software", + "receiptLink": "https://example.com/receipt17.jpg", + "status": "approved", + "creationDate": "2024-06-05T10:45:00Z", + "updateDate": "2024-06-07T14:30:00Z" + }, + { + "id": "03958305-8305-9385-0385-930583059385", + "employeeId": "5763cd4d5d6ad8dfc6c34883", + "price": { + "priceIncludingTax": 35.00, + "taxAmount": 5.83, + "priceExcludingTax": 29.17, + "currency": "USD" + }, + "label": "Parking Fees", + "description": "Parking fees at the conference center.", + "category": "Transportation", + "receiptLink": "https://example.com/receipt18.png", + "status": "submitted", + "creationDate": "2024-07-15T16:20:00Z", + "updateDate": "2024-07-15T16:20:00Z" + }, + { + "id": "93850395-8503-9585-0395-850395850395", + "employeeId": "5763cd4dc378a38ecd387737", + "price": { + "priceIncludingTax": 165.00, + "taxAmount": 27.50, + "priceExcludingTax": 137.50, + "currency": "USD" + }, + "label": "Training", + "description": "Registration for a webinar on digital marketing.", + "category": "Training", + "receiptLink": "https://example.com/receipt19.pdf", + "status": "in_review", + "creationDate": "2024-07-10T09:00:00Z", + "updateDate": "2024-07-11T14:30:00Z" + }, + { + "id": "85039585-0395-8503-9585-039585039585", + "employeeId": "5763cd4dfa6f96cd26c65787", + "price": { + "priceIncludingTax": 82.75, + "taxAmount": 13.79, + "priceExcludingTax": 68.96, + "currency": "EUR" + }, + "label": "Office Supplies", + "description": "Purchase of ink cartridges for the printer.", + "category": "Supplies", + "receiptLink": "https://example.com/receipt20.jpg", + "status": "approved", + "creationDate": "2024-06-28T11:45:00Z", + "updateDate": "2024-06-30T09:15:00Z" + } +] \ No newline at end of file diff --git a/steps/02.01-pages-layout/next.config.mjs b/steps/02.01-pages-layout/next.config.mjs new file mode 100644 index 0000000..16343f6 --- /dev/null +++ b/steps/02.01-pages-layout/next.config.mjs @@ -0,0 +1,15 @@ +const apiUrl = new URL(process.env.API_BASE_URL || 'http://localhost:3001'); + +/** @type {import('next').NextConfig} */ +const nextConfig = { + images: { + remotePatterns: [ + { + hostname: apiUrl.hostname, + port: apiUrl.port, + }, + ], + }, +}; + +export default nextConfig; diff --git a/steps/02.01-pages-layout/package.json b/steps/02.01-pages-layout/package.json new file mode 100644 index 0000000..30ff397 --- /dev/null +++ b/steps/02.01-pages-layout/package.json @@ -0,0 +1,37 @@ +{ + "name": "02.01", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "clsx": "^2.1.1", + "jose": "^5.6.3", + "jsonwebtoken": "^9.0.2", + "next": "14.2.5", + "react": "^18", + "react-dom": "^18", + "react-error-boundary": "^4.0.13", + "rehype-sanitize": "^6.0.0", + "rehype-stringify": "^10.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.0", + "server-only": "^0.0.1", + "showdown": "^2.1.0", + "unified": "^11.0.5" + }, + "devDependencies": { + "@types/jsonwebtoken": "^9.0.6", + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "@types/showdown": "^2.0.6", + "eslint": "^8", + "eslint-config-next": "14.2.5", + "typescript": "^5" + } +} diff --git a/steps/02.01-pages-layout/postcss.config.js b/steps/02.01-pages-layout/postcss.config.js new file mode 100644 index 0000000..33ad091 --- /dev/null +++ b/steps/02.01-pages-layout/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/steps/02.01-pages-layout/public/next.svg b/steps/02.01-pages-layout/public/next.svg new file mode 100644 index 0000000..5174b28 --- /dev/null +++ b/steps/02.01-pages-layout/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/steps/02.01-pages-layout/public/portraits/men/30.jpg b/steps/02.01-pages-layout/public/portraits/men/30.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d04b7a2669245212620be0cbb17922fd1cb3cbfe GIT binary patch literal 4349 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 w3$0jQtjS=6rPW;hG!!51^p@HCR)|Jncu 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 literal 0 HcmV?d00001 diff --git a/steps/02.01-pages-layout/public/portraits/men/78.jpg b/steps/02.01-pages-layout/public/portraits/men/78.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6438e80b9b56fcf7e6e0e9a02eb8cc54a53c91aa GIT binary patch literal 4643 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 Y-IZTg@->$i4XtycyzDV!v4pAr0qF|a9RL6T literal 0 HcmV?d00001 diff --git a/steps/02.01-pages-layout/public/portraits/men/86.jpg b/steps/02.01-pages-layout/public/portraits/men/86.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9358491105b401a359ef698035ab9b05b84e0457 GIT binary patch literal 5433 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 Kdoq>(yYN42Z*!dh literal 0 HcmV?d00001 diff --git a/steps/02.01-pages-layout/public/portraits/women/65.jpg b/steps/02.01-pages-layout/public/portraits/women/65.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3cab57987a6ff10c5639fad656fb0fc77ba569c9 GIT binary patch literal 5972 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 C^!eWa literal 0 HcmV?d00001 diff --git a/steps/02.01-pages-layout/public/portraits/women/85.jpg b/steps/02.01-pages-layout/public/portraits/women/85.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0a900f9e8874eddf5978f08e6de03815f23bb1ea GIT binary patch literal 3912 zcmb7;`9IW)+r~e$FvgZ;EMpyJgdzJ<)}bsjmSzSmLwyw~YZMC67@9*_hY^x}Fxkp7 z92}`bwidF4QKu|fr(~-vc{-vbPxwyA2F0MX1USdk=x5_BLmEu zOjIz+O$E;d<$kI6U%L^=iLDozlRz*cjnukjQHNvPDrG`BHNT3tE5kPeJ?ZW_Z*h>v zEaQ7ICvLXX=Xm02IHu%;Xys;EFp<L?(9bI6|+R(0oPK zai|y)`nPpDx_V$BtYfYPZJOC1wtqgGErwGAGYy)r+D_h1Y1 zyaLi~+#GJ1lnbfDRfT_L8{gRoba|GY5PzzPD1LEM=~2?=Bk2I^9&QoV*D~m9=3bHx zIvZJ+3!CxOKL9RxkefmLwVrvgjh{9Bv-rP^@HHw1+I(_ysx;a>qLWyj0?so^xDDdG z@p0`iu5a{VK|9Le@_kf`g@8CInN@Xl-Q}>}cUOjPMyzmGvYA?Q{+&d@QTxBQjDOY> zJ7dW0*f6^jxdi@xMQX*>?li+2_ga~~@)UYe@DWP!DsFEeZEn9beo!xl8wxHu z{45sfYsup4kkdo#DBB9Ccmoj|J2Y)kv9;I({TuhUR{iw?KH`#A8vW?g4xloH++ z6wa3HvyD$*GF8G}Fa+TuHV%MjlBL;}#x1@$JK+Uzs6$2Xik-%y03jb~WGj-;xUfUy zikr2Edb?t`v#g;9fCe{TBlzB3xpwa)NlCBts+$qY>HD*Nn8sqCt3PE!pX6{hjN`l&zV6aR;AGF?JnNnJfD1rJpGdDSR2KQA%+Zzke^w}C4px? z5MrMhSO%KUthhHv%^HC&4Wm*UJ-kB zq!&OeK?AcR&F3bIhC4#ln(5WgUwW-q_5hs4c#E#?-6X3m&r?I%ye}4~hQRUNf({>3 zLzZ8RZ%_V#R>InBChU^TIW&XcD)2V7ZP6 zALju(gQVwEkKQ~-xehztI<@RngOWHsc-R%-UNtoORwJR^+w8Z8s|zOf=YnJP?I4+} zBe#Vqa+eQT-a;LV{ALguMOu&F`fT*E{ayWgzIj?Em46tYBP4fQ)Ze5L>RA$yf=B1R zDZDarnrAdY83odsi}I%PnrB~NHB_R?mg)x6AJba%lB_BG!4Ix&ae{}AYY<8Om`puO z=sTU2(w+GiVvncfk)-L|3DU#ElT3tkwlv`q;b_*lW=u({mQKeMGk{J?7`V>4*cf{* z)uWxo>G{+WuAFfMg^6!v2xtAeJK{y5g!733&(yEho{pf0;0Z%aP!rWIS~Bd0gPJHI zpH8J(c**C~ZMQbmDGcb!Me$Q~=em$4)UWwxK@?Pc@Qnq|#U)pqO>1aW~ zz`pQw4Tf!yeDtW~&X|>4=eNyjDaNq}c>ifS-jz~=;=kJKHO&QNA62=bj_M^1iTd~f2-yE89-P; zeB$lx8O7ziw|5#m_8rEcuKh^$O{J}u{;%Z3)#xe#^z`MaLV`;V+lGlLi0A(0=RXqC^b6h6bVf5m7sEP!37%oYQ~t|8 z=Xb^j4;+D`*ug&UOlB~QEY6x$uEhjYt|~gEvTbZIFfm6WtUP5m*g3Mp<@Q<$cz)~1 zk(?pMko!xxdMgBG6|thx^e0Y`;GAN=6>MU{qk6gnW^(r*=w3TH3E47tXr+&7j%D02 zNSb%hfOb*wNC>7kuU@-GwxQ{`h>9j56p)pQ{&d9_g~cI@-$j?1urXC99sBcTG@Wo1 zPl0K#MxT%tMG6_fFfM9I&4ggiH?rGi$U>;~Cf7By-LIR~1?RfInksu8meLc}-=@FVg=gxh$5!&ZOfIl|%6~Djrnc=E7q@Ui zn!<_Xm8Tz4Lw~CBo>)dM*^B~Eh+@@0qI#9Oa%6j(-1Qz^^sfDE1lmqHzfQj(YV73h zSbOjid=)8k##;BU*vOeUCHSJ9Ju?yW+O&9z`OQ|EKqF!pu0Xv<8^}NN1I2Tizt9cu zRwGfaV^H{qWeU&Tomy&B5IOCp)pl&8HTJ;+rtWMA^=wdq(PSU=cGxqt80xCi9vzQJ zNt^#q>7EtGe!S$mZ#~lvuXre+Wp8RmoR1|bFIYF$(%mX?Qe)XghBF*ARCtlre1jy; z!)M)HWt~A%UbEurPEj`K5_n`3#iHE$Drgni|CO{N+@$SYC{YM zD^^7fd2+WhN@wr5YF%?4E_%+%zx_>gcr5MWpl(MVkSw5YIHJyLHl*DRj^_@S1-{dI z>TRiZ9-IC6Bn}_H3b3ABDP}mn-t2pP+UF`2gwqq{j)^@Zj0)&Q)(nQcI{FBY*xZ&s z%Pgmf&LYAp$ct`-kN9(E=86!3{S0wqs9nZhl&O>)s$ig`>TpSI;cShsloB zi_|ux|83-Yd87Ji@sf)|Sa(l##3ZGPRw6{SzHruuCV3dIt`u?k-XFInb+hh!kq|!U zflkP0ZcvYFMU;@gItIVRBZ=7-dnZdp!KcvOdU!kxbGi$0>k(9TB8~oMP)3+OGHq;R zFf3$4gQ);tmxZiq^`<9Kk96)zZdYijTOD${u=&fu_O$$*tkwFV@%v!#19#=T!#;zp fWjVO>XXx&Z5vA@4qqC^kr0*s9L!mL&2b2E;=DEz# literal 0 HcmV?d00001 diff --git a/steps/02.01-pages-layout/public/portraits/women/93.jpg b/steps/02.01-pages-layout/public/portraits/women/93.jpg new file mode 100644 index 0000000000000000000000000000000000000000..81ea0613898f679ad77f8e4563a93be893f38a07 GIT binary patch literal 4871 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 \ No newline at end of file diff --git a/steps/02.01-pages-layout/src/app/.gitkeep b/steps/02.01-pages-layout/src/app/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/steps/02.01-pages-layout/src/assets/images/profile-placeholder.jpg b/steps/02.01-pages-layout/src/assets/images/profile-placeholder.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6fa00ea6c9e371e542006bb4bd69ae5e6922a324 GIT binary patch literal 11940 zcmd^kbyQT{_xB7lLyB}aNJw``icZ zuh0AX{PSDuUGKd!>+btGpMCZ|XPUd#f}?@LHa0DwRsKnivOE+znC0C+G2 z9s-7khrlBsz#}4~BO@arA!FY}yMc~}jgOCqjf+c2LQO_UL`95?OU_76MMHa={x$&_ z6Dt!PD>dD1y6=?$5fBiN5s|Twk+J9qaS7@E^>NV%z(9oSh3f?YDFJX8KoAD-q8UI4 z00KbYy}deM&Vqn&urhoY47y$d0KkF3z>9If4HyiE4nhY2fPJXto>#j6OA><9GiKuv zfzHG`SUvVN63RhE;yrgcFiMdm2?s?IZYLh91FZka72w)pJW5=imq6O~iU^DZgmbO# zkQh_a73Y{?%K8T_(xlbbA6Jp{eib9~P5Ba;b=5#6{=tln+S>bay6WGx!MzPLjIH&5 z@ZkSmGvb*gMz zE*1F|BASS-(1q%J1zbs}x4x_s$2GEFAz*@`{?KRUtyjpEq%+>Hy)=ma<_e+DGo?lL z%AvbLE+wE|TDuwaDm<_Pcqj3w(yWC)#s^r~vSwCl(vxyo0YJ#+SZg?V&QjzGx|HCS z;|vVn5^#B5A`mXlR+n9H+9hyJ5ZpGeR2kPFCJz4%f|Bpoehz9f68R1M$CY|*r!vke zg0B2G3Vc)Bp(~~RXEqAqmh)5~XM#&Z?=L<^!n^!~u2|6JSo>Yi&nuFPo8=UbW+2Ni z7~aU8dJi(_`Jb%ccW7>erm?8Z?wBuBe=;b}jPi0;n*P|0-<6~tIcA96Pf>|aEi>_E zgoM_w(vcFqJ75 zG1&I`TXvT@-!xXW65m7=*-fY<5l_2ySXq0LA`IY%lHuVKaYPf&t5kR zrbhqn&h>+2YHobyutzFrRi5_6~a*l-Xd{5uLnE1v^?%I9%QA>; zv9u&B*Wx7rK&ZX%B)1my_zJm_pnajc%G%`(^_LK?Lri#LVMo>_a7}>o{;(USDxYu# znR2*{=Y8Q=y+W=@KJmkg#l|RRmk?-OFM6==nbLno=vOg_tgu5{M(^6vDA(7gv)qp} zdZ~Y1=r&o+&CfFJyu=_wpA3_gxUH zI!Jo7_BQ6Gk)U4NCFi<8`QYW~ay4ukUxAh7EvqKA&)&{nL01w)AmQ8KY0$M!LsGSu z>d_t`Vb9^re*bN3R6^v6{Zq2>q8o?yh<&JPZ_P1B`p@E9ve~v81hLe1W9+tbg2-vg z)Lc6U1fZ1Pb%171?gr6me(a+q5fIq5E_p#>04n-jcy$GigeOvF66iX0Q8DSdnX!+t zYdcSg@f>uoDhq3E%!g}doj+K1U8wG7KdtJrx{+SpznyxYyKgp=v^wD4RW<)Rk}zzC zLyrysf`M>g1lUIBr&Txr5CoRT5P@J~VUts`vT<-erb4F(hwXU~VY?w91nvUhAKWdS z)4rB{zluuxb$;83f$5j~N!7WD2_7WGl9b3&44;K!M}lAVE8ep?r=feOf+LR_Tp}`9 zn4s(Dowh%A^8U+n)Y!K55tGt=hT`{QpP=Voib-fD)qS$3$ByM!CvpnEjE(m>7)+}N zWzyIdyYrSs>wgiC&mx9e<-NQ$mQ9hNx!#bgo@|fW&cE6&x@>amkD1*qKCu*dR(*qF3*CTR0DRb*X4*XG z(H1+c51@~EeU3h2CrfYR>8eA~g&9YYX5uVb>sYq}jwHEcN(ySrHPpJScV~9sx(1om z4~9G9+$IgXiP})YIU>;>9mMoVL8Ig%ESJ((;`ucmW@zTH+t2qXzK+%&8sp3C8F`&U zI^Uc~cL4xu=tRZ`v41l-MZH~OSu$39O72Ao@h_S)PO~m-mg%k0SsH2KM0-b6U^vq-0Jlnj3 zv><$MB^Wz~1cOi5FYmeB4%1^UYIP7RdaKE8x_QJ?ZiZt2*iWZo6oKZpAIvkVhNj(Ykoie@-?M+!L~f8CyeKLSCMOLpx{=2T!R^LJR>2m-azQ{OymMcw zTfjmgG7LwaOhd!_zEr5N4sWP9jwtLd<^H2~p621aL77gb!iZv{(AW=jo$eUHZ4eFz z5|gGUUz>-`?RRjvBW@mbX-3hK zVL89F!$a3uX?=y+-F*qvJtMeU71UrL$0PU({BZGP zbtQs}q`@AZHi`pXQ43d5?$#TTkK`%k%--?>&h3EE+4PSTPrOZmsEr2P3-;{5xpon- z>Vkn9QePLvqJ$VW_o4xhI%-xTQh=3Ivw}vzD{TYSMSDd8BR?_h;Y@=OX|6t5Gg?_j zt0HH8gZ(sv198EAIWskJikP`KU9aUBn7^n@wN^E0uQ7icaapgSo+jK})E<1l5;YK8 zcPa*36{s=3uL>YA?7isMQUrV30f}H>v9eLHiz=XF%9C6FSP)IO`N~V#P(`s2W}#f0~LeSli#B3rSLIDkuh-U(0h6M~JS&^V^!58RPz*!NxDFfkz-A5WYuPl7eabE*XwrTIf%KC%3{ z)qu91S&1#82>zBiwxdCJ-TlW@`&hF2pW=SkDJMFdra3{95`Ux zPj}A;FlHzA3jZCEHIvk2pbP;IDXU4dz+|LzT7#EO6QyUhi`BO|C1HO9YJ~nu4T}I;cYxFwx5n0LGPNyXa6~MbC_}aRy)C@v0Zv&5 zuQd3I>2}*pUrBnMcDq9V6_<0>8%r)5T|ThoXX$bG-TjBE7`rtwu83tdZw&Pi+rQ1H zHF$d;HTuPM;c(VPIuH9h*HNbV2buwuArL`u9`rqy+wpAzna`0H`NqA&$t*#6@ZvLF z<}NyL?0W&U_HDOc9Wf!;>#gbW=~d|QA-amDSneJ%go_LqP;Vgk*si&VFP>}c4vxGu z;hTHk!+iQOJf6idV}Nr7&m;ix?$cogRoJNzpJ6~Np3fvQ!p}#dB$Fd-GLdSg7VQI-7kI9)&SF%IdqGm=sMIizJT-8=gGKQJk9i-#^QL0lCHE1jKIYQndYp?{7}#jrWZP)l z$NS|>C@AIG*hLidyf8&=UXimt24PX%ReX|K zgw7Ej@$0DyFARICVo-zk%wIYlNo+VXxjC9IeAId1_Rg`~(bS8?orK=WbZ9K)Rb32r zNSwHn>Fc^QL-ek6wh<80!w^=#=YAd~7k~tYvzXIk)JE#0C)NDTxZSKmo<0IIyfHy@ z>ADAjWzj6tG}8{NQ@9Iy_G8+h*nrQky8E;U5Er^7xN2E)$Uf~2*Ao{lk~(BKr4-Z{ zN>YeQcdDv7q@=8?Py5sua^k2eiK=|RIHhc<%MW|M$}@c8?WO*OP7q>@bDZ_g#w}M?c{#06tP86xOpej9$7h__d`>FhUGnxg z`nA;Cy|>SR)8zjMSG&AQ!Wy2O?$u9V`*PFn^2v|}hhZ+-!gG~wBhB;N5l+&#QRJMeX6(7e zS{eoOMD=V^y}r-RqK3Zh9rVlXt}q2`yZe$KIG8~K#1Trn(O@sUj&Fo>t*S&T&I1bkucJIw5ri`Qqtp&=f znYJf#&vQJVBTi*amN7r!&8C@PXLtBjfdqxa2Z~~-KRotMx~70846lD^)&-r zqgpH17swFQ-G-;z4bIp$^w^(Ar62)l9-|x1T0AqHH4vq|(Tr*srQhh?-kPKmW543D z1W6>AI8mRzG4E4M@X3J4rdM4NSsG8RD0^t@e&I;+nWu}*L zL^&r44xcu}M|yEbP)Fm}{kBA!QNv1USQ*`GAo%1-^P`TT;({+~;E~9s{r%}I8g4^h z5V75NOOwWYC?mZ`@!_%j9td8y*+-wx1)s5A>aAm*fn$ADi8)0j1M6!cAHF*Xb_f&A zgBO5jSddlcT)eELmGe-31isqY zk9OeYUyJ3()h$?hUKjO>KSt4~G%+*zGp-&xvZl4gb}N&5?(*Kr`^^!5<+q-?)QP<( z?;r8R?rNkGd+SMtj;tV%63zGyAucNk$$TYh&E0%CB@3uk9=2y}7v)Z>)eiTq9;x$T z2x$!yPHY8_Z0mQi#kmU#RMFTodP_t~@I`QlhI>lgxbeu0LJ{$+nzPZ%*VF1L85omU zMT3%Fe2BLi?%Bnr-?HMIin(s1NXreMC`M zD1sb2s+Dq=4KX0<6HcL@>2Y=~dKa0pWP1T>)J@`U!%y15Mq2Xzw|&5Th=lokhy>^Z z@SJ$VV(LO42$6Eq8$T9@(W5+I=Tp^C_iFUS2e*X-fE38Ba3CtMHg;Rl#yTlZ=~)k> zYuyKPi}Ti&tzpOW;r(^~3jo_@h*r+i5aOcG4`k|9zjbnm)dg+ zvU>@3+BCy@-JBNABcg>XA;_BdAdnmC?EI>Y(ToA8eGi`bF3h2A!g&*KQlcAApel20 zy%4?W#EOhQT`%82Ue5Rwr}o2v9tKL0{^#V;qIvI48ldL-ZDy?)9?k^M(Prj5k83Uf zkY<~7D6v}E$$v0 zaGGUw;4#d)c%aa+bZ^%TbCC${js5IfpFI3-;T@FUA2NQt`=eg~{(m^PmUstZF92Kr zzO9=v0?-}-Xa~;BztcV6`k~h&u=B5so?HD=gZ2>q8-p)TzkB)PS10$kil`UiincTEfWFM@E{k1#>_Z> zKWm*OZeuhEl|InFTzkB>LVglR$NdNlZWM~iZKl#G*J0{n)c>c^j+q$xU zN#Fg4WiadyTxic9N80h}Wo_35-9LG8be(Y}|B-v}r?x?RJpNSgpPB~k5&9GL?2j!I zSojln^2)_)&g}O5H+S9R8sSQ-edNEX7l7nHxIL9*#(D`GW|D^H%G}a8D#DHb);NXCeYq?bnHW3O|QFM6) zJ)3KZRoI4%05TiYgD&()?o05%oKFiI*2^)nNF=coal{p&PbV$L(}LdT?n67%_rO3& zYvd=loGf%vkr6uVRrVk=_PxEhWj5y~#-~#)smTtHdX>x6s6^#?oc&&+Po>&3YJZ`v z>~N2%2y<(uge3aoem4AyQ}(qktVm_oL1ot!F$qnVIAq~iqjTqYs9~xBJvap!=>REA zrrnKB;Dpj{PNrjI6CHAk<#r4=P=iX~v%tB>T>}?Zjy>UoBm89yLI;vQu+6~SyT;GL z%2c}_o9qKRogOCNS{h#rj#FMN%I ztA=NWlP4tq)OGA+``pTK3%Pb%#h%TA49U-ty^{f+4FF)BPBe(!pDrZ>z_sxe+xqT@vvlzUSrBWm$|vb1L~!@DUA`9hMEx{V#RS(da7 zxu<$S`2hgGA-0ifd;D}tHxD`IpmM|H4pH5KiN{pcYZ2YODb8ZlkOA>t?x_h->Rl%b zS!67ii3-`;&#oW0+9Ji}p3qaEAEMmfSJa)&JG_&3Cgp--%lbW!T>5&?)RrOt-$c}h?Tb{_4xY#Ew zsG@qIdYB(4Q38A^KQt^=_;L5zVYgxXZ@1Kceje~8$zNudmGlu91O~Jm*1-b7gbw?V z2!tU1h{TUj05Jd*Y@+fu-_xY8QorY5SRa$m!SXOaKQqAS-$T;O-Kd#Om%p@~lfzR$ zrPlGt4Lh_q8CyRzY&n5z=(Lx=-fd`i7>!z6y5<>SAVY;)_U?l^S>NY_xaTU?`P3vc zIAH&I*Iobs4`B}ADY5gOS`ur#0H^-l$N3Dh5^z=W8V_6oxBg0$L^&&?saQ1v^#-@3qMv?S$$QGp?hKn8nH)}KQeGsp8r3h?KAi>(5FsLZ4To{-J|Z^H`=X&Q z;c))dc(e*bKnWZM8l@U6@E{B?@~vEe3chq^sT;A3G?- zj?V*4bz5P*0LTvR8?avhEPMA>gdGpv4TK$!+*i0804NRTW2l8vUb7xD%i0a5@5Stp z`e?2AM|Hfom{T0=va&l1UK^WC%%`I#S=PMcuOSv6+qT>%pJ!qesF-}AHv=lS+{Jj~ z7P<~wy6{yOD^p2t@@G_8`aW{E)|QG^SN&Ee5HV7l-nsEX<6zJ^c;8M^?y1Y?3AT6d zVrq;9r71I-V<`V>FrJ8cN68$eMcWSh;;d+wO;_R zThk~Xg_1Q5VWPaT9G+Rx0CxpeY0pGqaMPdh^pR@^eSEMsa2Sz-TnY@4>Uy2lxP`4D z?dXUM$eyKkRz^S1IugH?WwUU1;anZDR+U&+wV%=p!&geuX$R~cgq45p0B@JxnY)IA zEpq{Qb54-Pa@v^D?cfYaG>Q^_(rH>1vjY*KF9gzW=1}mA4+MSbIgEJUl=vVnf^0|_ zDdV0mUI0=Dw|q^Y-~ICe4|+%Fu`qqBn$fv zT8P%_O(`{iggJ_2$-2}ZXUM84u)RW-n5L>aCa|-_*(g$qNhIUEmje4Tw+kT0C?c#G zvy68+rObUUqrsW{P#Kr;MKW;NpI3yO8;pS|5vlkl3D5EU0X*7~)BD2(?BgIhiA8&l zxEHHtgKl=-g0fMh^Ys@1=Dn2g=C8~fA^hSQVXOwVMmOWiuxK{m`iGS8apk3|!sfmGwj}sP zDBr9C6CH{V2X&@`ty1H3lB59ftHS8qdw#A>H^nNKOVb{Ztc9^n(iOj^^5f_Y3bkv)v6)9lVD0r|_)-yaa78 z$_#dis&7xZk=q9&I6KZha<5pOavmJSKi%)qacN_Y2LeOyeescudf zbiA?U?9YevwaV6alO`7#uf&JH@b<%DLPe6rjl!-oab%9t%=lyEcq?#~;kTq6xl%G$ ztI;?#=dDpfIZca+kFaHKv}`5)#Imv=4R6{t?Ks8VvT$Cl)a5}4>4_=%^hrzZzG%%s zn5#*szzKAW*KT71WwnK4sz(MY*lsm(lX{MiN{}EVh0n(_c9ul1dU2vhFg7ibyzov( zho#O_FCXc|MkyT@we`eOCnV};HM*HpJW`(~;vblZ(w@=K(xlXej3&|}Oj^EHx-pC} z$rvL>7;((=WG@xFZvRd7I3sx#CCyv~1>YdLSAEQTtGHmy#fHj0aa34I>p0&Su!C0+85{WLy+Jo1B+OZ{e4av-ReR@3YJ(;M zz0oD*q-h0aQ%LCsemlwC7U9!HY9&v7d!xB3V0c!qbN;EYuW~0zJsO87MJRG;j4~GM z!wW!fuN8s@X8gnZxG0luQLfB#lm!J9+Bi;JeRH{w`bpQ(?Txe9Dw6}PVgGs(%`b)e m>aIBzX~|65y0*1u%UVegt+JNI)Z4`imH;jI + + + + + + + + diff --git a/steps/02.01-pages-layout/src/assets/svg/logoDark.svg b/steps/02.01-pages-layout/src/assets/svg/logoDark.svg new file mode 100644 index 0000000..06eb6ed --- /dev/null +++ b/steps/02.01-pages-layout/src/assets/svg/logoDark.svg @@ -0,0 +1,28 @@ + + + + + + + + + diff --git a/steps/02.01-pages-layout/src/components/Alert.tsx b/steps/02.01-pages-layout/src/components/Alert.tsx new file mode 100644 index 0000000..1c90895 --- /dev/null +++ b/steps/02.01-pages-layout/src/components/Alert.tsx @@ -0,0 +1,17 @@ +import clsx from 'clsx'; + +type AlertProps = { + children: React.ReactNode; + className?: string; +}; + +const Alert: React.FC = ({ children, className }) => ( +
+ {children} +
+); + +export default Alert; diff --git a/steps/02.01-pages-layout/src/components/Button.tsx b/steps/02.01-pages-layout/src/components/Button.tsx new file mode 100644 index 0000000..2cee623 --- /dev/null +++ b/steps/02.01-pages-layout/src/components/Button.tsx @@ -0,0 +1,34 @@ +import clsx from 'clsx'; + +export type ButtonProps = { + children: React.ReactNode; + className?: string; + variant?: 'primary' | 'secondary'; + component?: C; +} & Omit, 'className' | 'variant'>; + +const classNames = { + primary: 'inline-block text-white bg-blue-700 hover:bg-blue-800 font-medium rounded-lg text-sm px-5 py-2.5', + secondary: [ + 'inline-block py-2.5 px-5 text-sm font-medium text-slate-900 bg-white rounded-lg border border-gray-200', + 'hover:bg-gray-100 hover:text-blue-700', + 'dark:bg-slate-900 dark:text-white dark:hover:bg-slate-950 dark:hover:text-blue-200 dark:hover:border-blue-200', + ].join(' '), +}; + +const Button = ({ + children, + className, + variant = 'secondary', + component, + ...restProps +}: ButtonProps) => { + const Component = component || 'button'; + return ( + + {children} + + ); +}; + +export default Button; diff --git a/steps/02.01-pages-layout/src/components/EmployeeForm.tsx b/steps/02.01-pages-layout/src/components/EmployeeForm.tsx new file mode 100644 index 0000000..dc15055 --- /dev/null +++ b/steps/02.01-pages-layout/src/components/EmployeeForm.tsx @@ -0,0 +1,113 @@ +'use client'; + +import Image from 'next/image'; +import TextField from '@/components/TextField'; +import { Person } from '@/types'; +import { useFormState } from 'react-dom'; + +import placeholderImage from '@/assets/images/profile-placeholder.jpg'; +import Button from './Button'; + +type ActionState = { + validationErrors?: { [key: string]: Array }; +}; + +type Action = (id: string, formData: FormData) => Promise; + +type EmployeeFormProps = { + employee?: Person; + action?: Action; + className?: string; +}; + +const initialState = { + validationErrors: {}, +} as ActionState; + +const EmployeeForm: React.FC = ({ employee, action, className }) => { + // @ts-ignore + const [state, formAction] = useFormState(action, initialState as unknown as void); + + return ( +
+
+ {employee +
+
+
+ + + + + +
+
+ + + +
+
+
+ +
+
+ ); +}; + +export default EmployeeForm; diff --git a/steps/02.01-pages-layout/src/components/ExpensesDetails.tsx b/steps/02.01-pages-layout/src/components/ExpensesDetails.tsx new file mode 100644 index 0000000..ac2e824 --- /dev/null +++ b/steps/02.01-pages-layout/src/components/ExpensesDetails.tsx @@ -0,0 +1,54 @@ +import { Expense } from '@/types'; +import Paper from './Paper'; + +type ExpenseDetailsRowProps = { + label: string; + value: string; +}; + +const ExpenseDetailsRow: React.FC = ({ label, value }) => ( +
+ {label} + {value} +
+); + +type ExpenseDetailsProps = { + expense: Expense; +}; + +const ExpenseDetails: React.FC = ({ expense }) => ( + <> +
+
+

Information

+ + + + + +
+
+

Workflow

+ + + + + +
+
+
+
+

Amount

+ + + +
+
+ +); + +export default ExpenseDetails; diff --git a/steps/02.01-pages-layout/src/components/ExpensesTable.tsx b/steps/02.01-pages-layout/src/components/ExpensesTable.tsx new file mode 100644 index 0000000..1b2e240 --- /dev/null +++ b/steps/02.01-pages-layout/src/components/ExpensesTable.tsx @@ -0,0 +1,61 @@ +'use client'; + +import { Expense } from '@/types'; +import clsx from 'clsx'; +import { useRouter } from 'next/navigation'; + +type ExpensesTableProps = { + expenses: Array; +}; + +const ExpensesTable: React.FC = ({ expenses }) => { + const router = useRouter(); + + const handleClick = (expenseId: string) => () => { + router.push(`/expenses/${expenseId}`); + }; + + return ( + + + + + + + + + + + {expenses.map((expense, index) => ( + + + + + + + ))} + +
+ Label + + Creation date + + Category + + Price +
{expense.label}{new Date(expense.creationDate).toLocaleDateString()}{expense.category} + {expense.price.priceIncludingTax} {expense.price.currency} +
+ ); +}; + +export default ExpensesTable; diff --git a/steps/02.01-pages-layout/src/components/Icons/ArrowLeft.tsx b/steps/02.01-pages-layout/src/components/Icons/ArrowLeft.tsx new file mode 100644 index 0000000..1cc10c7 --- /dev/null +++ b/steps/02.01-pages-layout/src/components/Icons/ArrowLeft.tsx @@ -0,0 +1,25 @@ +type ArrowLeftProps = { + className?: string; +}; + +const ArrowLeft: React.FC = ({ className }) => ( + +); + +export default ArrowLeft; diff --git a/steps/02.01-pages-layout/src/components/Icons/Eye.tsx b/steps/02.01-pages-layout/src/components/Icons/Eye.tsx new file mode 100644 index 0000000..04beb91 --- /dev/null +++ b/steps/02.01-pages-layout/src/components/Icons/Eye.tsx @@ -0,0 +1,11 @@ +type EyeProps = { + className?: string; +}; + +const Eye: React.FC = ({ className }) => ( + + + +); + +export default Eye; diff --git a/steps/02.01-pages-layout/src/components/Icons/Loader.tsx b/steps/02.01-pages-layout/src/components/Icons/Loader.tsx new file mode 100644 index 0000000..9c81994 --- /dev/null +++ b/steps/02.01-pages-layout/src/components/Icons/Loader.tsx @@ -0,0 +1,25 @@ +type LoaderProps = { + className?: string; +}; + +const Loader: React.FC = ({ className }) => ( + +); + +export default Loader; diff --git a/steps/02.01-pages-layout/src/components/NavigationMenu.tsx b/steps/02.01-pages-layout/src/components/NavigationMenu.tsx new file mode 100644 index 0000000..e3bf528 --- /dev/null +++ b/steps/02.01-pages-layout/src/components/NavigationMenu.tsx @@ -0,0 +1,25 @@ +const NavigationMenu = () => { + return ( + + ); +}; + +export default NavigationMenu; diff --git a/steps/02.01-pages-layout/src/components/PageTitle.tsx b/steps/02.01-pages-layout/src/components/PageTitle.tsx new file mode 100644 index 0000000..217fe69 --- /dev/null +++ b/steps/02.01-pages-layout/src/components/PageTitle.tsx @@ -0,0 +1,25 @@ +import Link from 'next/link'; + +import ArrowLeft from './Icons/ArrowLeft'; + +type PageTitleProps = { + children: React.ReactNode; + backHref?: string; +}; + +const PageTitle: React.FC = ({ children, backHref }) => ( +
+ {backHref && ( + + + Go back + + )} +

{children}

+
+); + +export default PageTitle; diff --git a/steps/02.01-pages-layout/src/components/Paper.tsx b/steps/02.01-pages-layout/src/components/Paper.tsx new file mode 100644 index 0000000..8ba656e --- /dev/null +++ b/steps/02.01-pages-layout/src/components/Paper.tsx @@ -0,0 +1,14 @@ +import clsx from 'clsx'; + +type PaperProps = React.HTMLAttributes & { + children: React.ReactNode; + rounded?: boolean; +}; + +const Paper: React.FC = ({ children, rounded = true, ...restProps }) => ( +
+ {children} +
+); + +export default Paper; diff --git a/steps/02.01-pages-layout/src/components/PersonCard.tsx b/steps/02.01-pages-layout/src/components/PersonCard.tsx new file mode 100644 index 0000000..b38ee53 --- /dev/null +++ b/steps/02.01-pages-layout/src/components/PersonCard.tsx @@ -0,0 +1,43 @@ +import Image from 'next/image'; + +import { Person } from '@/types'; + +import placeholderImage from '@/assets/images/profile-placeholder.jpg'; + +type PersonCardProps = React.HTMLAttributes & { + person: Person; + actions?: React.ReactNode; + compact?: boolean; +}; + +const PersonCard: React.FC = ({ person, actions, className, compact = false }) => { + return ( +
+
+ {`Picture + + {person.firstname} {person.lastname} + + {person.position} +
+ + {!compact && ( +
+ {person.phone} + {person.email} + {person.manager && {person.manager}} +
+ )} + + {actions &&
{actions}
} +
+ ); +}; + +export default PersonCard; diff --git a/steps/02.01-pages-layout/src/components/TextField.tsx b/steps/02.01-pages-layout/src/components/TextField.tsx new file mode 100644 index 0000000..1a008b8 --- /dev/null +++ b/steps/02.01-pages-layout/src/components/TextField.tsx @@ -0,0 +1,34 @@ +import clsx from 'clsx'; + +type TextFieldProps = React.InputHTMLAttributes & { + label?: string; + id: string; + type?: string; + className?: string; + errorMessages?: Array; +}; + +const TextField: React.FC = ({ label, id, type = 'text', className, errorMessages, ...restProps }) => { + return ( +
+ {label && ( + + )} + + + {errorMessages?.length &&

{errorMessages[0]}

} +
+ ); +}; + +export default TextField; diff --git a/steps/02.01-pages-layout/src/components/layouts/AuthLayout.tsx b/steps/02.01-pages-layout/src/components/layouts/AuthLayout.tsx new file mode 100644 index 0000000..cac31a7 --- /dev/null +++ b/steps/02.01-pages-layout/src/components/layouts/AuthLayout.tsx @@ -0,0 +1,12 @@ +type AuthLayoutProps = { + children: React.ReactNode; +}; + +const AuthLayout: React.FC = ({ children }) => ( +
+
+
{children}
+
+); + +export default AuthLayout; diff --git a/steps/02.01-pages-layout/src/components/layouts/DashboardLayout.tsx b/steps/02.01-pages-layout/src/components/layouts/DashboardLayout.tsx new file mode 100644 index 0000000..7970963 --- /dev/null +++ b/steps/02.01-pages-layout/src/components/layouts/DashboardLayout.tsx @@ -0,0 +1,24 @@ +import Link from 'next/link'; +import Image from 'next/image'; + +import NavigationMenu from '@/components/NavigationMenu'; + +import logo from '@/assets/svg/logo.svg'; + +type DashboardLayoutProps = { children: React.ReactNode }; + +const DashboardLayout: React.FC = async ({ children }) => { + return ( +
+
+ + People logo + + +
+
{children}
+
+ ); +}; + +export default DashboardLayout; diff --git a/steps/02.01-pages-layout/src/styles/global.css b/steps/02.01-pages-layout/src/styles/global.css new file mode 100644 index 0000000..f77ed90 --- /dev/null +++ b/steps/02.01-pages-layout/src/styles/global.css @@ -0,0 +1,41 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --color-bg-global: #e9effc; + --color-bg-primary: #ffffff; + --color-bg-secondary: #f5f5f5; + --color-text-primary: #000000; + + --spacing-sm: 0.5rem; + --spacing-md: 0.75rem; + --spacing-lg: 1rem; + --spacing-xl: 1.5rem; +} + +/* Headings */ + +.heading1 { + font-size: 2rem; + font-weight: bold; + margin-bottom: var(--spacing-lg); +} + +.heading2 { + font-size: 1.5rem; + font-weight: bold; + margin-bottom: var(--spacing-lg); +} + +.heading3 { + font-size: 1.125rem; + font-weight: bold; + margin-bottom: var(--spacing-lg); +} + +.heading4 { + font-size: 1rem; + font-weight: bold; + margin-bottom: var(--spacing-lg); +} diff --git a/steps/02.01-pages-layout/src/types.ts b/steps/02.01-pages-layout/src/types.ts new file mode 100644 index 0000000..82cffb5 --- /dev/null +++ b/steps/02.01-pages-layout/src/types.ts @@ -0,0 +1,39 @@ +export type Person = { + id: string; + photo?: string; + firstname: string; + lastname: string; + position: string; + entryDate: string; + birthDate: string; + gender: string; + email: string; + phone: string; + isManager: boolean; + manager?: string; + managerId?: string; +}; + +export type Expense = { + id: string; + employeeId: string; + price: { + priceIncludingTax: number; + taxAmount: number; + priceExcludingTax: number; + currency: string; + }; + label: string; + description: string; + category: string; + receiptLink: string; + status: 'approved' | 'created' | 'declined'; + creationDate: string; + updateDate: string; +}; + +export type PaginationAttributes = { + per_page?: number; + page: number; + total_pages: number; +}; diff --git a/steps/02.01-pages-layout/tailwind.config.js b/steps/02.01-pages-layout/tailwind.config.js new file mode 100644 index 0000000..eaa361c --- /dev/null +++ b/steps/02.01-pages-layout/tailwind.config.js @@ -0,0 +1,9 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ['./src/**/*.{js,ts,jsx,tsx,mdx}'], + darkMode: 'selector', + theme: { + extend: {}, + }, + plugins: [], +}; diff --git a/steps/02.01-pages-layout/tsconfig.json b/steps/02.01-pages-layout/tsconfig.json new file mode 100644 index 0000000..7b28589 --- /dev/null +++ b/steps/02.01-pages-layout/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/steps/02.02-navigation-solution/.env.example b/steps/02.02-navigation-solution/.env.example new file mode 100644 index 0000000..1ebabff --- /dev/null +++ b/steps/02.02-navigation-solution/.env.example @@ -0,0 +1,2 @@ +API_BASE_URL=http://localhost:3001 +API_KEY=XXXX diff --git a/steps/02.02-navigation-solution/.eslintrc.json b/steps/02.02-navigation-solution/.eslintrc.json new file mode 100644 index 0000000..bffb357 --- /dev/null +++ b/steps/02.02-navigation-solution/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/steps/02.02-navigation-solution/.gitignore b/steps/02.02-navigation-solution/.gitignore new file mode 100644 index 0000000..fd3dbb5 --- /dev/null +++ b/steps/02.02-navigation-solution/.gitignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/steps/02.02-navigation-solution/README.md b/steps/02.02-navigation-solution/README.md new file mode 100644 index 0000000..053312b --- /dev/null +++ b/steps/02.02-navigation-solution/README.md @@ -0,0 +1 @@ +# 02.02 - Navigation diff --git a/steps/02.02-navigation-solution/next.config.mjs b/steps/02.02-navigation-solution/next.config.mjs new file mode 100644 index 0000000..16343f6 --- /dev/null +++ b/steps/02.02-navigation-solution/next.config.mjs @@ -0,0 +1,15 @@ +const apiUrl = new URL(process.env.API_BASE_URL || 'http://localhost:3001'); + +/** @type {import('next').NextConfig} */ +const nextConfig = { + images: { + remotePatterns: [ + { + hostname: apiUrl.hostname, + port: apiUrl.port, + }, + ], + }, +}; + +export default nextConfig; diff --git a/steps/02.02-navigation-solution/package.json b/steps/02.02-navigation-solution/package.json new file mode 100644 index 0000000..a0b2b1d --- /dev/null +++ b/steps/02.02-navigation-solution/package.json @@ -0,0 +1,37 @@ +{ + "name": "02.02-solution", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "clsx": "^2.1.1", + "jose": "^5.6.3", + "jsonwebtoken": "^9.0.2", + "next": "14.2.5", + "react": "^18", + "react-dom": "^18", + "react-error-boundary": "^4.0.13", + "rehype-sanitize": "^6.0.0", + "rehype-stringify": "^10.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.0", + "server-only": "^0.0.1", + "showdown": "^2.1.0", + "unified": "^11.0.5" + }, + "devDependencies": { + "@types/jsonwebtoken": "^9.0.6", + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "@types/showdown": "^2.0.6", + "eslint": "^8", + "eslint-config-next": "14.2.5", + "typescript": "^5" + } +} diff --git a/steps/02.02-navigation-solution/postcss.config.js b/steps/02.02-navigation-solution/postcss.config.js new file mode 100644 index 0000000..33ad091 --- /dev/null +++ b/steps/02.02-navigation-solution/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/steps/02.02-navigation-solution/public/next.svg b/steps/02.02-navigation-solution/public/next.svg new file mode 100644 index 0000000..5174b28 --- /dev/null +++ b/steps/02.02-navigation-solution/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/steps/02.02-navigation-solution/public/portraits/men/30.jpg b/steps/02.02-navigation-solution/public/portraits/men/30.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d04b7a2669245212620be0cbb17922fd1cb3cbfe GIT binary patch literal 4349 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 w3$0jQtjS=6rPW;hG!!51^p@HCR)|Jncu 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 literal 0 HcmV?d00001 diff --git a/steps/02.02-navigation-solution/public/portraits/men/78.jpg b/steps/02.02-navigation-solution/public/portraits/men/78.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6438e80b9b56fcf7e6e0e9a02eb8cc54a53c91aa GIT binary patch literal 4643 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 Y-IZTg@->$i4XtycyzDV!v4pAr0qF|a9RL6T literal 0 HcmV?d00001 diff --git a/steps/02.02-navigation-solution/public/portraits/men/86.jpg b/steps/02.02-navigation-solution/public/portraits/men/86.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9358491105b401a359ef698035ab9b05b84e0457 GIT binary patch literal 5433 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 Kdoq>(yYN42Z*!dh literal 0 HcmV?d00001 diff --git a/steps/02.02-navigation-solution/public/portraits/women/65.jpg b/steps/02.02-navigation-solution/public/portraits/women/65.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3cab57987a6ff10c5639fad656fb0fc77ba569c9 GIT binary patch literal 5972 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 C^!eWa literal 0 HcmV?d00001 diff --git a/steps/02.02-navigation-solution/public/portraits/women/85.jpg b/steps/02.02-navigation-solution/public/portraits/women/85.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0a900f9e8874eddf5978f08e6de03815f23bb1ea GIT binary patch literal 3912 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 literal 0 HcmV?d00001 diff --git a/steps/02.02-navigation-solution/public/portraits/women/93.jpg b/steps/02.02-navigation-solution/public/portraits/women/93.jpg new file mode 100644 index 0000000000000000000000000000000000000000..81ea0613898f679ad77f8e4563a93be893f38a07 GIT binary patch literal 4871 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 \ No newline at end of file diff --git a/steps/02.02-navigation-solution/src/app/(auth)/layout.tsx b/steps/02.02-navigation-solution/src/app/(auth)/layout.tsx new file mode 100644 index 0000000..cac31a7 --- /dev/null +++ b/steps/02.02-navigation-solution/src/app/(auth)/layout.tsx @@ -0,0 +1,12 @@ +type AuthLayoutProps = { + children: React.ReactNode; +}; + +const AuthLayout: React.FC = ({ children }) => ( +
+
+
{children}
+
+); + +export default AuthLayout; diff --git a/steps/02.02-navigation-solution/src/app/(auth)/login/page.tsx b/steps/02.02-navigation-solution/src/app/(auth)/login/page.tsx new file mode 100644 index 0000000..3ae0596 --- /dev/null +++ b/steps/02.02-navigation-solution/src/app/(auth)/login/page.tsx @@ -0,0 +1,30 @@ +import { Metadata } from 'next'; + +import TextField from '@/components/TextField'; +import Button from '@/components/Button'; + +export const metadata: Metadata = { + title: 'SFEIR People | Login', +}; + +const LoginPage = () => { + return ( +
+

Welcome !

+ + + + + ); +}; + +export default LoginPage; diff --git a/steps/02.02-navigation-solution/src/app/(dashboard)/employees/[id]/edit/page.tsx b/steps/02.02-navigation-solution/src/app/(dashboard)/employees/[id]/edit/page.tsx new file mode 100644 index 0000000..9d29dc0 --- /dev/null +++ b/steps/02.02-navigation-solution/src/app/(dashboard)/employees/[id]/edit/page.tsx @@ -0,0 +1,24 @@ +import EmployeeForm from '@/components/EmployeeForm'; +import PageTitle from '@/components/PageTitle'; + +import employeesData from '@/data/employees.json'; + +const EmployeeDetail = async ({ params }: { params: { id: string } }) => { + const employee = employeesData.find((employee) => employee.id === params.id); + + if (!employee) return Single Employee - Not found; + + return ( + <> + + Single Employee - {employee.firstname} {employee.lastname} | Edit + + +
+ +
+ + ); +}; + +export default EmployeeDetail; diff --git a/steps/02.02-navigation-solution/src/app/(dashboard)/employees/[id]/page.tsx b/steps/02.02-navigation-solution/src/app/(dashboard)/employees/[id]/page.tsx new file mode 100644 index 0000000..a498c63 --- /dev/null +++ b/steps/02.02-navigation-solution/src/app/(dashboard)/employees/[id]/page.tsx @@ -0,0 +1,21 @@ +import PageTitle from '@/components/PageTitle'; +import PersonCard from '@/components/PersonCard'; + +import employeesData from '@/data/employees.json'; + +const EmployeeDetail = async ({ params }: { params: { id: string } }) => { + const employee = employeesData.find((employee) => employee.id === params.id); + + if (!employee) return Single Employee - Not found; + + return ( + <> + + Single Employee - {employee.firstname} {employee.lastname} + + + + ); +}; + +export default EmployeeDetail; diff --git a/steps/02.02-navigation-solution/src/app/(dashboard)/employees/new/page.tsx b/steps/02.02-navigation-solution/src/app/(dashboard)/employees/new/page.tsx new file mode 100644 index 0000000..f1e5157 --- /dev/null +++ b/steps/02.02-navigation-solution/src/app/(dashboard)/employees/new/page.tsx @@ -0,0 +1,18 @@ +import EmployeeForm from '@/components/EmployeeForm'; +import PageTitle from '@/components/PageTitle'; + +const EmployeeDetail = async () => { + return ( + <> + + Employees | Create + + +
+ +
+ + ); +}; + +export default EmployeeDetail; diff --git a/steps/02.02-navigation-solution/src/app/(dashboard)/employees/page.tsx b/steps/02.02-navigation-solution/src/app/(dashboard)/employees/page.tsx new file mode 100644 index 0000000..2a356a7 --- /dev/null +++ b/steps/02.02-navigation-solution/src/app/(dashboard)/employees/page.tsx @@ -0,0 +1,47 @@ +import Link from 'next/link'; + +import Button from '@/components/Button'; +import PageTitle from '@/components/PageTitle'; +import PersonCard from '@/components/PersonCard'; +import Search from '@/components/Search'; + +import employeesData from '@/data/employees.json'; + +const Employees = async ({ searchParams }: { searchParams: { search?: string } }) => { + const search = searchParams.search || ''; + const employees = employeesData.filter((employee) => + `${employee.firstname} ${employee.lastname}`.toLowerCase().includes(search.toLowerCase()) + ); + + return ( +
+ Employees +
+ + +
+
+ {employees?.map((employee) => ( + + + +
+ } + /> + ))} +
+ + ); +}; + +export default Employees; diff --git a/steps/02.02-navigation-solution/src/app/(dashboard)/expenses/[id]/page.tsx b/steps/02.02-navigation-solution/src/app/(dashboard)/expenses/[id]/page.tsx new file mode 100644 index 0000000..bda58e2 --- /dev/null +++ b/steps/02.02-navigation-solution/src/app/(dashboard)/expenses/[id]/page.tsx @@ -0,0 +1,18 @@ +import ExpenseDetails from '@/components/ExpensesDetails'; +import PageTitle from '@/components/PageTitle'; + +import expensesData from '@/data/expenses.json'; +import { Expense } from '@/types'; + +const SingleExpense = ({ params }: { params: { id: string } }) => { + const expense = expensesData.find((expense) => expense.id === params.id); + + return ( + <> + Single Expense - {expense?.label || 'Not found'} + {expense && } + + ); +}; + +export default SingleExpense; diff --git a/steps/02.02-navigation-solution/src/app/(dashboard)/expenses/page.tsx b/steps/02.02-navigation-solution/src/app/(dashboard)/expenses/page.tsx new file mode 100644 index 0000000..55d1d65 --- /dev/null +++ b/steps/02.02-navigation-solution/src/app/(dashboard)/expenses/page.tsx @@ -0,0 +1,17 @@ +import ExpensesTable from '@/components/ExpensesTable'; +import PageTitle from '@/components/PageTitle'; + +import { Expense } from '@/types'; + +import expensesData from '@/data/expenses.json'; + +const Expenses = async () => { + return ( + <> + Expenses + } /> + + ); +}; + +export default Expenses; diff --git a/steps/02.02-navigation-solution/src/app/(dashboard)/layout.tsx b/steps/02.02-navigation-solution/src/app/(dashboard)/layout.tsx new file mode 100644 index 0000000..07b02c8 --- /dev/null +++ b/steps/02.02-navigation-solution/src/app/(dashboard)/layout.tsx @@ -0,0 +1,29 @@ +import { Metadata } from 'next'; +import Link from 'next/link'; +import Image from 'next/image'; + +import NavigationMenu from '@/components/NavigationMenu'; + +import logo from '@/assets/svg/logo.svg'; + +type DashboardLayoutProps = { children: React.ReactNode }; + +export const metadata: Metadata = { + title: 'SFEIR People | Dashboard', +}; + +const DashboardLayout: React.FC = async ({ children }) => { + return ( +
+
+ + People logo + + +
+
{children}
+
+ ); +}; + +export default DashboardLayout; diff --git a/steps/02.02-navigation-solution/src/app/(dashboard)/page.tsx b/steps/02.02-navigation-solution/src/app/(dashboard)/page.tsx new file mode 100644 index 0000000..f581ebb --- /dev/null +++ b/steps/02.02-navigation-solution/src/app/(dashboard)/page.tsx @@ -0,0 +1,11 @@ +import PageTitle from '@/components/PageTitle'; + +const HomePage = () => { + return ( + <> + SFEIR People + + ); +}; + +export default HomePage; diff --git a/steps/02.02-navigation-solution/src/app/layout.tsx b/steps/02.02-navigation-solution/src/app/layout.tsx new file mode 100644 index 0000000..e7d90e9 --- /dev/null +++ b/steps/02.02-navigation-solution/src/app/layout.tsx @@ -0,0 +1,21 @@ +import type { Metadata } from 'next'; +import { Inter } from 'next/font/google'; + +const inter = Inter({ subsets: ['latin'] }); + +import '@/styles/global.css'; + +export const metadata: Metadata = { + title: 'SFEIR People', + description: 'SFEIR People dashboard application', +}; + +const RootLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => { + return ( + + {children} + + ); +}; + +export default RootLayout; diff --git a/steps/02.02-navigation-solution/src/assets/images/profile-placeholder.jpg b/steps/02.02-navigation-solution/src/assets/images/profile-placeholder.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6fa00ea6c9e371e542006bb4bd69ae5e6922a324 GIT binary patch literal 11940 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 m>aIBzX~|65y0*1u%UVegt+JNI)Z4`imH;jI + + + + + + + + diff --git a/steps/02.02-navigation-solution/src/assets/svg/logoDark.svg b/steps/02.02-navigation-solution/src/assets/svg/logoDark.svg new file mode 100644 index 0000000..06eb6ed --- /dev/null +++ b/steps/02.02-navigation-solution/src/assets/svg/logoDark.svg @@ -0,0 +1,28 @@ + + + + + + + + + diff --git a/steps/02.02-navigation-solution/src/components/Alert.tsx b/steps/02.02-navigation-solution/src/components/Alert.tsx new file mode 100644 index 0000000..1c90895 --- /dev/null +++ b/steps/02.02-navigation-solution/src/components/Alert.tsx @@ -0,0 +1,17 @@ +import clsx from 'clsx'; + +type AlertProps = { + children: React.ReactNode; + className?: string; +}; + +const Alert: React.FC = ({ children, className }) => ( +
+ {children} +
+); + +export default Alert; diff --git a/steps/02.02-navigation-solution/src/components/Button.tsx b/steps/02.02-navigation-solution/src/components/Button.tsx new file mode 100644 index 0000000..2cee623 --- /dev/null +++ b/steps/02.02-navigation-solution/src/components/Button.tsx @@ -0,0 +1,34 @@ +import clsx from 'clsx'; + +export type ButtonProps = { + children: React.ReactNode; + className?: string; + variant?: 'primary' | 'secondary'; + component?: C; +} & Omit, 'className' | 'variant'>; + +const classNames = { + primary: 'inline-block text-white bg-blue-700 hover:bg-blue-800 font-medium rounded-lg text-sm px-5 py-2.5', + secondary: [ + 'inline-block py-2.5 px-5 text-sm font-medium text-slate-900 bg-white rounded-lg border border-gray-200', + 'hover:bg-gray-100 hover:text-blue-700', + 'dark:bg-slate-900 dark:text-white dark:hover:bg-slate-950 dark:hover:text-blue-200 dark:hover:border-blue-200', + ].join(' '), +}; + +const Button = ({ + children, + className, + variant = 'secondary', + component, + ...restProps +}: ButtonProps) => { + const Component = component || 'button'; + return ( + + {children} + + ); +}; + +export default Button; diff --git a/steps/02.02-navigation-solution/src/components/EmployeeForm.tsx b/steps/02.02-navigation-solution/src/components/EmployeeForm.tsx new file mode 100644 index 0000000..dc15055 --- /dev/null +++ b/steps/02.02-navigation-solution/src/components/EmployeeForm.tsx @@ -0,0 +1,113 @@ +'use client'; + +import Image from 'next/image'; +import TextField from '@/components/TextField'; +import { Person } from '@/types'; +import { useFormState } from 'react-dom'; + +import placeholderImage from '@/assets/images/profile-placeholder.jpg'; +import Button from './Button'; + +type ActionState = { + validationErrors?: { [key: string]: Array }; +}; + +type Action = (id: string, formData: FormData) => Promise; + +type EmployeeFormProps = { + employee?: Person; + action?: Action; + className?: string; +}; + +const initialState = { + validationErrors: {}, +} as ActionState; + +const EmployeeForm: React.FC = ({ employee, action, className }) => { + // @ts-ignore + const [state, formAction] = useFormState(action, initialState as unknown as void); + + return ( +
+
+ {employee +
+
+
+ + + + + +
+
+ + + +
+
+
+ +
+
+ ); +}; + +export default EmployeeForm; diff --git a/steps/02.02-navigation-solution/src/components/ExpensesDetails.tsx b/steps/02.02-navigation-solution/src/components/ExpensesDetails.tsx new file mode 100644 index 0000000..f59b51a --- /dev/null +++ b/steps/02.02-navigation-solution/src/components/ExpensesDetails.tsx @@ -0,0 +1,56 @@ +import { Expense } from '@/types'; +import Paper from './Paper'; + +type ExpenseDetailsRowProps = { + label: string; + value: string; +}; + +const ExpenseDetailsRow: React.FC = ({ label, value }) => ( +
+ {label} + {value} +
+); + +type ExpenseDetailsProps = { + expense: Expense; +}; + +const ExpenseDetails: React.FC = ({ expense }) => ( + <> +
+
+

Information

+ + + + + +
+
+

Workflow

+ + + + + +
+
+
+
+

Amount

+ + + + + +
+
+ +); + +export default ExpenseDetails; diff --git a/steps/02.02-navigation-solution/src/components/ExpensesTable.tsx b/steps/02.02-navigation-solution/src/components/ExpensesTable.tsx new file mode 100644 index 0000000..1b2e240 --- /dev/null +++ b/steps/02.02-navigation-solution/src/components/ExpensesTable.tsx @@ -0,0 +1,61 @@ +'use client'; + +import { Expense } from '@/types'; +import clsx from 'clsx'; +import { useRouter } from 'next/navigation'; + +type ExpensesTableProps = { + expenses: Array; +}; + +const ExpensesTable: React.FC = ({ expenses }) => { + const router = useRouter(); + + const handleClick = (expenseId: string) => () => { + router.push(`/expenses/${expenseId}`); + }; + + return ( + + + + + + + + + + + {expenses.map((expense, index) => ( + + + + + + + ))} + +
+ Label + + Creation date + + Category + + Price +
{expense.label}{new Date(expense.creationDate).toLocaleDateString()}{expense.category} + {expense.price.priceIncludingTax} {expense.price.currency} +
+ ); +}; + +export default ExpensesTable; diff --git a/steps/02.02-navigation-solution/src/components/Icons/ArrowLeft.tsx b/steps/02.02-navigation-solution/src/components/Icons/ArrowLeft.tsx new file mode 100644 index 0000000..1cc10c7 --- /dev/null +++ b/steps/02.02-navigation-solution/src/components/Icons/ArrowLeft.tsx @@ -0,0 +1,25 @@ +type ArrowLeftProps = { + className?: string; +}; + +const ArrowLeft: React.FC = ({ className }) => ( + +); + +export default ArrowLeft; diff --git a/steps/02.02-navigation-solution/src/components/Icons/Eye.tsx b/steps/02.02-navigation-solution/src/components/Icons/Eye.tsx new file mode 100644 index 0000000..04beb91 --- /dev/null +++ b/steps/02.02-navigation-solution/src/components/Icons/Eye.tsx @@ -0,0 +1,11 @@ +type EyeProps = { + className?: string; +}; + +const Eye: React.FC = ({ className }) => ( + + + +); + +export default Eye; diff --git a/steps/02.02-navigation-solution/src/components/Icons/Loader.tsx b/steps/02.02-navigation-solution/src/components/Icons/Loader.tsx new file mode 100644 index 0000000..9c81994 --- /dev/null +++ b/steps/02.02-navigation-solution/src/components/Icons/Loader.tsx @@ -0,0 +1,25 @@ +type LoaderProps = { + className?: string; +}; + +const Loader: React.FC = ({ className }) => ( + +); + +export default Loader; diff --git a/steps/02.02-navigation-solution/src/components/NavigationItem.tsx b/steps/02.02-navigation-solution/src/components/NavigationItem.tsx new file mode 100644 index 0000000..9f68e10 --- /dev/null +++ b/steps/02.02-navigation-solution/src/components/NavigationItem.tsx @@ -0,0 +1,30 @@ +'use client'; + +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; + +import clsx from 'clsx'; + +type NavigationItemsProps = { + href: string; + children: React.ReactNode; +}; + +const NavigationItem: React.FC = ({ href, children }) => { + const pathname = usePathname(); + + return ( + + {children} + + ); +}; + +export default NavigationItem; diff --git a/steps/02.02-navigation-solution/src/components/NavigationMenu.tsx b/steps/02.02-navigation-solution/src/components/NavigationMenu.tsx new file mode 100644 index 0000000..4b778c6 --- /dev/null +++ b/steps/02.02-navigation-solution/src/components/NavigationMenu.tsx @@ -0,0 +1,21 @@ +import NavigationItem from './NavigationItem'; + +const NavigationMenu = () => { + return ( + + ); +}; + +export default NavigationMenu; diff --git a/steps/02.02-navigation-solution/src/components/PageTitle.tsx b/steps/02.02-navigation-solution/src/components/PageTitle.tsx new file mode 100644 index 0000000..217fe69 --- /dev/null +++ b/steps/02.02-navigation-solution/src/components/PageTitle.tsx @@ -0,0 +1,25 @@ +import Link from 'next/link'; + +import ArrowLeft from './Icons/ArrowLeft'; + +type PageTitleProps = { + children: React.ReactNode; + backHref?: string; +}; + +const PageTitle: React.FC = ({ children, backHref }) => ( +
+ {backHref && ( + + + Go back + + )} +

{children}

+
+); + +export default PageTitle; diff --git a/steps/02.02-navigation-solution/src/components/Pagination.tsx b/steps/02.02-navigation-solution/src/components/Pagination.tsx new file mode 100644 index 0000000..be9a43a --- /dev/null +++ b/steps/02.02-navigation-solution/src/components/Pagination.tsx @@ -0,0 +1,95 @@ +'use client'; + +import clsx from 'clsx'; +import Link from 'next/link'; +import { usePathname, useSearchParams } from 'next/navigation'; + +type PaginationProps = { + totalPages: number; + className?: string; +}; + +type PaginationShortcutProps = { + href: string; + disabled?: boolean; + className?: string; + children: React.ReactNode; +}; + +const PaginationShortcut: React.FC = ({ href, disabled, className, children }) => { + const classNames = clsx( + 'block text-center px-3 py-2 ms-0 border bg-white dark:bg-slate-900', + className, + !disabled && + 'hover:bg-gray-100 hover:text-gray-700 text-gray-500 border-gray-300 dark:text-white dark:border-gray-700 dark:hover:bg-slate-950 dark:hover:text-white', + disabled && 'text-gray-300 border-gray-200 dark:text-gray-500 dark:border-gray-600' + ); + + if (disabled) return
{children}
; + + return ( + + {children} + + ); +}; + +const Pagination: React.FC = ({ totalPages, className }) => { + const params = useSearchParams(); + const pathname = usePathname(); + + const currentPage = Number(params.get('page')) || 1; + + const getPageUrl = (page: number): string => { + const newParams = new URLSearchParams(params); + newParams.set('page', page.toString()); + return `${pathname}?${newParams.toString()}`; + }; + + return ( + + ); +}; + +export default Pagination; diff --git a/steps/02.02-navigation-solution/src/components/Paper.tsx b/steps/02.02-navigation-solution/src/components/Paper.tsx new file mode 100644 index 0000000..8ba656e --- /dev/null +++ b/steps/02.02-navigation-solution/src/components/Paper.tsx @@ -0,0 +1,14 @@ +import clsx from 'clsx'; + +type PaperProps = React.HTMLAttributes & { + children: React.ReactNode; + rounded?: boolean; +}; + +const Paper: React.FC = ({ children, rounded = true, ...restProps }) => ( +
+ {children} +
+); + +export default Paper; diff --git a/steps/02.02-navigation-solution/src/components/PersonCard.tsx b/steps/02.02-navigation-solution/src/components/PersonCard.tsx new file mode 100644 index 0000000..b38ee53 --- /dev/null +++ b/steps/02.02-navigation-solution/src/components/PersonCard.tsx @@ -0,0 +1,43 @@ +import Image from 'next/image'; + +import { Person } from '@/types'; + +import placeholderImage from '@/assets/images/profile-placeholder.jpg'; + +type PersonCardProps = React.HTMLAttributes & { + person: Person; + actions?: React.ReactNode; + compact?: boolean; +}; + +const PersonCard: React.FC = ({ person, actions, className, compact = false }) => { + return ( +
+
+ {`Picture + + {person.firstname} {person.lastname} + + {person.position} +
+ + {!compact && ( +
+ {person.phone} + {person.email} + {person.manager && {person.manager}} +
+ )} + + {actions &&
{actions}
} +
+ ); +}; + +export default PersonCard; diff --git a/steps/02.02-navigation-solution/src/components/Search.tsx b/steps/02.02-navigation-solution/src/components/Search.tsx new file mode 100644 index 0000000..98fe032 --- /dev/null +++ b/steps/02.02-navigation-solution/src/components/Search.tsx @@ -0,0 +1,43 @@ +'use client'; + +import { debounce } from '@/functions/timing'; +import clsx from 'clsx'; +import { usePathname, useRouter, useSearchParams } from 'next/navigation'; + +const Search = ({ ...restProps }) => { + const router = useRouter(); + const params = useSearchParams(); + const pathname = usePathname(); + + const handleChange = debounce((event: React.ChangeEvent) => { + const value = event.target?.value; + const newParams = new URLSearchParams(params); + newParams.delete('page'); + if (value) newParams.set('search', value); + else newParams.delete('search'); + router.replace(`${pathname}?${newParams.toString()}`); + }, 200); + + return ( + <> + + + + ); +}; + +export default Search; diff --git a/steps/02.02-navigation-solution/src/components/TextField.tsx b/steps/02.02-navigation-solution/src/components/TextField.tsx new file mode 100644 index 0000000..d8061e9 --- /dev/null +++ b/steps/02.02-navigation-solution/src/components/TextField.tsx @@ -0,0 +1,31 @@ +import clsx from 'clsx'; + +type TextFieldProps = React.InputHTMLAttributes & { + label: string; + id: string; + type?: string; + className?: string; + errorMessages?: Array; +}; + +const TextField: React.FC = ({ label, id, type = 'text', className, errorMessages, ...restProps }) => { + return ( +
+ + + {errorMessages?.length &&

{errorMessages[0]}

} +
+ ); +}; + +export default TextField; diff --git a/steps/02.02-navigation-solution/src/data/employees.json b/steps/02.02-navigation-solution/src/data/employees.json new file mode 100644 index 0000000..5f5342e --- /dev/null +++ b/steps/02.02-navigation-solution/src/data/employees.json @@ -0,0 +1,152 @@ +[ + { + "id": "5763cd4d9d2a4f259b53c901", + "photo": "/portraits/women/85.jpg", + "firstname": "Leanne", + "lastname": "Woodard", + "position": "Developer", + "entryDate": "27/10/2015", + "birthDate": "02/01/1974", + "gender": "f", + "email": "woodard.l@acme.com", + "phone": "0784112248", + "isManager": false, + "manager": "Erika", + "managerId": "5763cd4d3b57c672861bfa1f" + }, + { + "id": "5763cd4d51fdb6588742f99e", + "photo": "/portraits/men/56.jpg", + "firstname": "Castaneda", + "lastname": "Salinas", + "position": "Developer", + "entryDate": "04/10/2015", + "birthDate": "22/01/1963", + "gender": "m", + "email": "salinas.c@acme.com", + "phone": "0145652522", + "isManager": false, + "manager": "Erika", + "managerId": "5763cd4d3b57c672861bfa1f" + }, + { + "id": "5763cd4dba6362a3f92c954e", + "photo": "/portraits/women/24.jpg", + "firstname": "Phyllis", + "lastname": "Donovan", + "position": "Sales", + "entryDate": "30/03/2015", + "birthDate": "30/11/1951", + "gender": "f", + "email": "donovan.p@acme.com", + "phone": "0685230125", + "isManager": false, + "manager": "Erika", + "managerId": "5763cd4d3b57c672861bfa1f" + }, + { + "id": "5763cd4d3b57c672861bfa1f", + "photo": "/portraits/women/65.jpg", + "firstname": "Erika", + "lastname": "Guzman", + "position": "Product Owner", + "entryDate": "13/05/2016", + "birthDate": "19/03/1962", + "gender": "f", + "email": "guzman.e@acme.com", + "phone": "0678412587", + "isManager": true, + "manager": "Mercedes", + "managerId": "5763cd4d979b62a209809160" + }, + { + "id": "5763cd4d5fc36e4f842ca5a9", + "photo": "/portraits/men/30.jpg", + "firstname": "Moody", + "lastname": "Prince", + "position": "Developer", + "entryDate": "28/09/2015", + "birthDate": "15/04/1971", + "gender": "m", + "email": "prince.m@acme.com", + "phone": "0662589632", + "isManager": false, + "manager": "Mercedes", + "managerId": "5763cd4d979b62a209809160" + }, + { + "id": "5763cd4d979b62a209809160", + "photo": "/portraits/women/8.jpg", + "firstname": "Mercedes", + "lastname": "Hebert", + "position": "Product Owner", + "entryDate": "02/01/2016", + "birthDate": "20/07/1947", + "gender": "f", + "email": "hebert.m@acme.com", + "phone": "0125878522", + "isManager": true, + "manager": "Mclaughlin", + "managerId": "5763cd4dfa6f96cd26c65787" + }, + { + "id": "5763cd4d15e6c2c28b70f2e8", + "photo": "/portraits/men/86.jpg", + "firstname": "Howell", + "lastname": "Mcknight", + "position": "Sales", + "entryDate": "26/09/2015", + "birthDate": "18/07/1979", + "gender": "m", + "email": "mcknight.h@acme.com", + "phone": "0456987425", + "isManager": false, + "manager": "Mclaughlin", + "managerId": "5763cd4dfa6f96cd26c65787" + }, + { + "id": "5763cd4d5d6ad8dfc6c34883", + "photo": "/portraits/women/93.jpg", + "firstname": "Lizzie", + "lastname": "Morris", + "position": "Human Resources", + "entryDate": "03/05/2016", + "birthDate": "15/11/1981", + "gender": "f", + "email": "morris.l@acme.com", + "phone": "0662259988", + "isManager": false, + "manager": "Mclaughlin", + "managerId": "5763cd4dfa6f96cd26c65787" + }, + { + "id": "5763cd4dc378a38ecd387737", + "photo": "/portraits/men/34.jpg", + "firstname": "Roy", + "lastname": "Nielsen", + "position": "Sales", + "entryDate": "17/05/2016", + "birthDate": "21/10/1951", + "gender": "m", + "email": "nielsen.r@acme.com", + "phone": "0755669551", + "isManager": false, + "manager": "Mclaughlin", + "managerId": "5763cd4dfa6f96cd26c65787" + }, + { + "id": "5763cd4dfa6f96cd26c65787", + "photo": "/portraits/men/78.jpg", + "firstname": "Mclaughlin", + "lastname": "Cochran", + "position": "Director", + "entryDate": "11/04/2016", + "birthDate": "19/03/1973", + "gender": "m", + "email": "cochran.m@acme.com", + "phone": "0266334856", + "isManager": true, + "manager": "", + "managerId": "" + } +] diff --git a/steps/02.02-navigation-solution/src/data/expenses.json b/steps/02.02-navigation-solution/src/data/expenses.json new file mode 100644 index 0000000..0d096dc --- /dev/null +++ b/steps/02.02-navigation-solution/src/data/expenses.json @@ -0,0 +1,342 @@ +[ + { + "id": "0475830f-a563-44e0-8c5c-6d829c11a132", + "employeeId": "5763cd4d51fdb6588742f99e", + "price": { + "priceIncludingTax": 120.50, + "taxAmount": 20.50, + "priceExcludingTax": 100, + "currency": "EUR" + }, + "label": "Business Lunch", + "description": "Lunch with a client to discuss a new project.", + "category": "Meals", + "receiptLink": "https://example.com/receipt1.pdf", + "status": "approved", + "creationDate": "2024-03-15T09:30:00Z", + "updateDate": "2024-03-18T14:45:00Z" + }, + { + "id": "a2e8b2c4-99d8-4c13-9a9c-0a8a68623260", + "employeeId": "5763cd4d51fdb6588742f99e", + "price": { + "priceIncludingTax": 55.20, + "taxAmount": 7.20, + "priceExcludingTax": 48, + "currency": "USD" + }, + "label": "Office Supplies", + "description": "Purchase of paper, pens, etc.", + "category": "Supplies", + "receiptLink": "https://example.com/receipt2.jpg", + "status": "created", + "creationDate": "2024-05-02T11:20:00Z", + "updateDate": "2024-05-02T11:20:00Z" + }, + { + "id": "3d3fb561-0d9c-4021-8285-d2f49c40c47d", + "employeeId": "5763cd4d51fdb6588742f99e", + "price": { + "priceIncludingTax": 250, + "taxAmount": 0, + "priceExcludingTax": 250, + "currency": "EUR" + }, + "label": "Plane Ticket", + "description": "Business trip to Berlin.", + "category": "Travel", + "receiptLink": "https://example.com/receipt3.png", + "status": "declined", + "creationDate": "2024-04-10T16:45:00Z", + "updateDate": "2024-04-12T09:30:00Z" + }, + { + "id": "4c41689c-e5f5-4029-a181-38c498e7a82b", + "employeeId": "5763cd4d5fc36e4f842ca5a9", + "price": { + "priceIncludingTax": 80.00, + "taxAmount": 13.33, + "priceExcludingTax": 66.67, + "currency": "USD" + }, + "label": "Hotel", + "description": "Hotel night in London.", + "category": "Accommodation", + "receiptLink": "https://example.com/receipt4.pdf", + "status": "approved", + "creationDate": "2024-06-20T08:15:00Z", + "updateDate": "2024-06-22T10:30:00Z" + }, + { + "id": "871030e0-e485-41d5-882c-30201429a23f", + "employeeId": "5763cd4d5fc36e4f842ca5a9", + "price": { + "priceIncludingTax": 35.75, + "taxAmount": 5.75, + "priceExcludingTax": 30, + "currency": "EUR" + }, + "label": "Taxi Fare", + "description": "Ride from the airport to the office.", + "category": "Transportation", + "receiptLink": "https://example.com/receipt5.jpg", + "status": "submitted", + "creationDate": "2024-07-05T19:00:00Z", + "updateDate": "2024-07-05T19:00:00Z" + }, + { + "id": "e38e7368-8686-4211-a6a2-844806839a4c", + "employeeId": "5763cd4d979b62a209809160", + "price": { + "priceIncludingTax": 180.00, + "taxAmount": 30.00, + "priceExcludingTax": 150, + "currency": "USD" + }, + "label": "Car Rental", + "description": "Car rental for a week.", + "category": "Transportation", + "receiptLink": "https://example.com/receipt6.png", + "status": "approved", + "creationDate": "2024-02-28T13:40:00Z", + "updateDate": "2024-03-02T11:15:00Z" + }, + { + "id": "90432a7b-3009-491a-832b-a4622721a490", + "employeeId": "5763cd4d15e6c2c28b70f2e8", + "price": { + "priceIncludingTax": 65.00, + "taxAmount": 10.83, + "priceExcludingTax": 54.17, + "currency": "USD" + }, + "label": "Client Meal", + "description": "Dinner with a potential client.", + "category": "Meals", + "receiptLink": "https://example.com/receipt7.pdf", + "status": "in_review", + "creationDate": "2024-07-18T20:30:00Z", + "updateDate": "2024-07-19T09:00:00Z" + }, + { + "id": "a88a026a-e928-48b6-8a1d-90d980e7a423", + "employeeId": "5763cd4d5d6ad8dfc6c34883", + "price": { + "priceIncludingTax": 95.99, + "taxAmount": 15.99, + "priceExcludingTax": 80, + "currency": "EUR" + }, + "label": "Software", + "description": "Monthly subscription to project management software.", + "category": "Software", + "receiptLink": "https://example.com/receipt8.jpg", + "status": "approved", + "creationDate": "2024-05-10T10:00:00Z", + "updateDate": "2024-05-11T14:20:00Z" + }, + { + "id": "21e5615a-9c2a-40f2-a489-0c2f4a866098", + "employeeId": "5763cd4dc378a38ecd387737", + "price": { + "priceIncludingTax": 25.50, + "taxAmount": 4.25, + "priceExcludingTax": 21.25, + "currency": "USD" + }, + "label": "Parking Fees", + "description": "Parking fees at the airport.", + "category": "Transportation", + "receiptLink": "https://example.com/receipt9.png", + "status": "created", + "creationDate": "2024-07-30T17:45:00Z", + "updateDate": "2024-07-30T17:45:00Z" + }, + { + "id": "5200c69a-e387-4292-9282-98e66878524c", + "employeeId": "5763cd4dfa6f96cd26c65787", + "price": { + "priceIncludingTax": 150.00, + "taxAmount": 25.00, + "priceExcludingTax": 125, + "currency": "USD" + }, + "label": "Training", + "description": "Participation in professional training.", + "category": "Training", + "receiptLink": "https://example.com/receipt10.pdf", + "status": "approved", + "creationDate": "2024-04-05T09:00:00Z", + "updateDate": "2024-04-07T11:30:00Z" + }, + { + "id": "9b60a0a2-d2cf-41ab-a8a6-690080e77898", + "employeeId": "5763cd4d9d2a4f259b53c901", + "price": { + "priceIncludingTax": 75.20, + "taxAmount": 12.53, + "priceExcludingTax": 62.67, + "currency": "EUR" + }, + "label": "Office Supplies", + "description": "Purchase of office supplies.", + "category": "Supplies", + "receiptLink": "https://example.com/receipt11.jpg", + "status": "approved", + "creationDate": "2024-06-12T14:20:00Z", + "updateDate": "2024-06-14T10:15:00Z" + }, + { + "id": "8478041f-7a41-46f0-9158-34759c409873", + "employeeId": "5763cd4d51fdb6588742f99e", + "price": { + "priceIncludingTax": 280.00, + "taxAmount": 46.67, + "priceExcludingTax": 233.33, + "currency": "USD" + }, + "label": "Plane Ticket", + "description": "Business trip to New York.", + "category": "Travel", + "receiptLink": "https://example.com/receipt12.png", + "status": "submitted", + "creationDate": "2024-07-25T11:30:00Z", + "updateDate": "2024-07-25T11:30:00Z" + }, + { + "id": "4902854f-2049-4498-9597-74823829501f", + "employeeId": "5763cd4dba6362a3f92c954e", + "price": { + "priceIncludingTax": 95.00, + "taxAmount": 15.83, + "priceExcludingTax": 79.17, + "currency": "EUR" + }, + "label": "Hotel", + "description": "Hotel night in Paris.", + "category": "Accommodation", + "receiptLink": "https://example.com/receipt13.pdf", + "status": "in_review", + "creationDate": "2024-08-01T08:45:00Z", + "updateDate": "2024-08-02T10:00:00Z" + }, + { + "id": "4309573f-8903-4a42-a095-839529490582", + "employeeId": "5763cd4d3b57c672861bfa1f", + "price": { + "priceIncludingTax": 42.50, + "taxAmount": 7.08, + "priceExcludingTax": 35.42, + "currency": "EUR" + }, + "label": "Taxi Fare", + "description": "Ride from the station to the conference venue.", + "category": "Transportation", + "receiptLink": "https://example.com/receipt14.jpg", + "status": "approved", + "creationDate": "2024-05-20T18:30:00Z", + "updateDate": "2024-05-22T09:15:00Z" + }, + { + "id": "3028759f-9023-4a83-a785-928375928375", + "employeeId": "5763cd4d5fc36e4f842ca5a9", + "price": { + "priceIncludingTax": 190.00, + "taxAmount": 31.67, + "priceExcludingTax": 158.33, + "currency": "USD" + }, + "label": "Car Rental", + "description": "Weekend car rental.", + "category": "Transportation", + "receiptLink": "https://example.com/receipt15.png", + "status": "created", + "creationDate": "2024-07-28T12:00:00Z", + "updateDate": "2024-07-28T12:00:00Z" + }, + { + "id": "92837592-8375-9283-7592-837592837592", + "employeeId": "5763cd4d979b62a209809160", + "price": { + "priceIncludingTax": 70.00, + "taxAmount": 11.67, + "priceExcludingTax": 58.33, + "currency": "USD" + }, + "label": "Client Meal", + "description": "Lunch with a client to discuss a project.", + "category": "Meals", + "receiptLink": "https://example.com/receipt16.pdf", + "status": "declined", + "creationDate": "2024-03-08T13:15:00Z", + "updateDate": "2024-03-10T09:30:00Z" + }, + { + "id": "85930583-0385-9305-8303-859305830385", + "employeeId": "5763cd4d15e6c2c28b70f2e8", + "price": { + "priceIncludingTax": 110.50, + "taxAmount": 18.42, + "priceExcludingTax": 92.08, + "currency": "EUR" + }, + "label": "Software", + "description": "Purchase of a license for design software.", + "category": "Software", + "receiptLink": "https://example.com/receipt17.jpg", + "status": "approved", + "creationDate": "2024-06-05T10:45:00Z", + "updateDate": "2024-06-07T14:30:00Z" + }, + { + "id": "03958305-8305-9385-0385-930583059385", + "employeeId": "5763cd4d5d6ad8dfc6c34883", + "price": { + "priceIncludingTax": 35.00, + "taxAmount": 5.83, + "priceExcludingTax": 29.17, + "currency": "USD" + }, + "label": "Parking Fees", + "description": "Parking fees at the conference center.", + "category": "Transportation", + "receiptLink": "https://example.com/receipt18.png", + "status": "submitted", + "creationDate": "2024-07-15T16:20:00Z", + "updateDate": "2024-07-15T16:20:00Z" + }, + { + "id": "93850395-8503-9585-0395-850395850395", + "employeeId": "5763cd4dc378a38ecd387737", + "price": { + "priceIncludingTax": 165.00, + "taxAmount": 27.50, + "priceExcludingTax": 137.50, + "currency": "USD" + }, + "label": "Training", + "description": "Registration for a webinar on digital marketing.", + "category": "Training", + "receiptLink": "https://example.com/receipt19.pdf", + "status": "in_review", + "creationDate": "2024-07-10T09:00:00Z", + "updateDate": "2024-07-11T14:30:00Z" + }, + { + "id": "85039585-0395-8503-9585-039585039585", + "employeeId": "5763cd4dfa6f96cd26c65787", + "price": { + "priceIncludingTax": 82.75, + "taxAmount": 13.79, + "priceExcludingTax": 68.96, + "currency": "EUR" + }, + "label": "Office Supplies", + "description": "Purchase of ink cartridges for the printer.", + "category": "Supplies", + "receiptLink": "https://example.com/receipt20.jpg", + "status": "approved", + "creationDate": "2024-06-28T11:45:00Z", + "updateDate": "2024-06-30T09:15:00Z" + } +] \ No newline at end of file diff --git a/steps/02.02-navigation-solution/src/functions/timing.ts b/steps/02.02-navigation-solution/src/functions/timing.ts new file mode 100644 index 0000000..3b8c6f3 --- /dev/null +++ b/steps/02.02-navigation-solution/src/functions/timing.ts @@ -0,0 +1,7 @@ +export const debounce = (fn: Function, ms = 300) => { + let timeoutId: ReturnType; + return function (this: any, ...args: any[]) { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => fn.apply(this, args), ms); + }; +}; diff --git a/steps/02.02-navigation-solution/src/styles/global.css b/steps/02.02-navigation-solution/src/styles/global.css new file mode 100644 index 0000000..f77ed90 --- /dev/null +++ b/steps/02.02-navigation-solution/src/styles/global.css @@ -0,0 +1,41 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --color-bg-global: #e9effc; + --color-bg-primary: #ffffff; + --color-bg-secondary: #f5f5f5; + --color-text-primary: #000000; + + --spacing-sm: 0.5rem; + --spacing-md: 0.75rem; + --spacing-lg: 1rem; + --spacing-xl: 1.5rem; +} + +/* Headings */ + +.heading1 { + font-size: 2rem; + font-weight: bold; + margin-bottom: var(--spacing-lg); +} + +.heading2 { + font-size: 1.5rem; + font-weight: bold; + margin-bottom: var(--spacing-lg); +} + +.heading3 { + font-size: 1.125rem; + font-weight: bold; + margin-bottom: var(--spacing-lg); +} + +.heading4 { + font-size: 1rem; + font-weight: bold; + margin-bottom: var(--spacing-lg); +} diff --git a/steps/02.02-navigation-solution/src/types.ts b/steps/02.02-navigation-solution/src/types.ts new file mode 100644 index 0000000..82cffb5 --- /dev/null +++ b/steps/02.02-navigation-solution/src/types.ts @@ -0,0 +1,39 @@ +export type Person = { + id: string; + photo?: string; + firstname: string; + lastname: string; + position: string; + entryDate: string; + birthDate: string; + gender: string; + email: string; + phone: string; + isManager: boolean; + manager?: string; + managerId?: string; +}; + +export type Expense = { + id: string; + employeeId: string; + price: { + priceIncludingTax: number; + taxAmount: number; + priceExcludingTax: number; + currency: string; + }; + label: string; + description: string; + category: string; + receiptLink: string; + status: 'approved' | 'created' | 'declined'; + creationDate: string; + updateDate: string; +}; + +export type PaginationAttributes = { + per_page?: number; + page: number; + total_pages: number; +}; diff --git a/steps/02.02-navigation-solution/tailwind.config.js b/steps/02.02-navigation-solution/tailwind.config.js new file mode 100644 index 0000000..eaa361c --- /dev/null +++ b/steps/02.02-navigation-solution/tailwind.config.js @@ -0,0 +1,9 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ['./src/**/*.{js,ts,jsx,tsx,mdx}'], + darkMode: 'selector', + theme: { + extend: {}, + }, + plugins: [], +}; diff --git a/steps/02.02-navigation-solution/tsconfig.json b/steps/02.02-navigation-solution/tsconfig.json new file mode 100644 index 0000000..7b28589 --- /dev/null +++ b/steps/02.02-navigation-solution/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/steps/02.02-navigation/.env.example b/steps/02.02-navigation/.env.example new file mode 100644 index 0000000..1ebabff --- /dev/null +++ b/steps/02.02-navigation/.env.example @@ -0,0 +1,2 @@ +API_BASE_URL=http://localhost:3001 +API_KEY=XXXX diff --git a/steps/02.02-navigation/.eslintrc.json b/steps/02.02-navigation/.eslintrc.json new file mode 100644 index 0000000..bffb357 --- /dev/null +++ b/steps/02.02-navigation/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/steps/02.02-navigation/.gitignore b/steps/02.02-navigation/.gitignore new file mode 100644 index 0000000..fd3dbb5 --- /dev/null +++ b/steps/02.02-navigation/.gitignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/steps/02.02-navigation/README.md b/steps/02.02-navigation/README.md new file mode 100644 index 0000000..053312b --- /dev/null +++ b/steps/02.02-navigation/README.md @@ -0,0 +1 @@ +# 02.02 - Navigation diff --git a/steps/02.02-navigation/next.config.mjs b/steps/02.02-navigation/next.config.mjs new file mode 100644 index 0000000..16343f6 --- /dev/null +++ b/steps/02.02-navigation/next.config.mjs @@ -0,0 +1,15 @@ +const apiUrl = new URL(process.env.API_BASE_URL || 'http://localhost:3001'); + +/** @type {import('next').NextConfig} */ +const nextConfig = { + images: { + remotePatterns: [ + { + hostname: apiUrl.hostname, + port: apiUrl.port, + }, + ], + }, +}; + +export default nextConfig; diff --git a/steps/02.02-navigation/package.json b/steps/02.02-navigation/package.json new file mode 100644 index 0000000..3fde290 --- /dev/null +++ b/steps/02.02-navigation/package.json @@ -0,0 +1,37 @@ +{ + "name": "02.02", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "clsx": "^2.1.1", + "jose": "^5.6.3", + "jsonwebtoken": "^9.0.2", + "next": "14.2.5", + "react": "^18", + "react-dom": "^18", + "react-error-boundary": "^4.0.13", + "rehype-sanitize": "^6.0.0", + "rehype-stringify": "^10.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.0", + "server-only": "^0.0.1", + "showdown": "^2.1.0", + "unified": "^11.0.5" + }, + "devDependencies": { + "@types/jsonwebtoken": "^9.0.6", + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "@types/showdown": "^2.0.6", + "eslint": "^8", + "eslint-config-next": "14.2.5", + "typescript": "^5" + } +} diff --git a/steps/02.02-navigation/postcss.config.js b/steps/02.02-navigation/postcss.config.js new file mode 100644 index 0000000..33ad091 --- /dev/null +++ b/steps/02.02-navigation/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/steps/02.02-navigation/public/next.svg b/steps/02.02-navigation/public/next.svg new file mode 100644 index 0000000..5174b28 --- /dev/null +++ b/steps/02.02-navigation/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/steps/02.02-navigation/public/portraits/men/30.jpg b/steps/02.02-navigation/public/portraits/men/30.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d04b7a2669245212620be0cbb17922fd1cb3cbfe GIT binary patch literal 4349 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 w3$0jQtjS=6rPW;hG!!51^p@HCR)|Jncu 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 literal 0 HcmV?d00001 diff --git a/steps/02.02-navigation/public/portraits/men/78.jpg b/steps/02.02-navigation/public/portraits/men/78.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6438e80b9b56fcf7e6e0e9a02eb8cc54a53c91aa GIT binary patch literal 4643 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 Y-IZTg@->$i4XtycyzDV!v4pAr0qF|a9RL6T literal 0 HcmV?d00001 diff --git a/steps/02.02-navigation/public/portraits/men/86.jpg b/steps/02.02-navigation/public/portraits/men/86.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9358491105b401a359ef698035ab9b05b84e0457 GIT binary patch literal 5433 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 Kdoq>(yYN42Z*!dh literal 0 HcmV?d00001 diff --git a/steps/02.02-navigation/public/portraits/women/65.jpg b/steps/02.02-navigation/public/portraits/women/65.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3cab57987a6ff10c5639fad656fb0fc77ba569c9 GIT binary patch literal 5972 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 C^!eWa literal 0 HcmV?d00001 diff --git a/steps/02.02-navigation/public/portraits/women/85.jpg b/steps/02.02-navigation/public/portraits/women/85.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0a900f9e8874eddf5978f08e6de03815f23bb1ea GIT binary patch literal 3912 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 literal 0 HcmV?d00001 diff --git a/steps/02.02-navigation/public/portraits/women/93.jpg b/steps/02.02-navigation/public/portraits/women/93.jpg new file mode 100644 index 0000000000000000000000000000000000000000..81ea0613898f679ad77f8e4563a93be893f38a07 GIT binary patch literal 4871 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 \ No newline at end of file diff --git a/steps/02.02-navigation/src/app/(auth)/layout.tsx b/steps/02.02-navigation/src/app/(auth)/layout.tsx new file mode 100644 index 0000000..cac31a7 --- /dev/null +++ b/steps/02.02-navigation/src/app/(auth)/layout.tsx @@ -0,0 +1,12 @@ +type AuthLayoutProps = { + children: React.ReactNode; +}; + +const AuthLayout: React.FC = ({ children }) => ( +
+
+
{children}
+
+); + +export default AuthLayout; diff --git a/steps/02.02-navigation/src/app/(auth)/login/page.tsx b/steps/02.02-navigation/src/app/(auth)/login/page.tsx new file mode 100644 index 0000000..3ae0596 --- /dev/null +++ b/steps/02.02-navigation/src/app/(auth)/login/page.tsx @@ -0,0 +1,30 @@ +import { Metadata } from 'next'; + +import TextField from '@/components/TextField'; +import Button from '@/components/Button'; + +export const metadata: Metadata = { + title: 'SFEIR People | Login', +}; + +const LoginPage = () => { + return ( +
+

Welcome !

+ + + + + ); +}; + +export default LoginPage; diff --git a/steps/02.02-navigation/src/app/(dashboard)/employees/[id]/edit/page.tsx b/steps/02.02-navigation/src/app/(dashboard)/employees/[id]/edit/page.tsx new file mode 100644 index 0000000..9d29dc0 --- /dev/null +++ b/steps/02.02-navigation/src/app/(dashboard)/employees/[id]/edit/page.tsx @@ -0,0 +1,24 @@ +import EmployeeForm from '@/components/EmployeeForm'; +import PageTitle from '@/components/PageTitle'; + +import employeesData from '@/data/employees.json'; + +const EmployeeDetail = async ({ params }: { params: { id: string } }) => { + const employee = employeesData.find((employee) => employee.id === params.id); + + if (!employee) return Single Employee - Not found; + + return ( + <> + + Single Employee - {employee.firstname} {employee.lastname} | Edit + + +
+ +
+ + ); +}; + +export default EmployeeDetail; diff --git a/steps/02.02-navigation/src/app/(dashboard)/employees/[id]/page.tsx b/steps/02.02-navigation/src/app/(dashboard)/employees/[id]/page.tsx new file mode 100644 index 0000000..a498c63 --- /dev/null +++ b/steps/02.02-navigation/src/app/(dashboard)/employees/[id]/page.tsx @@ -0,0 +1,21 @@ +import PageTitle from '@/components/PageTitle'; +import PersonCard from '@/components/PersonCard'; + +import employeesData from '@/data/employees.json'; + +const EmployeeDetail = async ({ params }: { params: { id: string } }) => { + const employee = employeesData.find((employee) => employee.id === params.id); + + if (!employee) return Single Employee - Not found; + + return ( + <> + + Single Employee - {employee.firstname} {employee.lastname} + + + + ); +}; + +export default EmployeeDetail; diff --git a/steps/02.02-navigation/src/app/(dashboard)/employees/new/page.tsx b/steps/02.02-navigation/src/app/(dashboard)/employees/new/page.tsx new file mode 100644 index 0000000..f1e5157 --- /dev/null +++ b/steps/02.02-navigation/src/app/(dashboard)/employees/new/page.tsx @@ -0,0 +1,18 @@ +import EmployeeForm from '@/components/EmployeeForm'; +import PageTitle from '@/components/PageTitle'; + +const EmployeeDetail = async () => { + return ( + <> + + Employees | Create + + +
+ +
+ + ); +}; + +export default EmployeeDetail; diff --git a/steps/02.02-navigation/src/app/(dashboard)/employees/page.tsx b/steps/02.02-navigation/src/app/(dashboard)/employees/page.tsx new file mode 100644 index 0000000..55c3cd8 --- /dev/null +++ b/steps/02.02-navigation/src/app/(dashboard)/employees/page.tsx @@ -0,0 +1,19 @@ +import PageTitle from '@/components/PageTitle'; +import PersonCard from '@/components/PersonCard'; + +import employeesData from '@/data/employees.json'; + +const Employees = async () => { + return ( +
+ Employees +
+ {employeesData?.map((employee) => ( + + ))} +
+
+ ); +}; + +export default Employees; diff --git a/steps/02.02-navigation/src/app/(dashboard)/expenses/[id]/page.tsx b/steps/02.02-navigation/src/app/(dashboard)/expenses/[id]/page.tsx new file mode 100644 index 0000000..bda58e2 --- /dev/null +++ b/steps/02.02-navigation/src/app/(dashboard)/expenses/[id]/page.tsx @@ -0,0 +1,18 @@ +import ExpenseDetails from '@/components/ExpensesDetails'; +import PageTitle from '@/components/PageTitle'; + +import expensesData from '@/data/expenses.json'; +import { Expense } from '@/types'; + +const SingleExpense = ({ params }: { params: { id: string } }) => { + const expense = expensesData.find((expense) => expense.id === params.id); + + return ( + <> + Single Expense - {expense?.label || 'Not found'} + {expense && } + + ); +}; + +export default SingleExpense; diff --git a/steps/02.02-navigation/src/app/(dashboard)/expenses/page.tsx b/steps/02.02-navigation/src/app/(dashboard)/expenses/page.tsx new file mode 100644 index 0000000..55d1d65 --- /dev/null +++ b/steps/02.02-navigation/src/app/(dashboard)/expenses/page.tsx @@ -0,0 +1,17 @@ +import ExpensesTable from '@/components/ExpensesTable'; +import PageTitle from '@/components/PageTitle'; + +import { Expense } from '@/types'; + +import expensesData from '@/data/expenses.json'; + +const Expenses = async () => { + return ( + <> + Expenses + } /> + + ); +}; + +export default Expenses; diff --git a/steps/02.02-navigation/src/app/(dashboard)/layout.tsx b/steps/02.02-navigation/src/app/(dashboard)/layout.tsx new file mode 100644 index 0000000..07b02c8 --- /dev/null +++ b/steps/02.02-navigation/src/app/(dashboard)/layout.tsx @@ -0,0 +1,29 @@ +import { Metadata } from 'next'; +import Link from 'next/link'; +import Image from 'next/image'; + +import NavigationMenu from '@/components/NavigationMenu'; + +import logo from '@/assets/svg/logo.svg'; + +type DashboardLayoutProps = { children: React.ReactNode }; + +export const metadata: Metadata = { + title: 'SFEIR People | Dashboard', +}; + +const DashboardLayout: React.FC = async ({ children }) => { + return ( +
+
+ + People logo + + +
+
{children}
+
+ ); +}; + +export default DashboardLayout; diff --git a/steps/02.02-navigation/src/app/(dashboard)/page.tsx b/steps/02.02-navigation/src/app/(dashboard)/page.tsx new file mode 100644 index 0000000..f581ebb --- /dev/null +++ b/steps/02.02-navigation/src/app/(dashboard)/page.tsx @@ -0,0 +1,11 @@ +import PageTitle from '@/components/PageTitle'; + +const HomePage = () => { + return ( + <> + SFEIR People + + ); +}; + +export default HomePage; diff --git a/steps/02.02-navigation/src/app/layout.tsx b/steps/02.02-navigation/src/app/layout.tsx new file mode 100644 index 0000000..e7d90e9 --- /dev/null +++ b/steps/02.02-navigation/src/app/layout.tsx @@ -0,0 +1,21 @@ +import type { Metadata } from 'next'; +import { Inter } from 'next/font/google'; + +const inter = Inter({ subsets: ['latin'] }); + +import '@/styles/global.css'; + +export const metadata: Metadata = { + title: 'SFEIR People', + description: 'SFEIR People dashboard application', +}; + +const RootLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => { + return ( + + {children} + + ); +}; + +export default RootLayout; diff --git a/steps/02.02-navigation/src/assets/images/profile-placeholder.jpg b/steps/02.02-navigation/src/assets/images/profile-placeholder.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6fa00ea6c9e371e542006bb4bd69ae5e6922a324 GIT binary patch literal 11940 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 m>aIBzX~|65y0*1u%UVegt+JNI)Z4`imH;jI + + + + + + + + diff --git a/steps/02.02-navigation/src/assets/svg/logoDark.svg b/steps/02.02-navigation/src/assets/svg/logoDark.svg new file mode 100644 index 0000000..06eb6ed --- /dev/null +++ b/steps/02.02-navigation/src/assets/svg/logoDark.svg @@ -0,0 +1,28 @@ + + + + + + + + + diff --git a/steps/02.02-navigation/src/components/Alert.tsx b/steps/02.02-navigation/src/components/Alert.tsx new file mode 100644 index 0000000..1c90895 --- /dev/null +++ b/steps/02.02-navigation/src/components/Alert.tsx @@ -0,0 +1,17 @@ +import clsx from 'clsx'; + +type AlertProps = { + children: React.ReactNode; + className?: string; +}; + +const Alert: React.FC = ({ children, className }) => ( +
+ {children} +
+); + +export default Alert; diff --git a/steps/02.02-navigation/src/components/Button.tsx b/steps/02.02-navigation/src/components/Button.tsx new file mode 100644 index 0000000..2cee623 --- /dev/null +++ b/steps/02.02-navigation/src/components/Button.tsx @@ -0,0 +1,34 @@ +import clsx from 'clsx'; + +export type ButtonProps = { + children: React.ReactNode; + className?: string; + variant?: 'primary' | 'secondary'; + component?: C; +} & Omit, 'className' | 'variant'>; + +const classNames = { + primary: 'inline-block text-white bg-blue-700 hover:bg-blue-800 font-medium rounded-lg text-sm px-5 py-2.5', + secondary: [ + 'inline-block py-2.5 px-5 text-sm font-medium text-slate-900 bg-white rounded-lg border border-gray-200', + 'hover:bg-gray-100 hover:text-blue-700', + 'dark:bg-slate-900 dark:text-white dark:hover:bg-slate-950 dark:hover:text-blue-200 dark:hover:border-blue-200', + ].join(' '), +}; + +const Button = ({ + children, + className, + variant = 'secondary', + component, + ...restProps +}: ButtonProps) => { + const Component = component || 'button'; + return ( + + {children} + + ); +}; + +export default Button; diff --git a/steps/02.02-navigation/src/components/EmployeeForm.tsx b/steps/02.02-navigation/src/components/EmployeeForm.tsx new file mode 100644 index 0000000..dc15055 --- /dev/null +++ b/steps/02.02-navigation/src/components/EmployeeForm.tsx @@ -0,0 +1,113 @@ +'use client'; + +import Image from 'next/image'; +import TextField from '@/components/TextField'; +import { Person } from '@/types'; +import { useFormState } from 'react-dom'; + +import placeholderImage from '@/assets/images/profile-placeholder.jpg'; +import Button from './Button'; + +type ActionState = { + validationErrors?: { [key: string]: Array }; +}; + +type Action = (id: string, formData: FormData) => Promise; + +type EmployeeFormProps = { + employee?: Person; + action?: Action; + className?: string; +}; + +const initialState = { + validationErrors: {}, +} as ActionState; + +const EmployeeForm: React.FC = ({ employee, action, className }) => { + // @ts-ignore + const [state, formAction] = useFormState(action, initialState as unknown as void); + + return ( +
+
+ {employee +
+
+
+ + + + + +
+
+ + + +
+
+
+ +
+
+ ); +}; + +export default EmployeeForm; diff --git a/steps/02.02-navigation/src/components/ExpensesDetails.tsx b/steps/02.02-navigation/src/components/ExpensesDetails.tsx new file mode 100644 index 0000000..f59b51a --- /dev/null +++ b/steps/02.02-navigation/src/components/ExpensesDetails.tsx @@ -0,0 +1,56 @@ +import { Expense } from '@/types'; +import Paper from './Paper'; + +type ExpenseDetailsRowProps = { + label: string; + value: string; +}; + +const ExpenseDetailsRow: React.FC = ({ label, value }) => ( +
+ {label} + {value} +
+); + +type ExpenseDetailsProps = { + expense: Expense; +}; + +const ExpenseDetails: React.FC = ({ expense }) => ( + <> +
+
+

Information

+ + + + + +
+
+

Workflow

+ + + + + +
+
+
+
+

Amount

+ + + + + +
+
+ +); + +export default ExpenseDetails; diff --git a/steps/02.02-navigation/src/components/ExpensesTable.tsx b/steps/02.02-navigation/src/components/ExpensesTable.tsx new file mode 100644 index 0000000..d124cab --- /dev/null +++ b/steps/02.02-navigation/src/components/ExpensesTable.tsx @@ -0,0 +1,49 @@ +import { Expense } from '@/types'; +import clsx from 'clsx'; + +type ExpensesTableProps = { + expenses: Array; +}; + +const ExpensesTable: React.FC = ({ expenses }) => { + return ( + + + + + + + + + + + {expenses.map((expense, index) => ( + + + + + + + ))} + +
+ Label + + Creation date + + Category + + Price +
{expense.label}{new Date(expense.creationDate).toLocaleDateString()}{expense.category} + {expense.price.priceIncludingTax} {expense.price.currency} +
+ ); +}; + +export default ExpensesTable; diff --git a/steps/02.02-navigation/src/components/Icons/ArrowLeft.tsx b/steps/02.02-navigation/src/components/Icons/ArrowLeft.tsx new file mode 100644 index 0000000..1cc10c7 --- /dev/null +++ b/steps/02.02-navigation/src/components/Icons/ArrowLeft.tsx @@ -0,0 +1,25 @@ +type ArrowLeftProps = { + className?: string; +}; + +const ArrowLeft: React.FC = ({ className }) => ( + +); + +export default ArrowLeft; diff --git a/steps/02.02-navigation/src/components/Icons/Eye.tsx b/steps/02.02-navigation/src/components/Icons/Eye.tsx new file mode 100644 index 0000000..04beb91 --- /dev/null +++ b/steps/02.02-navigation/src/components/Icons/Eye.tsx @@ -0,0 +1,11 @@ +type EyeProps = { + className?: string; +}; + +const Eye: React.FC = ({ className }) => ( + + + +); + +export default Eye; diff --git a/steps/02.02-navigation/src/components/Icons/Loader.tsx b/steps/02.02-navigation/src/components/Icons/Loader.tsx new file mode 100644 index 0000000..9c81994 --- /dev/null +++ b/steps/02.02-navigation/src/components/Icons/Loader.tsx @@ -0,0 +1,25 @@ +type LoaderProps = { + className?: string; +}; + +const Loader: React.FC = ({ className }) => ( + +); + +export default Loader; diff --git a/steps/02.02-navigation/src/components/NavigationMenu.tsx b/steps/02.02-navigation/src/components/NavigationMenu.tsx new file mode 100644 index 0000000..e3bf528 --- /dev/null +++ b/steps/02.02-navigation/src/components/NavigationMenu.tsx @@ -0,0 +1,25 @@ +const NavigationMenu = () => { + return ( + + ); +}; + +export default NavigationMenu; diff --git a/steps/02.02-navigation/src/components/PageTitle.tsx b/steps/02.02-navigation/src/components/PageTitle.tsx new file mode 100644 index 0000000..217fe69 --- /dev/null +++ b/steps/02.02-navigation/src/components/PageTitle.tsx @@ -0,0 +1,25 @@ +import Link from 'next/link'; + +import ArrowLeft from './Icons/ArrowLeft'; + +type PageTitleProps = { + children: React.ReactNode; + backHref?: string; +}; + +const PageTitle: React.FC = ({ children, backHref }) => ( +
+ {backHref && ( + + + Go back + + )} +

{children}

+
+); + +export default PageTitle; diff --git a/steps/02.02-navigation/src/components/Paper.tsx b/steps/02.02-navigation/src/components/Paper.tsx new file mode 100644 index 0000000..8ba656e --- /dev/null +++ b/steps/02.02-navigation/src/components/Paper.tsx @@ -0,0 +1,14 @@ +import clsx from 'clsx'; + +type PaperProps = React.HTMLAttributes & { + children: React.ReactNode; + rounded?: boolean; +}; + +const Paper: React.FC = ({ children, rounded = true, ...restProps }) => ( +
+ {children} +
+); + +export default Paper; diff --git a/steps/02.02-navigation/src/components/PersonCard.tsx b/steps/02.02-navigation/src/components/PersonCard.tsx new file mode 100644 index 0000000..b38ee53 --- /dev/null +++ b/steps/02.02-navigation/src/components/PersonCard.tsx @@ -0,0 +1,43 @@ +import Image from 'next/image'; + +import { Person } from '@/types'; + +import placeholderImage from '@/assets/images/profile-placeholder.jpg'; + +type PersonCardProps = React.HTMLAttributes & { + person: Person; + actions?: React.ReactNode; + compact?: boolean; +}; + +const PersonCard: React.FC = ({ person, actions, className, compact = false }) => { + return ( +
+
+ {`Picture + + {person.firstname} {person.lastname} + + {person.position} +
+ + {!compact && ( +
+ {person.phone} + {person.email} + {person.manager && {person.manager}} +
+ )} + + {actions &&
{actions}
} +
+ ); +}; + +export default PersonCard; diff --git a/steps/02.02-navigation/src/components/TextField.tsx b/steps/02.02-navigation/src/components/TextField.tsx new file mode 100644 index 0000000..d8061e9 --- /dev/null +++ b/steps/02.02-navigation/src/components/TextField.tsx @@ -0,0 +1,31 @@ +import clsx from 'clsx'; + +type TextFieldProps = React.InputHTMLAttributes & { + label: string; + id: string; + type?: string; + className?: string; + errorMessages?: Array; +}; + +const TextField: React.FC = ({ label, id, type = 'text', className, errorMessages, ...restProps }) => { + return ( +
+ + + {errorMessages?.length &&

{errorMessages[0]}

} +
+ ); +}; + +export default TextField; diff --git a/steps/02.02-navigation/src/data/employees.json b/steps/02.02-navigation/src/data/employees.json new file mode 100644 index 0000000..5f5342e --- /dev/null +++ b/steps/02.02-navigation/src/data/employees.json @@ -0,0 +1,152 @@ +[ + { + "id": "5763cd4d9d2a4f259b53c901", + "photo": "/portraits/women/85.jpg", + "firstname": "Leanne", + "lastname": "Woodard", + "position": "Developer", + "entryDate": "27/10/2015", + "birthDate": "02/01/1974", + "gender": "f", + "email": "woodard.l@acme.com", + "phone": "0784112248", + "isManager": false, + "manager": "Erika", + "managerId": "5763cd4d3b57c672861bfa1f" + }, + { + "id": "5763cd4d51fdb6588742f99e", + "photo": "/portraits/men/56.jpg", + "firstname": "Castaneda", + "lastname": "Salinas", + "position": "Developer", + "entryDate": "04/10/2015", + "birthDate": "22/01/1963", + "gender": "m", + "email": "salinas.c@acme.com", + "phone": "0145652522", + "isManager": false, + "manager": "Erika", + "managerId": "5763cd4d3b57c672861bfa1f" + }, + { + "id": "5763cd4dba6362a3f92c954e", + "photo": "/portraits/women/24.jpg", + "firstname": "Phyllis", + "lastname": "Donovan", + "position": "Sales", + "entryDate": "30/03/2015", + "birthDate": "30/11/1951", + "gender": "f", + "email": "donovan.p@acme.com", + "phone": "0685230125", + "isManager": false, + "manager": "Erika", + "managerId": "5763cd4d3b57c672861bfa1f" + }, + { + "id": "5763cd4d3b57c672861bfa1f", + "photo": "/portraits/women/65.jpg", + "firstname": "Erika", + "lastname": "Guzman", + "position": "Product Owner", + "entryDate": "13/05/2016", + "birthDate": "19/03/1962", + "gender": "f", + "email": "guzman.e@acme.com", + "phone": "0678412587", + "isManager": true, + "manager": "Mercedes", + "managerId": "5763cd4d979b62a209809160" + }, + { + "id": "5763cd4d5fc36e4f842ca5a9", + "photo": "/portraits/men/30.jpg", + "firstname": "Moody", + "lastname": "Prince", + "position": "Developer", + "entryDate": "28/09/2015", + "birthDate": "15/04/1971", + "gender": "m", + "email": "prince.m@acme.com", + "phone": "0662589632", + "isManager": false, + "manager": "Mercedes", + "managerId": "5763cd4d979b62a209809160" + }, + { + "id": "5763cd4d979b62a209809160", + "photo": "/portraits/women/8.jpg", + "firstname": "Mercedes", + "lastname": "Hebert", + "position": "Product Owner", + "entryDate": "02/01/2016", + "birthDate": "20/07/1947", + "gender": "f", + "email": "hebert.m@acme.com", + "phone": "0125878522", + "isManager": true, + "manager": "Mclaughlin", + "managerId": "5763cd4dfa6f96cd26c65787" + }, + { + "id": "5763cd4d15e6c2c28b70f2e8", + "photo": "/portraits/men/86.jpg", + "firstname": "Howell", + "lastname": "Mcknight", + "position": "Sales", + "entryDate": "26/09/2015", + "birthDate": "18/07/1979", + "gender": "m", + "email": "mcknight.h@acme.com", + "phone": "0456987425", + "isManager": false, + "manager": "Mclaughlin", + "managerId": "5763cd4dfa6f96cd26c65787" + }, + { + "id": "5763cd4d5d6ad8dfc6c34883", + "photo": "/portraits/women/93.jpg", + "firstname": "Lizzie", + "lastname": "Morris", + "position": "Human Resources", + "entryDate": "03/05/2016", + "birthDate": "15/11/1981", + "gender": "f", + "email": "morris.l@acme.com", + "phone": "0662259988", + "isManager": false, + "manager": "Mclaughlin", + "managerId": "5763cd4dfa6f96cd26c65787" + }, + { + "id": "5763cd4dc378a38ecd387737", + "photo": "/portraits/men/34.jpg", + "firstname": "Roy", + "lastname": "Nielsen", + "position": "Sales", + "entryDate": "17/05/2016", + "birthDate": "21/10/1951", + "gender": "m", + "email": "nielsen.r@acme.com", + "phone": "0755669551", + "isManager": false, + "manager": "Mclaughlin", + "managerId": "5763cd4dfa6f96cd26c65787" + }, + { + "id": "5763cd4dfa6f96cd26c65787", + "photo": "/portraits/men/78.jpg", + "firstname": "Mclaughlin", + "lastname": "Cochran", + "position": "Director", + "entryDate": "11/04/2016", + "birthDate": "19/03/1973", + "gender": "m", + "email": "cochran.m@acme.com", + "phone": "0266334856", + "isManager": true, + "manager": "", + "managerId": "" + } +] diff --git a/steps/02.02-navigation/src/data/expenses.json b/steps/02.02-navigation/src/data/expenses.json new file mode 100644 index 0000000..0d096dc --- /dev/null +++ b/steps/02.02-navigation/src/data/expenses.json @@ -0,0 +1,342 @@ +[ + { + "id": "0475830f-a563-44e0-8c5c-6d829c11a132", + "employeeId": "5763cd4d51fdb6588742f99e", + "price": { + "priceIncludingTax": 120.50, + "taxAmount": 20.50, + "priceExcludingTax": 100, + "currency": "EUR" + }, + "label": "Business Lunch", + "description": "Lunch with a client to discuss a new project.", + "category": "Meals", + "receiptLink": "https://example.com/receipt1.pdf", + "status": "approved", + "creationDate": "2024-03-15T09:30:00Z", + "updateDate": "2024-03-18T14:45:00Z" + }, + { + "id": "a2e8b2c4-99d8-4c13-9a9c-0a8a68623260", + "employeeId": "5763cd4d51fdb6588742f99e", + "price": { + "priceIncludingTax": 55.20, + "taxAmount": 7.20, + "priceExcludingTax": 48, + "currency": "USD" + }, + "label": "Office Supplies", + "description": "Purchase of paper, pens, etc.", + "category": "Supplies", + "receiptLink": "https://example.com/receipt2.jpg", + "status": "created", + "creationDate": "2024-05-02T11:20:00Z", + "updateDate": "2024-05-02T11:20:00Z" + }, + { + "id": "3d3fb561-0d9c-4021-8285-d2f49c40c47d", + "employeeId": "5763cd4d51fdb6588742f99e", + "price": { + "priceIncludingTax": 250, + "taxAmount": 0, + "priceExcludingTax": 250, + "currency": "EUR" + }, + "label": "Plane Ticket", + "description": "Business trip to Berlin.", + "category": "Travel", + "receiptLink": "https://example.com/receipt3.png", + "status": "declined", + "creationDate": "2024-04-10T16:45:00Z", + "updateDate": "2024-04-12T09:30:00Z" + }, + { + "id": "4c41689c-e5f5-4029-a181-38c498e7a82b", + "employeeId": "5763cd4d5fc36e4f842ca5a9", + "price": { + "priceIncludingTax": 80.00, + "taxAmount": 13.33, + "priceExcludingTax": 66.67, + "currency": "USD" + }, + "label": "Hotel", + "description": "Hotel night in London.", + "category": "Accommodation", + "receiptLink": "https://example.com/receipt4.pdf", + "status": "approved", + "creationDate": "2024-06-20T08:15:00Z", + "updateDate": "2024-06-22T10:30:00Z" + }, + { + "id": "871030e0-e485-41d5-882c-30201429a23f", + "employeeId": "5763cd4d5fc36e4f842ca5a9", + "price": { + "priceIncludingTax": 35.75, + "taxAmount": 5.75, + "priceExcludingTax": 30, + "currency": "EUR" + }, + "label": "Taxi Fare", + "description": "Ride from the airport to the office.", + "category": "Transportation", + "receiptLink": "https://example.com/receipt5.jpg", + "status": "submitted", + "creationDate": "2024-07-05T19:00:00Z", + "updateDate": "2024-07-05T19:00:00Z" + }, + { + "id": "e38e7368-8686-4211-a6a2-844806839a4c", + "employeeId": "5763cd4d979b62a209809160", + "price": { + "priceIncludingTax": 180.00, + "taxAmount": 30.00, + "priceExcludingTax": 150, + "currency": "USD" + }, + "label": "Car Rental", + "description": "Car rental for a week.", + "category": "Transportation", + "receiptLink": "https://example.com/receipt6.png", + "status": "approved", + "creationDate": "2024-02-28T13:40:00Z", + "updateDate": "2024-03-02T11:15:00Z" + }, + { + "id": "90432a7b-3009-491a-832b-a4622721a490", + "employeeId": "5763cd4d15e6c2c28b70f2e8", + "price": { + "priceIncludingTax": 65.00, + "taxAmount": 10.83, + "priceExcludingTax": 54.17, + "currency": "USD" + }, + "label": "Client Meal", + "description": "Dinner with a potential client.", + "category": "Meals", + "receiptLink": "https://example.com/receipt7.pdf", + "status": "in_review", + "creationDate": "2024-07-18T20:30:00Z", + "updateDate": "2024-07-19T09:00:00Z" + }, + { + "id": "a88a026a-e928-48b6-8a1d-90d980e7a423", + "employeeId": "5763cd4d5d6ad8dfc6c34883", + "price": { + "priceIncludingTax": 95.99, + "taxAmount": 15.99, + "priceExcludingTax": 80, + "currency": "EUR" + }, + "label": "Software", + "description": "Monthly subscription to project management software.", + "category": "Software", + "receiptLink": "https://example.com/receipt8.jpg", + "status": "approved", + "creationDate": "2024-05-10T10:00:00Z", + "updateDate": "2024-05-11T14:20:00Z" + }, + { + "id": "21e5615a-9c2a-40f2-a489-0c2f4a866098", + "employeeId": "5763cd4dc378a38ecd387737", + "price": { + "priceIncludingTax": 25.50, + "taxAmount": 4.25, + "priceExcludingTax": 21.25, + "currency": "USD" + }, + "label": "Parking Fees", + "description": "Parking fees at the airport.", + "category": "Transportation", + "receiptLink": "https://example.com/receipt9.png", + "status": "created", + "creationDate": "2024-07-30T17:45:00Z", + "updateDate": "2024-07-30T17:45:00Z" + }, + { + "id": "5200c69a-e387-4292-9282-98e66878524c", + "employeeId": "5763cd4dfa6f96cd26c65787", + "price": { + "priceIncludingTax": 150.00, + "taxAmount": 25.00, + "priceExcludingTax": 125, + "currency": "USD" + }, + "label": "Training", + "description": "Participation in professional training.", + "category": "Training", + "receiptLink": "https://example.com/receipt10.pdf", + "status": "approved", + "creationDate": "2024-04-05T09:00:00Z", + "updateDate": "2024-04-07T11:30:00Z" + }, + { + "id": "9b60a0a2-d2cf-41ab-a8a6-690080e77898", + "employeeId": "5763cd4d9d2a4f259b53c901", + "price": { + "priceIncludingTax": 75.20, + "taxAmount": 12.53, + "priceExcludingTax": 62.67, + "currency": "EUR" + }, + "label": "Office Supplies", + "description": "Purchase of office supplies.", + "category": "Supplies", + "receiptLink": "https://example.com/receipt11.jpg", + "status": "approved", + "creationDate": "2024-06-12T14:20:00Z", + "updateDate": "2024-06-14T10:15:00Z" + }, + { + "id": "8478041f-7a41-46f0-9158-34759c409873", + "employeeId": "5763cd4d51fdb6588742f99e", + "price": { + "priceIncludingTax": 280.00, + "taxAmount": 46.67, + "priceExcludingTax": 233.33, + "currency": "USD" + }, + "label": "Plane Ticket", + "description": "Business trip to New York.", + "category": "Travel", + "receiptLink": "https://example.com/receipt12.png", + "status": "submitted", + "creationDate": "2024-07-25T11:30:00Z", + "updateDate": "2024-07-25T11:30:00Z" + }, + { + "id": "4902854f-2049-4498-9597-74823829501f", + "employeeId": "5763cd4dba6362a3f92c954e", + "price": { + "priceIncludingTax": 95.00, + "taxAmount": 15.83, + "priceExcludingTax": 79.17, + "currency": "EUR" + }, + "label": "Hotel", + "description": "Hotel night in Paris.", + "category": "Accommodation", + "receiptLink": "https://example.com/receipt13.pdf", + "status": "in_review", + "creationDate": "2024-08-01T08:45:00Z", + "updateDate": "2024-08-02T10:00:00Z" + }, + { + "id": "4309573f-8903-4a42-a095-839529490582", + "employeeId": "5763cd4d3b57c672861bfa1f", + "price": { + "priceIncludingTax": 42.50, + "taxAmount": 7.08, + "priceExcludingTax": 35.42, + "currency": "EUR" + }, + "label": "Taxi Fare", + "description": "Ride from the station to the conference venue.", + "category": "Transportation", + "receiptLink": "https://example.com/receipt14.jpg", + "status": "approved", + "creationDate": "2024-05-20T18:30:00Z", + "updateDate": "2024-05-22T09:15:00Z" + }, + { + "id": "3028759f-9023-4a83-a785-928375928375", + "employeeId": "5763cd4d5fc36e4f842ca5a9", + "price": { + "priceIncludingTax": 190.00, + "taxAmount": 31.67, + "priceExcludingTax": 158.33, + "currency": "USD" + }, + "label": "Car Rental", + "description": "Weekend car rental.", + "category": "Transportation", + "receiptLink": "https://example.com/receipt15.png", + "status": "created", + "creationDate": "2024-07-28T12:00:00Z", + "updateDate": "2024-07-28T12:00:00Z" + }, + { + "id": "92837592-8375-9283-7592-837592837592", + "employeeId": "5763cd4d979b62a209809160", + "price": { + "priceIncludingTax": 70.00, + "taxAmount": 11.67, + "priceExcludingTax": 58.33, + "currency": "USD" + }, + "label": "Client Meal", + "description": "Lunch with a client to discuss a project.", + "category": "Meals", + "receiptLink": "https://example.com/receipt16.pdf", + "status": "declined", + "creationDate": "2024-03-08T13:15:00Z", + "updateDate": "2024-03-10T09:30:00Z" + }, + { + "id": "85930583-0385-9305-8303-859305830385", + "employeeId": "5763cd4d15e6c2c28b70f2e8", + "price": { + "priceIncludingTax": 110.50, + "taxAmount": 18.42, + "priceExcludingTax": 92.08, + "currency": "EUR" + }, + "label": "Software", + "description": "Purchase of a license for design software.", + "category": "Software", + "receiptLink": "https://example.com/receipt17.jpg", + "status": "approved", + "creationDate": "2024-06-05T10:45:00Z", + "updateDate": "2024-06-07T14:30:00Z" + }, + { + "id": "03958305-8305-9385-0385-930583059385", + "employeeId": "5763cd4d5d6ad8dfc6c34883", + "price": { + "priceIncludingTax": 35.00, + "taxAmount": 5.83, + "priceExcludingTax": 29.17, + "currency": "USD" + }, + "label": "Parking Fees", + "description": "Parking fees at the conference center.", + "category": "Transportation", + "receiptLink": "https://example.com/receipt18.png", + "status": "submitted", + "creationDate": "2024-07-15T16:20:00Z", + "updateDate": "2024-07-15T16:20:00Z" + }, + { + "id": "93850395-8503-9585-0395-850395850395", + "employeeId": "5763cd4dc378a38ecd387737", + "price": { + "priceIncludingTax": 165.00, + "taxAmount": 27.50, + "priceExcludingTax": 137.50, + "currency": "USD" + }, + "label": "Training", + "description": "Registration for a webinar on digital marketing.", + "category": "Training", + "receiptLink": "https://example.com/receipt19.pdf", + "status": "in_review", + "creationDate": "2024-07-10T09:00:00Z", + "updateDate": "2024-07-11T14:30:00Z" + }, + { + "id": "85039585-0395-8503-9585-039585039585", + "employeeId": "5763cd4dfa6f96cd26c65787", + "price": { + "priceIncludingTax": 82.75, + "taxAmount": 13.79, + "priceExcludingTax": 68.96, + "currency": "EUR" + }, + "label": "Office Supplies", + "description": "Purchase of ink cartridges for the printer.", + "category": "Supplies", + "receiptLink": "https://example.com/receipt20.jpg", + "status": "approved", + "creationDate": "2024-06-28T11:45:00Z", + "updateDate": "2024-06-30T09:15:00Z" + } +] \ No newline at end of file diff --git a/steps/02.02-navigation/src/functions/timing.ts b/steps/02.02-navigation/src/functions/timing.ts new file mode 100644 index 0000000..3b8c6f3 --- /dev/null +++ b/steps/02.02-navigation/src/functions/timing.ts @@ -0,0 +1,7 @@ +export const debounce = (fn: Function, ms = 300) => { + let timeoutId: ReturnType; + return function (this: any, ...args: any[]) { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => fn.apply(this, args), ms); + }; +}; diff --git a/steps/02.02-navigation/src/styles/global.css b/steps/02.02-navigation/src/styles/global.css new file mode 100644 index 0000000..f77ed90 --- /dev/null +++ b/steps/02.02-navigation/src/styles/global.css @@ -0,0 +1,41 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --color-bg-global: #e9effc; + --color-bg-primary: #ffffff; + --color-bg-secondary: #f5f5f5; + --color-text-primary: #000000; + + --spacing-sm: 0.5rem; + --spacing-md: 0.75rem; + --spacing-lg: 1rem; + --spacing-xl: 1.5rem; +} + +/* Headings */ + +.heading1 { + font-size: 2rem; + font-weight: bold; + margin-bottom: var(--spacing-lg); +} + +.heading2 { + font-size: 1.5rem; + font-weight: bold; + margin-bottom: var(--spacing-lg); +} + +.heading3 { + font-size: 1.125rem; + font-weight: bold; + margin-bottom: var(--spacing-lg); +} + +.heading4 { + font-size: 1rem; + font-weight: bold; + margin-bottom: var(--spacing-lg); +} diff --git a/steps/02.02-navigation/src/types.ts b/steps/02.02-navigation/src/types.ts new file mode 100644 index 0000000..82cffb5 --- /dev/null +++ b/steps/02.02-navigation/src/types.ts @@ -0,0 +1,39 @@ +export type Person = { + id: string; + photo?: string; + firstname: string; + lastname: string; + position: string; + entryDate: string; + birthDate: string; + gender: string; + email: string; + phone: string; + isManager: boolean; + manager?: string; + managerId?: string; +}; + +export type Expense = { + id: string; + employeeId: string; + price: { + priceIncludingTax: number; + taxAmount: number; + priceExcludingTax: number; + currency: string; + }; + label: string; + description: string; + category: string; + receiptLink: string; + status: 'approved' | 'created' | 'declined'; + creationDate: string; + updateDate: string; +}; + +export type PaginationAttributes = { + per_page?: number; + page: number; + total_pages: number; +}; diff --git a/steps/02.02-navigation/tailwind.config.js b/steps/02.02-navigation/tailwind.config.js new file mode 100644 index 0000000..eaa361c --- /dev/null +++ b/steps/02.02-navigation/tailwind.config.js @@ -0,0 +1,9 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ['./src/**/*.{js,ts,jsx,tsx,mdx}'], + darkMode: 'selector', + theme: { + extend: {}, + }, + plugins: [], +}; diff --git a/steps/02.02-navigation/tsconfig.json b/steps/02.02-navigation/tsconfig.json new file mode 100644 index 0000000..7b28589 --- /dev/null +++ b/steps/02.02-navigation/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/steps/03.01-server-components-solution/.env.example b/steps/03.01-server-components-solution/.env.example new file mode 100644 index 0000000..1ebabff --- /dev/null +++ b/steps/03.01-server-components-solution/.env.example @@ -0,0 +1,2 @@ +API_BASE_URL=http://localhost:3001 +API_KEY=XXXX diff --git a/steps/03.01-server-components-solution/.eslintrc.json b/steps/03.01-server-components-solution/.eslintrc.json new file mode 100644 index 0000000..bffb357 --- /dev/null +++ b/steps/03.01-server-components-solution/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/steps/03.01-server-components-solution/.gitignore b/steps/03.01-server-components-solution/.gitignore new file mode 100644 index 0000000..fd3dbb5 --- /dev/null +++ b/steps/03.01-server-components-solution/.gitignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/steps/03.01-server-components-solution/.gitkeep b/steps/03.01-server-components-solution/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/steps/03.01-server-components-solution/README.md b/steps/03.01-server-components-solution/README.md new file mode 100644 index 0000000..1f91c62 --- /dev/null +++ b/steps/03.01-server-components-solution/README.md @@ -0,0 +1 @@ +# 03.01 - Server Components diff --git a/steps/03.01-server-components-solution/logs.txt b/steps/03.01-server-components-solution/logs.txt new file mode 100644 index 0000000..e69de29 diff --git a/steps/03.01-server-components-solution/next.config.mjs b/steps/03.01-server-components-solution/next.config.mjs new file mode 100644 index 0000000..16343f6 --- /dev/null +++ b/steps/03.01-server-components-solution/next.config.mjs @@ -0,0 +1,15 @@ +const apiUrl = new URL(process.env.API_BASE_URL || 'http://localhost:3001'); + +/** @type {import('next').NextConfig} */ +const nextConfig = { + images: { + remotePatterns: [ + { + hostname: apiUrl.hostname, + port: apiUrl.port, + }, + ], + }, +}; + +export default nextConfig; diff --git a/steps/03.01-server-components-solution/package.json b/steps/03.01-server-components-solution/package.json new file mode 100644 index 0000000..3545e02 --- /dev/null +++ b/steps/03.01-server-components-solution/package.json @@ -0,0 +1,38 @@ +{ + "name": "03.01-solution", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "bright": "^0.8.5", + "clsx": "^2.1.1", + "jose": "^5.6.3", + "jsonwebtoken": "^9.0.2", + "next": "14.2.5", + "react": "^18", + "react-dom": "^18", + "react-error-boundary": "^4.0.13", + "rehype-sanitize": "^6.0.0", + "rehype-stringify": "^10.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.0", + "server-only": "^0.0.1", + "showdown": "^2.1.0", + "unified": "^11.0.5" + }, + "devDependencies": { + "@types/jsonwebtoken": "^9.0.6", + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "@types/showdown": "^2.0.6", + "eslint": "^8", + "eslint-config-next": "14.2.5", + "typescript": "^5" + } +} diff --git a/steps/03.01-server-components-solution/postcss.config.js b/steps/03.01-server-components-solution/postcss.config.js new file mode 100644 index 0000000..33ad091 --- /dev/null +++ b/steps/03.01-server-components-solution/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/steps/03.01-server-components-solution/public/next.svg b/steps/03.01-server-components-solution/public/next.svg new file mode 100644 index 0000000..5174b28 --- /dev/null +++ b/steps/03.01-server-components-solution/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/steps/03.01-server-components-solution/public/portraits/men/30.jpg b/steps/03.01-server-components-solution/public/portraits/men/30.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d04b7a2669245212620be0cbb17922fd1cb3cbfe GIT binary patch literal 4349 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 w3$0jQtjS=6rPW;hG!!51^p@HCR)|Jncu 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 literal 0 HcmV?d00001 diff --git a/steps/03.01-server-components-solution/public/portraits/men/78.jpg b/steps/03.01-server-components-solution/public/portraits/men/78.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6438e80b9b56fcf7e6e0e9a02eb8cc54a53c91aa GIT binary patch literal 4643 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 Y-IZTg@->$i4XtycyzDV!v4pAr0qF|a9RL6T literal 0 HcmV?d00001 diff --git a/steps/03.01-server-components-solution/public/portraits/men/86.jpg b/steps/03.01-server-components-solution/public/portraits/men/86.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9358491105b401a359ef698035ab9b05b84e0457 GIT binary patch literal 5433 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 Kdoq>(yYN42Z*!dh literal 0 HcmV?d00001 diff --git a/steps/03.01-server-components-solution/public/portraits/women/65.jpg b/steps/03.01-server-components-solution/public/portraits/women/65.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3cab57987a6ff10c5639fad656fb0fc77ba569c9 GIT binary patch literal 5972 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 C^!eWa literal 0 HcmV?d00001 diff --git a/steps/03.01-server-components-solution/public/portraits/women/85.jpg b/steps/03.01-server-components-solution/public/portraits/women/85.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0a900f9e8874eddf5978f08e6de03815f23bb1ea GIT binary patch literal 3912 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 literal 0 HcmV?d00001 diff --git a/steps/03.01-server-components-solution/public/portraits/women/93.jpg b/steps/03.01-server-components-solution/public/portraits/women/93.jpg new file mode 100644 index 0000000000000000000000000000000000000000..81ea0613898f679ad77f8e4563a93be893f38a07 GIT binary patch literal 4871 zcmbtVc{G%7`@hFnW@HB}=RNOrpXa_l=UP73b6@9q?(1Og;00hZ)Whfj7z_pspal+I zQWRmdwVkjyGd+y4F601!BF)FuH;7UW0AIgg0#08`z}C)QfMyxc0%mXoZ~|$(YfykD z7HbUtt?bVLhzWox=|ir68}_dqN8H?lTmgU~AonTP075W?H6Uyn794Ph(;&?1dLHiv z;W7wI5ug`@@Y6%P%U}4_A@=+We?7zmOPn@zHaZ9kc>aZ@4zbH$`0y+OuICB9P>c_R z(Y`(*&^!E*!;{dt`&n5)n&;0G1aLqfXaND>0z!Zn@BzU<8q$7H&;E7Z@jrQ{z#qzS zh4uh&2@s$RPjDW}m4d7xAPBfa+5@t?L(2z>faI|EhZ=ytdm7}SaL5N8na&UZs?nx0I)#4rak+=`v2-T#C^yQN@oGEi~zuR9DrN50T6}i zF*+P90&PGAr=+BWQ$ZUQ6%{oN9fAfD#v@0NbSOqiMkFg3RO?|5n0~ZV&(*Mm5WmCf`z+rJI6o65}pi)%u zKim*Ipny}NsF(%i6(Dk9^BwBInB(~L;@*P=KnI7sC^!n70XBE;ZIs)a>CNkZ;HWlH zGijFDjiFMs-UeHz>8t~mZhK0SO=KRu>a9-DW;7JkygzD~WT^6Zb%l|L@^J2#MS{(<%Pz(TGY@=>=*0tw(+N*~e7f3N&8k?IkvjJ!!*^o5bXLCm`Ry zh)nG@Z}?rL-+~@rTCs5>K9SbS^$}wmHohLW55FIILS4?IXODx~>zSg%NXZCN_DiAdq@NUv%- zR7}=NzT8@&Ty0Xx$aq7=Ov~;Cu8IM%^ZJ%yR9_TgI~ptX1(r^U>sH}bycjU1Erx1?DN+|DUtIh;pwrS% zHt96Gg`uw=?&QAP_4d5nnmxLuD_ddreLI0DXbNgV&ZfSB|1>fn8e!-yxx&|O*LGTF z_HcjTk!7{2WO!tGcVzGuZHs79zRNGg>*v!4oXmu&)Y8p}j@~$}vzNflq9V;tEi_5i zQ_17Sw9~p@Jf?JIjoTw)Z}h)Bc>t!hkI+9yBHf$}L+^{e^P5b z9L%`+n8fBHFcID-%4B9v7jyjXXQUU+k(C$C-YuI8T;tkm8~3u;S}73px;bnVWdXaD zRP7-~czH)^Lzi@PRL{{)%izp3Z1V{B(%?w4Hgj&gXa=dZ2rc4Yx3o*$dAA$uW7=>o zj9ws?IlpM9p(Uu2p%JaD?Xwoh9VeY*_4V$8xmAmG&+dL^=C{7(_;dD?kI&pcZ*0@n zu=KOk%g?fkP}1}KhAEl5)uioq+mFFac5}PJbR)lHVi{@Wx=bPe3OKoD3;*f!g--!G$WbM7xURU>#GHk>Jtq~RQMUAfVdJZ$(m{fBs*`eoPm70hB` zQRvGfc*Z-qKfL0SxMx1b=XF{CL|L-Z1!AhD<~uKyL`d3USr8bkx}yNPz7JC8ml$m#RJDH|ZQh!29d+ z(X}l)^fA8FL5 z3#gq*u6ob>rux_xgkmd?(u!^csw>6ne?%o6fLmsw`6Ju0HT5kBj!8DJ8Ozcltb9b7 z2EI;V9?P{fUg*jG`IK??_>u^nL$Ieo$8-PPr;|QSc$udKKKAeLYqs}j6*ngsqMMz} z6|-2PX5nA1g}K&mx)jY^SPzu=$drone>QSFecUSgOR49J8_b;RmWxa_IS zfYi~C-?i0L3g5tB##=dxEsf=jfg-wATvMMo?r9|&M;XkxQ!F?3ON~UW1@-7W5J4LE z+AR^cB3wE(Q9=3kF7FFPsFXm=7XhTW>|}VXw}D< zkW!2?if{R5s9wL8<3_Rw!mVpK4xg0x8SbnzQ6cwX{p8dH{x^kaVeVJ`s-Xu*;|=1< z!wr5B`}ohu?@u9FRUiKIK9;=LH@;kAEt0-2vi>ZGz*e$b z3xj>oOS!UETFbaazn<@SZIAO3>T}j?bot5Vw(JXuQ4%=z84^|{7GF8PA*!|LXI-*& zHmD@4YEE)ruKfjjTHmlO%v)gVByXBFA=s$Fz=l2~zI{&gl-NkyopX+>3IsB4LQwMY z!YA&f7*xkZvtfU5!ZB|aDNI8OMRpNPO(Re(mYu8c)Lo3B$Y^*{KwqQ*gbk^TVt6*mX-gCq@hULZK-J8xyQuw^M9v`5y04@;QZfNCa(~ zWIH+Q|8R4e_o^*dlZJ9u75AFOHirjm^Po+t0h%QCg3eN##l>$XE<<1WXIHC;_+=AQ z7SdClL3t%HA8!XNKX3H3ZZXZyWRJH?Xx#i>Tu1TblB#P_wp}P!(pQnC?NQ1!WlvU% zz+R#L*Z79{i$~woEICwU!MZipyA5zVH4O5~qE~#go#eXV1ljporL$7VaMPRLHII4$ z$r>A0{SPb)4Inmkiq8n4!ykQ5P5(}~npSY{f0(05hja9Ny2`2%BV`&v;zi1U)4sgJ zSn7r^DO8%I^<_0m=rWd2?a=upJ$uEe#d5vLA1ywD0b^(qmbd5Vb~LlZ^EZn|cE_dRYQY;9{b{ z3k<7F*)YcsDtJXMI}E)l6!adU?CGohhyOm08>g zZdO4{q-ADEvlN%gp0xF@oOJ*6&I&x=3nNfI<7sPEe@jWailMab!BhRYAJr~V^CEn% zO{*KdE15;Y{cl)iJrx+DSU1Y?M*`Fz-}!(TbG_jKbVMZoc{eWqbF%rLeZ=nfo{2d} zWRq-3AWEV4^dvvO98T9q;~L#FP>BdFKO4VQZz?1mBlt30l8@ zxipuWm{9-C)N)KAB!0HhOU26D-Y?mT==+kmOWrQb3uRaKC9M>w(lHgzn{u>a_C2pA zohJ8h!8DF!$T0HJm@iV?EqEeH|DHkDRNSZeEv%iYb;R_;DPMWI*eny9niDa>LF_)J z)zoWdH^jRoOVXToUM4+hWoP$(gssE)~(=CkpK~o7^!m@tJ?K@7{Uhas4dX=sHbK zu;lUa?$YIMru4<|pS3j^wgtz_sXAR1ZxM?lnVQ{?y+8{8mP? \ No newline at end of file diff --git a/steps/03.01-server-components-solution/src/app/(auth)/layout.tsx b/steps/03.01-server-components-solution/src/app/(auth)/layout.tsx new file mode 100644 index 0000000..cac31a7 --- /dev/null +++ b/steps/03.01-server-components-solution/src/app/(auth)/layout.tsx @@ -0,0 +1,12 @@ +type AuthLayoutProps = { + children: React.ReactNode; +}; + +const AuthLayout: React.FC = ({ children }) => ( +
+
+
{children}
+
+); + +export default AuthLayout; diff --git a/steps/03.01-server-components-solution/src/app/(auth)/login/page.tsx b/steps/03.01-server-components-solution/src/app/(auth)/login/page.tsx new file mode 100644 index 0000000..3ae0596 --- /dev/null +++ b/steps/03.01-server-components-solution/src/app/(auth)/login/page.tsx @@ -0,0 +1,30 @@ +import { Metadata } from 'next'; + +import TextField from '@/components/TextField'; +import Button from '@/components/Button'; + +export const metadata: Metadata = { + title: 'SFEIR People | Login', +}; + +const LoginPage = () => { + return ( +
+

Welcome !

+ + + + + ); +}; + +export default LoginPage; diff --git a/steps/03.01-server-components-solution/src/app/(dashboard)/employees/[id]/edit/page.tsx b/steps/03.01-server-components-solution/src/app/(dashboard)/employees/[id]/edit/page.tsx new file mode 100644 index 0000000..9d29dc0 --- /dev/null +++ b/steps/03.01-server-components-solution/src/app/(dashboard)/employees/[id]/edit/page.tsx @@ -0,0 +1,24 @@ +import EmployeeForm from '@/components/EmployeeForm'; +import PageTitle from '@/components/PageTitle'; + +import employeesData from '@/data/employees.json'; + +const EmployeeDetail = async ({ params }: { params: { id: string } }) => { + const employee = employeesData.find((employee) => employee.id === params.id); + + if (!employee) return Single Employee - Not found; + + return ( + <> + + Single Employee - {employee.firstname} {employee.lastname} | Edit + + +
+ +
+ + ); +}; + +export default EmployeeDetail; diff --git a/steps/03.01-server-components-solution/src/app/(dashboard)/employees/[id]/page.tsx b/steps/03.01-server-components-solution/src/app/(dashboard)/employees/[id]/page.tsx new file mode 100644 index 0000000..a498c63 --- /dev/null +++ b/steps/03.01-server-components-solution/src/app/(dashboard)/employees/[id]/page.tsx @@ -0,0 +1,21 @@ +import PageTitle from '@/components/PageTitle'; +import PersonCard from '@/components/PersonCard'; + +import employeesData from '@/data/employees.json'; + +const EmployeeDetail = async ({ params }: { params: { id: string } }) => { + const employee = employeesData.find((employee) => employee.id === params.id); + + if (!employee) return Single Employee - Not found; + + return ( + <> + + Single Employee - {employee.firstname} {employee.lastname} + + + + ); +}; + +export default EmployeeDetail; diff --git a/steps/03.01-server-components-solution/src/app/(dashboard)/employees/log/page.tsx b/steps/03.01-server-components-solution/src/app/(dashboard)/employees/log/page.tsx new file mode 100644 index 0000000..c68e088 --- /dev/null +++ b/steps/03.01-server-components-solution/src/app/(dashboard)/employees/log/page.tsx @@ -0,0 +1,16 @@ +import { Code } from 'bright'; + +import { promises as fs } from 'fs'; +import path from 'path'; + +const Logs = async () => { + const logsFile = path.resolve(process.cwd(), 'logs.txt'); + try { + const logs = await fs.readFile(logsFile, 'utf8'); + return {logs}; + } catch (err) { + return 'No logs file found :/'; + } +}; + +export default Logs; diff --git a/steps/03.01-server-components-solution/src/app/(dashboard)/employees/new/page.tsx b/steps/03.01-server-components-solution/src/app/(dashboard)/employees/new/page.tsx new file mode 100644 index 0000000..f1e5157 --- /dev/null +++ b/steps/03.01-server-components-solution/src/app/(dashboard)/employees/new/page.tsx @@ -0,0 +1,18 @@ +import EmployeeForm from '@/components/EmployeeForm'; +import PageTitle from '@/components/PageTitle'; + +const EmployeeDetail = async () => { + return ( + <> + + Employees | Create + + +
+ +
+ + ); +}; + +export default EmployeeDetail; diff --git a/steps/03.01-server-components-solution/src/app/(dashboard)/employees/page.tsx b/steps/03.01-server-components-solution/src/app/(dashboard)/employees/page.tsx new file mode 100644 index 0000000..f14ba81 --- /dev/null +++ b/steps/03.01-server-components-solution/src/app/(dashboard)/employees/page.tsx @@ -0,0 +1,59 @@ +import Link from 'next/link'; + +import { promises as fs } from 'fs'; +import path from 'path'; + +import Button from '@/components/Button'; +import PageTitle from '@/components/PageTitle'; +import PersonCard from '@/components/PersonCard'; +import Search from '@/components/Search'; + +import employeesData from '@/data/employees.json'; + +const Employees = async ({ searchParams }: { searchParams: { search?: string } }) => { + const search = searchParams.search || ''; + const employees = employeesData.filter((employee) => + `${employee.firstname} ${employee.lastname}`.toLowerCase().includes(search.toLowerCase()) + ); + + const trackingObject = { + date: Date.now().toString(), + search: searchParams.search || '', + results: employees.length, + }; + + const logFilePath = path.join(process.cwd(), 'logs.txt'); + await fs.appendFile(logFilePath, JSON.stringify(trackingObject) + '\n'); + + return ( +
+ Employees +
+ + +
+
+ {employees?.map((employee) => ( + + + +
+ } + /> + ))} +
+ + ); +}; + +export default Employees; diff --git a/steps/03.01-server-components-solution/src/app/(dashboard)/expenses/[id]/page.tsx b/steps/03.01-server-components-solution/src/app/(dashboard)/expenses/[id]/page.tsx new file mode 100644 index 0000000..bda58e2 --- /dev/null +++ b/steps/03.01-server-components-solution/src/app/(dashboard)/expenses/[id]/page.tsx @@ -0,0 +1,18 @@ +import ExpenseDetails from '@/components/ExpensesDetails'; +import PageTitle from '@/components/PageTitle'; + +import expensesData from '@/data/expenses.json'; +import { Expense } from '@/types'; + +const SingleExpense = ({ params }: { params: { id: string } }) => { + const expense = expensesData.find((expense) => expense.id === params.id); + + return ( + <> + Single Expense - {expense?.label || 'Not found'} + {expense && } + + ); +}; + +export default SingleExpense; diff --git a/steps/03.01-server-components-solution/src/app/(dashboard)/expenses/page.tsx b/steps/03.01-server-components-solution/src/app/(dashboard)/expenses/page.tsx new file mode 100644 index 0000000..55d1d65 --- /dev/null +++ b/steps/03.01-server-components-solution/src/app/(dashboard)/expenses/page.tsx @@ -0,0 +1,17 @@ +import ExpensesTable from '@/components/ExpensesTable'; +import PageTitle from '@/components/PageTitle'; + +import { Expense } from '@/types'; + +import expensesData from '@/data/expenses.json'; + +const Expenses = async () => { + return ( + <> + Expenses + } /> + + ); +}; + +export default Expenses; diff --git a/steps/03.01-server-components-solution/src/app/(dashboard)/layout.tsx b/steps/03.01-server-components-solution/src/app/(dashboard)/layout.tsx new file mode 100644 index 0000000..f6e4708 --- /dev/null +++ b/steps/03.01-server-components-solution/src/app/(dashboard)/layout.tsx @@ -0,0 +1,37 @@ +import { Metadata } from 'next'; +import Link from 'next/link'; +import Image from 'next/image'; + +import { promises as fs } from 'fs'; +import path from 'path'; + +import NavigationMenu from '@/components/NavigationMenu'; + +import logo from '@/assets/svg/logo.svg'; + +type DashboardLayoutProps = { children: React.ReactNode }; + +export const metadata: Metadata = { + title: 'SFEIR People | Dashboard', +}; + +const DashboardLayout: React.FC = async ({ children }) => { + const packageJsonPath = path.join(process.cwd(), 'package.json'); + const packageJsonContent = await fs.readFile(packageJsonPath, 'utf-8'); + const packageJson = JSON.parse(packageJsonContent); + + return ( +
+
+ + People logo + + +
Version: {packageJson.version}
+
+
{children}
+
+ ); +}; + +export default DashboardLayout; diff --git a/steps/03.01-server-components-solution/src/app/(dashboard)/page.tsx b/steps/03.01-server-components-solution/src/app/(dashboard)/page.tsx new file mode 100644 index 0000000..f581ebb --- /dev/null +++ b/steps/03.01-server-components-solution/src/app/(dashboard)/page.tsx @@ -0,0 +1,11 @@ +import PageTitle from '@/components/PageTitle'; + +const HomePage = () => { + return ( + <> + SFEIR People + + ); +}; + +export default HomePage; diff --git a/steps/03.01-server-components-solution/src/app/layout.tsx b/steps/03.01-server-components-solution/src/app/layout.tsx new file mode 100644 index 0000000..e7d90e9 --- /dev/null +++ b/steps/03.01-server-components-solution/src/app/layout.tsx @@ -0,0 +1,21 @@ +import type { Metadata } from 'next'; +import { Inter } from 'next/font/google'; + +const inter = Inter({ subsets: ['latin'] }); + +import '@/styles/global.css'; + +export const metadata: Metadata = { + title: 'SFEIR People', + description: 'SFEIR People dashboard application', +}; + +const RootLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => { + return ( + + {children} + + ); +}; + +export default RootLayout; diff --git a/steps/03.01-server-components-solution/src/assets/images/profile-placeholder.jpg b/steps/03.01-server-components-solution/src/assets/images/profile-placeholder.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6fa00ea6c9e371e542006bb4bd69ae5e6922a324 GIT binary patch literal 11940 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 m>aIBzX~|65y0*1u%UVegt+JNI)Z4`imH;jI + + + + + + + + diff --git a/steps/03.01-server-components-solution/src/assets/svg/logoDark.svg b/steps/03.01-server-components-solution/src/assets/svg/logoDark.svg new file mode 100644 index 0000000..06eb6ed --- /dev/null +++ b/steps/03.01-server-components-solution/src/assets/svg/logoDark.svg @@ -0,0 +1,28 @@ + + + + + + + + + diff --git a/steps/03.01-server-components-solution/src/components/Alert.tsx b/steps/03.01-server-components-solution/src/components/Alert.tsx new file mode 100644 index 0000000..1c90895 --- /dev/null +++ b/steps/03.01-server-components-solution/src/components/Alert.tsx @@ -0,0 +1,17 @@ +import clsx from 'clsx'; + +type AlertProps = { + children: React.ReactNode; + className?: string; +}; + +const Alert: React.FC = ({ children, className }) => ( +
+ {children} +
+); + +export default Alert; diff --git a/steps/03.01-server-components-solution/src/components/Button.tsx b/steps/03.01-server-components-solution/src/components/Button.tsx new file mode 100644 index 0000000..2cee623 --- /dev/null +++ b/steps/03.01-server-components-solution/src/components/Button.tsx @@ -0,0 +1,34 @@ +import clsx from 'clsx'; + +export type ButtonProps = { + children: React.ReactNode; + className?: string; + variant?: 'primary' | 'secondary'; + component?: C; +} & Omit, 'className' | 'variant'>; + +const classNames = { + primary: 'inline-block text-white bg-blue-700 hover:bg-blue-800 font-medium rounded-lg text-sm px-5 py-2.5', + secondary: [ + 'inline-block py-2.5 px-5 text-sm font-medium text-slate-900 bg-white rounded-lg border border-gray-200', + 'hover:bg-gray-100 hover:text-blue-700', + 'dark:bg-slate-900 dark:text-white dark:hover:bg-slate-950 dark:hover:text-blue-200 dark:hover:border-blue-200', + ].join(' '), +}; + +const Button = ({ + children, + className, + variant = 'secondary', + component, + ...restProps +}: ButtonProps) => { + const Component = component || 'button'; + return ( + + {children} + + ); +}; + +export default Button; diff --git a/steps/03.01-server-components-solution/src/components/EmployeeForm.tsx b/steps/03.01-server-components-solution/src/components/EmployeeForm.tsx new file mode 100644 index 0000000..dc15055 --- /dev/null +++ b/steps/03.01-server-components-solution/src/components/EmployeeForm.tsx @@ -0,0 +1,113 @@ +'use client'; + +import Image from 'next/image'; +import TextField from '@/components/TextField'; +import { Person } from '@/types'; +import { useFormState } from 'react-dom'; + +import placeholderImage from '@/assets/images/profile-placeholder.jpg'; +import Button from './Button'; + +type ActionState = { + validationErrors?: { [key: string]: Array }; +}; + +type Action = (id: string, formData: FormData) => Promise; + +type EmployeeFormProps = { + employee?: Person; + action?: Action; + className?: string; +}; + +const initialState = { + validationErrors: {}, +} as ActionState; + +const EmployeeForm: React.FC = ({ employee, action, className }) => { + // @ts-ignore + const [state, formAction] = useFormState(action, initialState as unknown as void); + + return ( +
+
+ {employee +
+
+
+ + + + + +
+
+ + + +
+
+
+ +
+
+ ); +}; + +export default EmployeeForm; diff --git a/steps/03.01-server-components-solution/src/components/ExpensesDetails.tsx b/steps/03.01-server-components-solution/src/components/ExpensesDetails.tsx new file mode 100644 index 0000000..f59b51a --- /dev/null +++ b/steps/03.01-server-components-solution/src/components/ExpensesDetails.tsx @@ -0,0 +1,56 @@ +import { Expense } from '@/types'; +import Paper from './Paper'; + +type ExpenseDetailsRowProps = { + label: string; + value: string; +}; + +const ExpenseDetailsRow: React.FC = ({ label, value }) => ( +
+ {label} + {value} +
+); + +type ExpenseDetailsProps = { + expense: Expense; +}; + +const ExpenseDetails: React.FC = ({ expense }) => ( + <> +
+
+

Information

+ + + + + +
+
+

Workflow

+ + + + + +
+
+
+
+

Amount

+ + + + + +
+
+ +); + +export default ExpenseDetails; diff --git a/steps/03.01-server-components-solution/src/components/ExpensesTable.tsx b/steps/03.01-server-components-solution/src/components/ExpensesTable.tsx new file mode 100644 index 0000000..1b2e240 --- /dev/null +++ b/steps/03.01-server-components-solution/src/components/ExpensesTable.tsx @@ -0,0 +1,61 @@ +'use client'; + +import { Expense } from '@/types'; +import clsx from 'clsx'; +import { useRouter } from 'next/navigation'; + +type ExpensesTableProps = { + expenses: Array; +}; + +const ExpensesTable: React.FC = ({ expenses }) => { + const router = useRouter(); + + const handleClick = (expenseId: string) => () => { + router.push(`/expenses/${expenseId}`); + }; + + return ( + + + + + + + + + + + {expenses.map((expense, index) => ( + + + + + + + ))} + +
+ Label + + Creation date + + Category + + Price +
{expense.label}{new Date(expense.creationDate).toLocaleDateString()}{expense.category} + {expense.price.priceIncludingTax} {expense.price.currency} +
+ ); +}; + +export default ExpensesTable; diff --git a/steps/03.01-server-components-solution/src/components/Icons/ArrowLeft.tsx b/steps/03.01-server-components-solution/src/components/Icons/ArrowLeft.tsx new file mode 100644 index 0000000..1cc10c7 --- /dev/null +++ b/steps/03.01-server-components-solution/src/components/Icons/ArrowLeft.tsx @@ -0,0 +1,25 @@ +type ArrowLeftProps = { + className?: string; +}; + +const ArrowLeft: React.FC = ({ className }) => ( + +); + +export default ArrowLeft; diff --git a/steps/03.01-server-components-solution/src/components/Icons/Eye.tsx b/steps/03.01-server-components-solution/src/components/Icons/Eye.tsx new file mode 100644 index 0000000..04beb91 --- /dev/null +++ b/steps/03.01-server-components-solution/src/components/Icons/Eye.tsx @@ -0,0 +1,11 @@ +type EyeProps = { + className?: string; +}; + +const Eye: React.FC = ({ className }) => ( + + + +); + +export default Eye; diff --git a/steps/03.01-server-components-solution/src/components/Icons/Loader.tsx b/steps/03.01-server-components-solution/src/components/Icons/Loader.tsx new file mode 100644 index 0000000..9c81994 --- /dev/null +++ b/steps/03.01-server-components-solution/src/components/Icons/Loader.tsx @@ -0,0 +1,25 @@ +type LoaderProps = { + className?: string; +}; + +const Loader: React.FC = ({ className }) => ( + +); + +export default Loader; diff --git a/steps/03.01-server-components-solution/src/components/NavigationItem.tsx b/steps/03.01-server-components-solution/src/components/NavigationItem.tsx new file mode 100644 index 0000000..9f68e10 --- /dev/null +++ b/steps/03.01-server-components-solution/src/components/NavigationItem.tsx @@ -0,0 +1,30 @@ +'use client'; + +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; + +import clsx from 'clsx'; + +type NavigationItemsProps = { + href: string; + children: React.ReactNode; +}; + +const NavigationItem: React.FC = ({ href, children }) => { + const pathname = usePathname(); + + return ( + + {children} + + ); +}; + +export default NavigationItem; diff --git a/steps/03.01-server-components-solution/src/components/NavigationMenu.tsx b/steps/03.01-server-components-solution/src/components/NavigationMenu.tsx new file mode 100644 index 0000000..4b778c6 --- /dev/null +++ b/steps/03.01-server-components-solution/src/components/NavigationMenu.tsx @@ -0,0 +1,21 @@ +import NavigationItem from './NavigationItem'; + +const NavigationMenu = () => { + return ( + + ); +}; + +export default NavigationMenu; diff --git a/steps/03.01-server-components-solution/src/components/PageTitle.tsx b/steps/03.01-server-components-solution/src/components/PageTitle.tsx new file mode 100644 index 0000000..217fe69 --- /dev/null +++ b/steps/03.01-server-components-solution/src/components/PageTitle.tsx @@ -0,0 +1,25 @@ +import Link from 'next/link'; + +import ArrowLeft from './Icons/ArrowLeft'; + +type PageTitleProps = { + children: React.ReactNode; + backHref?: string; +}; + +const PageTitle: React.FC = ({ children, backHref }) => ( +
+ {backHref && ( + + + Go back + + )} +

{children}

+
+); + +export default PageTitle; diff --git a/steps/03.01-server-components-solution/src/components/Pagination.tsx b/steps/03.01-server-components-solution/src/components/Pagination.tsx new file mode 100644 index 0000000..be9a43a --- /dev/null +++ b/steps/03.01-server-components-solution/src/components/Pagination.tsx @@ -0,0 +1,95 @@ +'use client'; + +import clsx from 'clsx'; +import Link from 'next/link'; +import { usePathname, useSearchParams } from 'next/navigation'; + +type PaginationProps = { + totalPages: number; + className?: string; +}; + +type PaginationShortcutProps = { + href: string; + disabled?: boolean; + className?: string; + children: React.ReactNode; +}; + +const PaginationShortcut: React.FC = ({ href, disabled, className, children }) => { + const classNames = clsx( + 'block text-center px-3 py-2 ms-0 border bg-white dark:bg-slate-900', + className, + !disabled && + 'hover:bg-gray-100 hover:text-gray-700 text-gray-500 border-gray-300 dark:text-white dark:border-gray-700 dark:hover:bg-slate-950 dark:hover:text-white', + disabled && 'text-gray-300 border-gray-200 dark:text-gray-500 dark:border-gray-600' + ); + + if (disabled) return
{children}
; + + return ( + + {children} + + ); +}; + +const Pagination: React.FC = ({ totalPages, className }) => { + const params = useSearchParams(); + const pathname = usePathname(); + + const currentPage = Number(params.get('page')) || 1; + + const getPageUrl = (page: number): string => { + const newParams = new URLSearchParams(params); + newParams.set('page', page.toString()); + return `${pathname}?${newParams.toString()}`; + }; + + return ( + + ); +}; + +export default Pagination; diff --git a/steps/03.01-server-components-solution/src/components/Paper.tsx b/steps/03.01-server-components-solution/src/components/Paper.tsx new file mode 100644 index 0000000..8ba656e --- /dev/null +++ b/steps/03.01-server-components-solution/src/components/Paper.tsx @@ -0,0 +1,14 @@ +import clsx from 'clsx'; + +type PaperProps = React.HTMLAttributes & { + children: React.ReactNode; + rounded?: boolean; +}; + +const Paper: React.FC = ({ children, rounded = true, ...restProps }) => ( +
+ {children} +
+); + +export default Paper; diff --git a/steps/03.01-server-components-solution/src/components/PersonCard.tsx b/steps/03.01-server-components-solution/src/components/PersonCard.tsx new file mode 100644 index 0000000..b38ee53 --- /dev/null +++ b/steps/03.01-server-components-solution/src/components/PersonCard.tsx @@ -0,0 +1,43 @@ +import Image from 'next/image'; + +import { Person } from '@/types'; + +import placeholderImage from '@/assets/images/profile-placeholder.jpg'; + +type PersonCardProps = React.HTMLAttributes & { + person: Person; + actions?: React.ReactNode; + compact?: boolean; +}; + +const PersonCard: React.FC = ({ person, actions, className, compact = false }) => { + return ( +
+
+ {`Picture + + {person.firstname} {person.lastname} + + {person.position} +
+ + {!compact && ( +
+ {person.phone} + {person.email} + {person.manager && {person.manager}} +
+ )} + + {actions &&
{actions}
} +
+ ); +}; + +export default PersonCard; diff --git a/steps/03.01-server-components-solution/src/components/Search.tsx b/steps/03.01-server-components-solution/src/components/Search.tsx new file mode 100644 index 0000000..98fe032 --- /dev/null +++ b/steps/03.01-server-components-solution/src/components/Search.tsx @@ -0,0 +1,43 @@ +'use client'; + +import { debounce } from '@/functions/timing'; +import clsx from 'clsx'; +import { usePathname, useRouter, useSearchParams } from 'next/navigation'; + +const Search = ({ ...restProps }) => { + const router = useRouter(); + const params = useSearchParams(); + const pathname = usePathname(); + + const handleChange = debounce((event: React.ChangeEvent) => { + const value = event.target?.value; + const newParams = new URLSearchParams(params); + newParams.delete('page'); + if (value) newParams.set('search', value); + else newParams.delete('search'); + router.replace(`${pathname}?${newParams.toString()}`); + }, 200); + + return ( + <> + + + + ); +}; + +export default Search; diff --git a/steps/03.01-server-components-solution/src/components/TextField.tsx b/steps/03.01-server-components-solution/src/components/TextField.tsx new file mode 100644 index 0000000..d8061e9 --- /dev/null +++ b/steps/03.01-server-components-solution/src/components/TextField.tsx @@ -0,0 +1,31 @@ +import clsx from 'clsx'; + +type TextFieldProps = React.InputHTMLAttributes & { + label: string; + id: string; + type?: string; + className?: string; + errorMessages?: Array; +}; + +const TextField: React.FC = ({ label, id, type = 'text', className, errorMessages, ...restProps }) => { + return ( +
+ + + {errorMessages?.length &&

{errorMessages[0]}

} +
+ ); +}; + +export default TextField; diff --git a/steps/03.01-server-components-solution/src/data/employees.json b/steps/03.01-server-components-solution/src/data/employees.json new file mode 100644 index 0000000..5f5342e --- /dev/null +++ b/steps/03.01-server-components-solution/src/data/employees.json @@ -0,0 +1,152 @@ +[ + { + "id": "5763cd4d9d2a4f259b53c901", + "photo": "/portraits/women/85.jpg", + "firstname": "Leanne", + "lastname": "Woodard", + "position": "Developer", + "entryDate": "27/10/2015", + "birthDate": "02/01/1974", + "gender": "f", + "email": "woodard.l@acme.com", + "phone": "0784112248", + "isManager": false, + "manager": "Erika", + "managerId": "5763cd4d3b57c672861bfa1f" + }, + { + "id": "5763cd4d51fdb6588742f99e", + "photo": "/portraits/men/56.jpg", + "firstname": "Castaneda", + "lastname": "Salinas", + "position": "Developer", + "entryDate": "04/10/2015", + "birthDate": "22/01/1963", + "gender": "m", + "email": "salinas.c@acme.com", + "phone": "0145652522", + "isManager": false, + "manager": "Erika", + "managerId": "5763cd4d3b57c672861bfa1f" + }, + { + "id": "5763cd4dba6362a3f92c954e", + "photo": "/portraits/women/24.jpg", + "firstname": "Phyllis", + "lastname": "Donovan", + "position": "Sales", + "entryDate": "30/03/2015", + "birthDate": "30/11/1951", + "gender": "f", + "email": "donovan.p@acme.com", + "phone": "0685230125", + "isManager": false, + "manager": "Erika", + "managerId": "5763cd4d3b57c672861bfa1f" + }, + { + "id": "5763cd4d3b57c672861bfa1f", + "photo": "/portraits/women/65.jpg", + "firstname": "Erika", + "lastname": "Guzman", + "position": "Product Owner", + "entryDate": "13/05/2016", + "birthDate": "19/03/1962", + "gender": "f", + "email": "guzman.e@acme.com", + "phone": "0678412587", + "isManager": true, + "manager": "Mercedes", + "managerId": "5763cd4d979b62a209809160" + }, + { + "id": "5763cd4d5fc36e4f842ca5a9", + "photo": "/portraits/men/30.jpg", + "firstname": "Moody", + "lastname": "Prince", + "position": "Developer", + "entryDate": "28/09/2015", + "birthDate": "15/04/1971", + "gender": "m", + "email": "prince.m@acme.com", + "phone": "0662589632", + "isManager": false, + "manager": "Mercedes", + "managerId": "5763cd4d979b62a209809160" + }, + { + "id": "5763cd4d979b62a209809160", + "photo": "/portraits/women/8.jpg", + "firstname": "Mercedes", + "lastname": "Hebert", + "position": "Product Owner", + "entryDate": "02/01/2016", + "birthDate": "20/07/1947", + "gender": "f", + "email": "hebert.m@acme.com", + "phone": "0125878522", + "isManager": true, + "manager": "Mclaughlin", + "managerId": "5763cd4dfa6f96cd26c65787" + }, + { + "id": "5763cd4d15e6c2c28b70f2e8", + "photo": "/portraits/men/86.jpg", + "firstname": "Howell", + "lastname": "Mcknight", + "position": "Sales", + "entryDate": "26/09/2015", + "birthDate": "18/07/1979", + "gender": "m", + "email": "mcknight.h@acme.com", + "phone": "0456987425", + "isManager": false, + "manager": "Mclaughlin", + "managerId": "5763cd4dfa6f96cd26c65787" + }, + { + "id": "5763cd4d5d6ad8dfc6c34883", + "photo": "/portraits/women/93.jpg", + "firstname": "Lizzie", + "lastname": "Morris", + "position": "Human Resources", + "entryDate": "03/05/2016", + "birthDate": "15/11/1981", + "gender": "f", + "email": "morris.l@acme.com", + "phone": "0662259988", + "isManager": false, + "manager": "Mclaughlin", + "managerId": "5763cd4dfa6f96cd26c65787" + }, + { + "id": "5763cd4dc378a38ecd387737", + "photo": "/portraits/men/34.jpg", + "firstname": "Roy", + "lastname": "Nielsen", + "position": "Sales", + "entryDate": "17/05/2016", + "birthDate": "21/10/1951", + "gender": "m", + "email": "nielsen.r@acme.com", + "phone": "0755669551", + "isManager": false, + "manager": "Mclaughlin", + "managerId": "5763cd4dfa6f96cd26c65787" + }, + { + "id": "5763cd4dfa6f96cd26c65787", + "photo": "/portraits/men/78.jpg", + "firstname": "Mclaughlin", + "lastname": "Cochran", + "position": "Director", + "entryDate": "11/04/2016", + "birthDate": "19/03/1973", + "gender": "m", + "email": "cochran.m@acme.com", + "phone": "0266334856", + "isManager": true, + "manager": "", + "managerId": "" + } +] diff --git a/steps/03.01-server-components-solution/src/data/expenses.json b/steps/03.01-server-components-solution/src/data/expenses.json new file mode 100644 index 0000000..0d096dc --- /dev/null +++ b/steps/03.01-server-components-solution/src/data/expenses.json @@ -0,0 +1,342 @@ +[ + { + "id": "0475830f-a563-44e0-8c5c-6d829c11a132", + "employeeId": "5763cd4d51fdb6588742f99e", + "price": { + "priceIncludingTax": 120.50, + "taxAmount": 20.50, + "priceExcludingTax": 100, + "currency": "EUR" + }, + "label": "Business Lunch", + "description": "Lunch with a client to discuss a new project.", + "category": "Meals", + "receiptLink": "https://example.com/receipt1.pdf", + "status": "approved", + "creationDate": "2024-03-15T09:30:00Z", + "updateDate": "2024-03-18T14:45:00Z" + }, + { + "id": "a2e8b2c4-99d8-4c13-9a9c-0a8a68623260", + "employeeId": "5763cd4d51fdb6588742f99e", + "price": { + "priceIncludingTax": 55.20, + "taxAmount": 7.20, + "priceExcludingTax": 48, + "currency": "USD" + }, + "label": "Office Supplies", + "description": "Purchase of paper, pens, etc.", + "category": "Supplies", + "receiptLink": "https://example.com/receipt2.jpg", + "status": "created", + "creationDate": "2024-05-02T11:20:00Z", + "updateDate": "2024-05-02T11:20:00Z" + }, + { + "id": "3d3fb561-0d9c-4021-8285-d2f49c40c47d", + "employeeId": "5763cd4d51fdb6588742f99e", + "price": { + "priceIncludingTax": 250, + "taxAmount": 0, + "priceExcludingTax": 250, + "currency": "EUR" + }, + "label": "Plane Ticket", + "description": "Business trip to Berlin.", + "category": "Travel", + "receiptLink": "https://example.com/receipt3.png", + "status": "declined", + "creationDate": "2024-04-10T16:45:00Z", + "updateDate": "2024-04-12T09:30:00Z" + }, + { + "id": "4c41689c-e5f5-4029-a181-38c498e7a82b", + "employeeId": "5763cd4d5fc36e4f842ca5a9", + "price": { + "priceIncludingTax": 80.00, + "taxAmount": 13.33, + "priceExcludingTax": 66.67, + "currency": "USD" + }, + "label": "Hotel", + "description": "Hotel night in London.", + "category": "Accommodation", + "receiptLink": "https://example.com/receipt4.pdf", + "status": "approved", + "creationDate": "2024-06-20T08:15:00Z", + "updateDate": "2024-06-22T10:30:00Z" + }, + { + "id": "871030e0-e485-41d5-882c-30201429a23f", + "employeeId": "5763cd4d5fc36e4f842ca5a9", + "price": { + "priceIncludingTax": 35.75, + "taxAmount": 5.75, + "priceExcludingTax": 30, + "currency": "EUR" + }, + "label": "Taxi Fare", + "description": "Ride from the airport to the office.", + "category": "Transportation", + "receiptLink": "https://example.com/receipt5.jpg", + "status": "submitted", + "creationDate": "2024-07-05T19:00:00Z", + "updateDate": "2024-07-05T19:00:00Z" + }, + { + "id": "e38e7368-8686-4211-a6a2-844806839a4c", + "employeeId": "5763cd4d979b62a209809160", + "price": { + "priceIncludingTax": 180.00, + "taxAmount": 30.00, + "priceExcludingTax": 150, + "currency": "USD" + }, + "label": "Car Rental", + "description": "Car rental for a week.", + "category": "Transportation", + "receiptLink": "https://example.com/receipt6.png", + "status": "approved", + "creationDate": "2024-02-28T13:40:00Z", + "updateDate": "2024-03-02T11:15:00Z" + }, + { + "id": "90432a7b-3009-491a-832b-a4622721a490", + "employeeId": "5763cd4d15e6c2c28b70f2e8", + "price": { + "priceIncludingTax": 65.00, + "taxAmount": 10.83, + "priceExcludingTax": 54.17, + "currency": "USD" + }, + "label": "Client Meal", + "description": "Dinner with a potential client.", + "category": "Meals", + "receiptLink": "https://example.com/receipt7.pdf", + "status": "in_review", + "creationDate": "2024-07-18T20:30:00Z", + "updateDate": "2024-07-19T09:00:00Z" + }, + { + "id": "a88a026a-e928-48b6-8a1d-90d980e7a423", + "employeeId": "5763cd4d5d6ad8dfc6c34883", + "price": { + "priceIncludingTax": 95.99, + "taxAmount": 15.99, + "priceExcludingTax": 80, + "currency": "EUR" + }, + "label": "Software", + "description": "Monthly subscription to project management software.", + "category": "Software", + "receiptLink": "https://example.com/receipt8.jpg", + "status": "approved", + "creationDate": "2024-05-10T10:00:00Z", + "updateDate": "2024-05-11T14:20:00Z" + }, + { + "id": "21e5615a-9c2a-40f2-a489-0c2f4a866098", + "employeeId": "5763cd4dc378a38ecd387737", + "price": { + "priceIncludingTax": 25.50, + "taxAmount": 4.25, + "priceExcludingTax": 21.25, + "currency": "USD" + }, + "label": "Parking Fees", + "description": "Parking fees at the airport.", + "category": "Transportation", + "receiptLink": "https://example.com/receipt9.png", + "status": "created", + "creationDate": "2024-07-30T17:45:00Z", + "updateDate": "2024-07-30T17:45:00Z" + }, + { + "id": "5200c69a-e387-4292-9282-98e66878524c", + "employeeId": "5763cd4dfa6f96cd26c65787", + "price": { + "priceIncludingTax": 150.00, + "taxAmount": 25.00, + "priceExcludingTax": 125, + "currency": "USD" + }, + "label": "Training", + "description": "Participation in professional training.", + "category": "Training", + "receiptLink": "https://example.com/receipt10.pdf", + "status": "approved", + "creationDate": "2024-04-05T09:00:00Z", + "updateDate": "2024-04-07T11:30:00Z" + }, + { + "id": "9b60a0a2-d2cf-41ab-a8a6-690080e77898", + "employeeId": "5763cd4d9d2a4f259b53c901", + "price": { + "priceIncludingTax": 75.20, + "taxAmount": 12.53, + "priceExcludingTax": 62.67, + "currency": "EUR" + }, + "label": "Office Supplies", + "description": "Purchase of office supplies.", + "category": "Supplies", + "receiptLink": "https://example.com/receipt11.jpg", + "status": "approved", + "creationDate": "2024-06-12T14:20:00Z", + "updateDate": "2024-06-14T10:15:00Z" + }, + { + "id": "8478041f-7a41-46f0-9158-34759c409873", + "employeeId": "5763cd4d51fdb6588742f99e", + "price": { + "priceIncludingTax": 280.00, + "taxAmount": 46.67, + "priceExcludingTax": 233.33, + "currency": "USD" + }, + "label": "Plane Ticket", + "description": "Business trip to New York.", + "category": "Travel", + "receiptLink": "https://example.com/receipt12.png", + "status": "submitted", + "creationDate": "2024-07-25T11:30:00Z", + "updateDate": "2024-07-25T11:30:00Z" + }, + { + "id": "4902854f-2049-4498-9597-74823829501f", + "employeeId": "5763cd4dba6362a3f92c954e", + "price": { + "priceIncludingTax": 95.00, + "taxAmount": 15.83, + "priceExcludingTax": 79.17, + "currency": "EUR" + }, + "label": "Hotel", + "description": "Hotel night in Paris.", + "category": "Accommodation", + "receiptLink": "https://example.com/receipt13.pdf", + "status": "in_review", + "creationDate": "2024-08-01T08:45:00Z", + "updateDate": "2024-08-02T10:00:00Z" + }, + { + "id": "4309573f-8903-4a42-a095-839529490582", + "employeeId": "5763cd4d3b57c672861bfa1f", + "price": { + "priceIncludingTax": 42.50, + "taxAmount": 7.08, + "priceExcludingTax": 35.42, + "currency": "EUR" + }, + "label": "Taxi Fare", + "description": "Ride from the station to the conference venue.", + "category": "Transportation", + "receiptLink": "https://example.com/receipt14.jpg", + "status": "approved", + "creationDate": "2024-05-20T18:30:00Z", + "updateDate": "2024-05-22T09:15:00Z" + }, + { + "id": "3028759f-9023-4a83-a785-928375928375", + "employeeId": "5763cd4d5fc36e4f842ca5a9", + "price": { + "priceIncludingTax": 190.00, + "taxAmount": 31.67, + "priceExcludingTax": 158.33, + "currency": "USD" + }, + "label": "Car Rental", + "description": "Weekend car rental.", + "category": "Transportation", + "receiptLink": "https://example.com/receipt15.png", + "status": "created", + "creationDate": "2024-07-28T12:00:00Z", + "updateDate": "2024-07-28T12:00:00Z" + }, + { + "id": "92837592-8375-9283-7592-837592837592", + "employeeId": "5763cd4d979b62a209809160", + "price": { + "priceIncludingTax": 70.00, + "taxAmount": 11.67, + "priceExcludingTax": 58.33, + "currency": "USD" + }, + "label": "Client Meal", + "description": "Lunch with a client to discuss a project.", + "category": "Meals", + "receiptLink": "https://example.com/receipt16.pdf", + "status": "declined", + "creationDate": "2024-03-08T13:15:00Z", + "updateDate": "2024-03-10T09:30:00Z" + }, + { + "id": "85930583-0385-9305-8303-859305830385", + "employeeId": "5763cd4d15e6c2c28b70f2e8", + "price": { + "priceIncludingTax": 110.50, + "taxAmount": 18.42, + "priceExcludingTax": 92.08, + "currency": "EUR" + }, + "label": "Software", + "description": "Purchase of a license for design software.", + "category": "Software", + "receiptLink": "https://example.com/receipt17.jpg", + "status": "approved", + "creationDate": "2024-06-05T10:45:00Z", + "updateDate": "2024-06-07T14:30:00Z" + }, + { + "id": "03958305-8305-9385-0385-930583059385", + "employeeId": "5763cd4d5d6ad8dfc6c34883", + "price": { + "priceIncludingTax": 35.00, + "taxAmount": 5.83, + "priceExcludingTax": 29.17, + "currency": "USD" + }, + "label": "Parking Fees", + "description": "Parking fees at the conference center.", + "category": "Transportation", + "receiptLink": "https://example.com/receipt18.png", + "status": "submitted", + "creationDate": "2024-07-15T16:20:00Z", + "updateDate": "2024-07-15T16:20:00Z" + }, + { + "id": "93850395-8503-9585-0395-850395850395", + "employeeId": "5763cd4dc378a38ecd387737", + "price": { + "priceIncludingTax": 165.00, + "taxAmount": 27.50, + "priceExcludingTax": 137.50, + "currency": "USD" + }, + "label": "Training", + "description": "Registration for a webinar on digital marketing.", + "category": "Training", + "receiptLink": "https://example.com/receipt19.pdf", + "status": "in_review", + "creationDate": "2024-07-10T09:00:00Z", + "updateDate": "2024-07-11T14:30:00Z" + }, + { + "id": "85039585-0395-8503-9585-039585039585", + "employeeId": "5763cd4dfa6f96cd26c65787", + "price": { + "priceIncludingTax": 82.75, + "taxAmount": 13.79, + "priceExcludingTax": 68.96, + "currency": "EUR" + }, + "label": "Office Supplies", + "description": "Purchase of ink cartridges for the printer.", + "category": "Supplies", + "receiptLink": "https://example.com/receipt20.jpg", + "status": "approved", + "creationDate": "2024-06-28T11:45:00Z", + "updateDate": "2024-06-30T09:15:00Z" + } +] \ No newline at end of file diff --git a/steps/03.01-server-components-solution/src/functions/timing.ts b/steps/03.01-server-components-solution/src/functions/timing.ts new file mode 100644 index 0000000..3b8c6f3 --- /dev/null +++ b/steps/03.01-server-components-solution/src/functions/timing.ts @@ -0,0 +1,7 @@ +export const debounce = (fn: Function, ms = 300) => { + let timeoutId: ReturnType; + return function (this: any, ...args: any[]) { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => fn.apply(this, args), ms); + }; +}; diff --git a/steps/03.01-server-components-solution/src/styles/global.css b/steps/03.01-server-components-solution/src/styles/global.css new file mode 100644 index 0000000..f77ed90 --- /dev/null +++ b/steps/03.01-server-components-solution/src/styles/global.css @@ -0,0 +1,41 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --color-bg-global: #e9effc; + --color-bg-primary: #ffffff; + --color-bg-secondary: #f5f5f5; + --color-text-primary: #000000; + + --spacing-sm: 0.5rem; + --spacing-md: 0.75rem; + --spacing-lg: 1rem; + --spacing-xl: 1.5rem; +} + +/* Headings */ + +.heading1 { + font-size: 2rem; + font-weight: bold; + margin-bottom: var(--spacing-lg); +} + +.heading2 { + font-size: 1.5rem; + font-weight: bold; + margin-bottom: var(--spacing-lg); +} + +.heading3 { + font-size: 1.125rem; + font-weight: bold; + margin-bottom: var(--spacing-lg); +} + +.heading4 { + font-size: 1rem; + font-weight: bold; + margin-bottom: var(--spacing-lg); +} diff --git a/steps/03.01-server-components-solution/src/types.ts b/steps/03.01-server-components-solution/src/types.ts new file mode 100644 index 0000000..82cffb5 --- /dev/null +++ b/steps/03.01-server-components-solution/src/types.ts @@ -0,0 +1,39 @@ +export type Person = { + id: string; + photo?: string; + firstname: string; + lastname: string; + position: string; + entryDate: string; + birthDate: string; + gender: string; + email: string; + phone: string; + isManager: boolean; + manager?: string; + managerId?: string; +}; + +export type Expense = { + id: string; + employeeId: string; + price: { + priceIncludingTax: number; + taxAmount: number; + priceExcludingTax: number; + currency: string; + }; + label: string; + description: string; + category: string; + receiptLink: string; + status: 'approved' | 'created' | 'declined'; + creationDate: string; + updateDate: string; +}; + +export type PaginationAttributes = { + per_page?: number; + page: number; + total_pages: number; +}; diff --git a/steps/03.01-server-components-solution/tailwind.config.js b/steps/03.01-server-components-solution/tailwind.config.js new file mode 100644 index 0000000..eaa361c --- /dev/null +++ b/steps/03.01-server-components-solution/tailwind.config.js @@ -0,0 +1,9 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ['./src/**/*.{js,ts,jsx,tsx,mdx}'], + darkMode: 'selector', + theme: { + extend: {}, + }, + plugins: [], +}; diff --git a/steps/03.01-server-components-solution/tsconfig.json b/steps/03.01-server-components-solution/tsconfig.json new file mode 100644 index 0000000..7b28589 --- /dev/null +++ b/steps/03.01-server-components-solution/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/steps/03.01-server-components/.env.example b/steps/03.01-server-components/.env.example new file mode 100644 index 0000000..1ebabff --- /dev/null +++ b/steps/03.01-server-components/.env.example @@ -0,0 +1,2 @@ +API_BASE_URL=http://localhost:3001 +API_KEY=XXXX diff --git a/steps/03.01-server-components/.eslintrc.json b/steps/03.01-server-components/.eslintrc.json new file mode 100644 index 0000000..bffb357 --- /dev/null +++ b/steps/03.01-server-components/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/steps/03.01-server-components/.gitignore b/steps/03.01-server-components/.gitignore new file mode 100644 index 0000000..fd3dbb5 --- /dev/null +++ b/steps/03.01-server-components/.gitignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/steps/03.01-server-components/.gitkeep b/steps/03.01-server-components/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/steps/03.01-server-components/README.md b/steps/03.01-server-components/README.md new file mode 100644 index 0000000..1f91c62 --- /dev/null +++ b/steps/03.01-server-components/README.md @@ -0,0 +1 @@ +# 03.01 - Server Components diff --git a/steps/03.01-server-components/next.config.mjs b/steps/03.01-server-components/next.config.mjs new file mode 100644 index 0000000..16343f6 --- /dev/null +++ b/steps/03.01-server-components/next.config.mjs @@ -0,0 +1,15 @@ +const apiUrl = new URL(process.env.API_BASE_URL || 'http://localhost:3001'); + +/** @type {import('next').NextConfig} */ +const nextConfig = { + images: { + remotePatterns: [ + { + hostname: apiUrl.hostname, + port: apiUrl.port, + }, + ], + }, +}; + +export default nextConfig; diff --git a/steps/03.01-server-components/package.json b/steps/03.01-server-components/package.json new file mode 100644 index 0000000..6ff3b97 --- /dev/null +++ b/steps/03.01-server-components/package.json @@ -0,0 +1,38 @@ +{ + "name": "03.01", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "bright": "^0.8.5", + "clsx": "^2.1.1", + "jose": "^5.6.3", + "jsonwebtoken": "^9.0.2", + "next": "14.2.5", + "react": "^18", + "react-dom": "^18", + "react-error-boundary": "^4.0.13", + "rehype-sanitize": "^6.0.0", + "rehype-stringify": "^10.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.0", + "server-only": "^0.0.1", + "showdown": "^2.1.0", + "unified": "^11.0.5" + }, + "devDependencies": { + "@types/jsonwebtoken": "^9.0.6", + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "@types/showdown": "^2.0.6", + "eslint": "^8", + "eslint-config-next": "14.2.5", + "typescript": "^5" + } +} diff --git a/steps/03.01-server-components/postcss.config.js b/steps/03.01-server-components/postcss.config.js new file mode 100644 index 0000000..33ad091 --- /dev/null +++ b/steps/03.01-server-components/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/steps/03.01-server-components/public/next.svg b/steps/03.01-server-components/public/next.svg new file mode 100644 index 0000000..5174b28 --- /dev/null +++ b/steps/03.01-server-components/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/steps/03.01-server-components/public/portraits/men/30.jpg b/steps/03.01-server-components/public/portraits/men/30.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d04b7a2669245212620be0cbb17922fd1cb3cbfe GIT binary patch literal 4349 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 w3$0jQtjS=6rPW;hG!!51^p@HCR)|Jncu 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 literal 0 HcmV?d00001 diff --git a/steps/03.01-server-components/public/portraits/men/78.jpg b/steps/03.01-server-components/public/portraits/men/78.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6438e80b9b56fcf7e6e0e9a02eb8cc54a53c91aa GIT binary patch literal 4643 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 Y-IZTg@->$i4XtycyzDV!v4pAr0qF|a9RL6T literal 0 HcmV?d00001 diff --git a/steps/03.01-server-components/public/portraits/men/86.jpg b/steps/03.01-server-components/public/portraits/men/86.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9358491105b401a359ef698035ab9b05b84e0457 GIT binary patch literal 5433 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 Kdoq>(yYN42Z*!dh literal 0 HcmV?d00001 diff --git a/steps/03.01-server-components/public/portraits/women/65.jpg b/steps/03.01-server-components/public/portraits/women/65.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3cab57987a6ff10c5639fad656fb0fc77ba569c9 GIT binary patch literal 5972 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 C^!eWa literal 0 HcmV?d00001 diff --git a/steps/03.01-server-components/public/portraits/women/85.jpg b/steps/03.01-server-components/public/portraits/women/85.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0a900f9e8874eddf5978f08e6de03815f23bb1ea GIT binary patch literal 3912 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 literal 0 HcmV?d00001 diff --git a/steps/03.01-server-components/public/portraits/women/93.jpg b/steps/03.01-server-components/public/portraits/women/93.jpg new file mode 100644 index 0000000000000000000000000000000000000000..81ea0613898f679ad77f8e4563a93be893f38a07 GIT binary patch literal 4871 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 \ No newline at end of file diff --git a/steps/03.01-server-components/src/app/(auth)/layout.tsx b/steps/03.01-server-components/src/app/(auth)/layout.tsx new file mode 100644 index 0000000..cac31a7 --- /dev/null +++ b/steps/03.01-server-components/src/app/(auth)/layout.tsx @@ -0,0 +1,12 @@ +type AuthLayoutProps = { + children: React.ReactNode; +}; + +const AuthLayout: React.FC = ({ children }) => ( +
+
+
{children}
+
+); + +export default AuthLayout; diff --git a/steps/03.01-server-components/src/app/(auth)/login/page.tsx b/steps/03.01-server-components/src/app/(auth)/login/page.tsx new file mode 100644 index 0000000..3ae0596 --- /dev/null +++ b/steps/03.01-server-components/src/app/(auth)/login/page.tsx @@ -0,0 +1,30 @@ +import { Metadata } from 'next'; + +import TextField from '@/components/TextField'; +import Button from '@/components/Button'; + +export const metadata: Metadata = { + title: 'SFEIR People | Login', +}; + +const LoginPage = () => { + return ( +
+

Welcome !

+ + + + + ); +}; + +export default LoginPage; diff --git a/steps/03.01-server-components/src/app/(dashboard)/employees/[id]/edit/page.tsx b/steps/03.01-server-components/src/app/(dashboard)/employees/[id]/edit/page.tsx new file mode 100644 index 0000000..9d29dc0 --- /dev/null +++ b/steps/03.01-server-components/src/app/(dashboard)/employees/[id]/edit/page.tsx @@ -0,0 +1,24 @@ +import EmployeeForm from '@/components/EmployeeForm'; +import PageTitle from '@/components/PageTitle'; + +import employeesData from '@/data/employees.json'; + +const EmployeeDetail = async ({ params }: { params: { id: string } }) => { + const employee = employeesData.find((employee) => employee.id === params.id); + + if (!employee) return Single Employee - Not found; + + return ( + <> + + Single Employee - {employee.firstname} {employee.lastname} | Edit + + +
+ +
+ + ); +}; + +export default EmployeeDetail; diff --git a/steps/03.01-server-components/src/app/(dashboard)/employees/[id]/page.tsx b/steps/03.01-server-components/src/app/(dashboard)/employees/[id]/page.tsx new file mode 100644 index 0000000..a498c63 --- /dev/null +++ b/steps/03.01-server-components/src/app/(dashboard)/employees/[id]/page.tsx @@ -0,0 +1,21 @@ +import PageTitle from '@/components/PageTitle'; +import PersonCard from '@/components/PersonCard'; + +import employeesData from '@/data/employees.json'; + +const EmployeeDetail = async ({ params }: { params: { id: string } }) => { + const employee = employeesData.find((employee) => employee.id === params.id); + + if (!employee) return Single Employee - Not found; + + return ( + <> + + Single Employee - {employee.firstname} {employee.lastname} + + + + ); +}; + +export default EmployeeDetail; diff --git a/steps/03.01-server-components/src/app/(dashboard)/employees/new/page.tsx b/steps/03.01-server-components/src/app/(dashboard)/employees/new/page.tsx new file mode 100644 index 0000000..f1e5157 --- /dev/null +++ b/steps/03.01-server-components/src/app/(dashboard)/employees/new/page.tsx @@ -0,0 +1,18 @@ +import EmployeeForm from '@/components/EmployeeForm'; +import PageTitle from '@/components/PageTitle'; + +const EmployeeDetail = async () => { + return ( + <> + + Employees | Create + + +
+ +
+ + ); +}; + +export default EmployeeDetail; diff --git a/steps/03.01-server-components/src/app/(dashboard)/employees/page.tsx b/steps/03.01-server-components/src/app/(dashboard)/employees/page.tsx new file mode 100644 index 0000000..2a356a7 --- /dev/null +++ b/steps/03.01-server-components/src/app/(dashboard)/employees/page.tsx @@ -0,0 +1,47 @@ +import Link from 'next/link'; + +import Button from '@/components/Button'; +import PageTitle from '@/components/PageTitle'; +import PersonCard from '@/components/PersonCard'; +import Search from '@/components/Search'; + +import employeesData from '@/data/employees.json'; + +const Employees = async ({ searchParams }: { searchParams: { search?: string } }) => { + const search = searchParams.search || ''; + const employees = employeesData.filter((employee) => + `${employee.firstname} ${employee.lastname}`.toLowerCase().includes(search.toLowerCase()) + ); + + return ( +
+ Employees +
+ + +
+
+ {employees?.map((employee) => ( + + + +
+ } + /> + ))} +
+ + ); +}; + +export default Employees; diff --git a/steps/03.01-server-components/src/app/(dashboard)/expenses/[id]/page.tsx b/steps/03.01-server-components/src/app/(dashboard)/expenses/[id]/page.tsx new file mode 100644 index 0000000..bda58e2 --- /dev/null +++ b/steps/03.01-server-components/src/app/(dashboard)/expenses/[id]/page.tsx @@ -0,0 +1,18 @@ +import ExpenseDetails from '@/components/ExpensesDetails'; +import PageTitle from '@/components/PageTitle'; + +import expensesData from '@/data/expenses.json'; +import { Expense } from '@/types'; + +const SingleExpense = ({ params }: { params: { id: string } }) => { + const expense = expensesData.find((expense) => expense.id === params.id); + + return ( + <> + Single Expense - {expense?.label || 'Not found'} + {expense && } + + ); +}; + +export default SingleExpense; diff --git a/steps/03.01-server-components/src/app/(dashboard)/expenses/page.tsx b/steps/03.01-server-components/src/app/(dashboard)/expenses/page.tsx new file mode 100644 index 0000000..55d1d65 --- /dev/null +++ b/steps/03.01-server-components/src/app/(dashboard)/expenses/page.tsx @@ -0,0 +1,17 @@ +import ExpensesTable from '@/components/ExpensesTable'; +import PageTitle from '@/components/PageTitle'; + +import { Expense } from '@/types'; + +import expensesData from '@/data/expenses.json'; + +const Expenses = async () => { + return ( + <> + Expenses + } /> + + ); +}; + +export default Expenses; diff --git a/steps/03.01-server-components/src/app/(dashboard)/layout.tsx b/steps/03.01-server-components/src/app/(dashboard)/layout.tsx new file mode 100644 index 0000000..b264008 --- /dev/null +++ b/steps/03.01-server-components/src/app/(dashboard)/layout.tsx @@ -0,0 +1,30 @@ +import { Metadata } from 'next'; +import Link from 'next/link'; +import Image from 'next/image'; + +import NavigationMenu from '@/components/NavigationMenu'; + +import logo from '@/assets/svg/logo.svg'; + +type DashboardLayoutProps = { children: React.ReactNode }; + +export const metadata: Metadata = { + title: 'SFEIR People | Dashboard', +}; + +const DashboardLayout: React.FC = async ({ children }) => { + return ( +
+
+ + People logo + + +
Version: ?.?.?
+
+
{children}
+
+ ); +}; + +export default DashboardLayout; diff --git a/steps/03.01-server-components/src/app/(dashboard)/page.tsx b/steps/03.01-server-components/src/app/(dashboard)/page.tsx new file mode 100644 index 0000000..f581ebb --- /dev/null +++ b/steps/03.01-server-components/src/app/(dashboard)/page.tsx @@ -0,0 +1,11 @@ +import PageTitle from '@/components/PageTitle'; + +const HomePage = () => { + return ( + <> + SFEIR People + + ); +}; + +export default HomePage; diff --git a/steps/03.01-server-components/src/app/layout.tsx b/steps/03.01-server-components/src/app/layout.tsx new file mode 100644 index 0000000..e7d90e9 --- /dev/null +++ b/steps/03.01-server-components/src/app/layout.tsx @@ -0,0 +1,21 @@ +import type { Metadata } from 'next'; +import { Inter } from 'next/font/google'; + +const inter = Inter({ subsets: ['latin'] }); + +import '@/styles/global.css'; + +export const metadata: Metadata = { + title: 'SFEIR People', + description: 'SFEIR People dashboard application', +}; + +const RootLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => { + return ( + + {children} + + ); +}; + +export default RootLayout; diff --git a/steps/03.01-server-components/src/assets/images/profile-placeholder.jpg b/steps/03.01-server-components/src/assets/images/profile-placeholder.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6fa00ea6c9e371e542006bb4bd69ae5e6922a324 GIT binary patch literal 11940 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 m>aIBzX~|65y0*1u%UVegt+JNI)Z4`imH;jI + + + + + + + + diff --git a/steps/03.01-server-components/src/assets/svg/logoDark.svg b/steps/03.01-server-components/src/assets/svg/logoDark.svg new file mode 100644 index 0000000..06eb6ed --- /dev/null +++ b/steps/03.01-server-components/src/assets/svg/logoDark.svg @@ -0,0 +1,28 @@ + + + + + + + + + diff --git a/steps/03.01-server-components/src/components/Alert.tsx b/steps/03.01-server-components/src/components/Alert.tsx new file mode 100644 index 0000000..1c90895 --- /dev/null +++ b/steps/03.01-server-components/src/components/Alert.tsx @@ -0,0 +1,17 @@ +import clsx from 'clsx'; + +type AlertProps = { + children: React.ReactNode; + className?: string; +}; + +const Alert: React.FC = ({ children, className }) => ( +
+ {children} +
+); + +export default Alert; diff --git a/steps/03.01-server-components/src/components/Button.tsx b/steps/03.01-server-components/src/components/Button.tsx new file mode 100644 index 0000000..2cee623 --- /dev/null +++ b/steps/03.01-server-components/src/components/Button.tsx @@ -0,0 +1,34 @@ +import clsx from 'clsx'; + +export type ButtonProps = { + children: React.ReactNode; + className?: string; + variant?: 'primary' | 'secondary'; + component?: C; +} & Omit, 'className' | 'variant'>; + +const classNames = { + primary: 'inline-block text-white bg-blue-700 hover:bg-blue-800 font-medium rounded-lg text-sm px-5 py-2.5', + secondary: [ + 'inline-block py-2.5 px-5 text-sm font-medium text-slate-900 bg-white rounded-lg border border-gray-200', + 'hover:bg-gray-100 hover:text-blue-700', + 'dark:bg-slate-900 dark:text-white dark:hover:bg-slate-950 dark:hover:text-blue-200 dark:hover:border-blue-200', + ].join(' '), +}; + +const Button = ({ + children, + className, + variant = 'secondary', + component, + ...restProps +}: ButtonProps) => { + const Component = component || 'button'; + return ( + + {children} + + ); +}; + +export default Button; diff --git a/steps/03.01-server-components/src/components/EmployeeForm.tsx b/steps/03.01-server-components/src/components/EmployeeForm.tsx new file mode 100644 index 0000000..dc15055 --- /dev/null +++ b/steps/03.01-server-components/src/components/EmployeeForm.tsx @@ -0,0 +1,113 @@ +'use client'; + +import Image from 'next/image'; +import TextField from '@/components/TextField'; +import { Person } from '@/types'; +import { useFormState } from 'react-dom'; + +import placeholderImage from '@/assets/images/profile-placeholder.jpg'; +import Button from './Button'; + +type ActionState = { + validationErrors?: { [key: string]: Array }; +}; + +type Action = (id: string, formData: FormData) => Promise; + +type EmployeeFormProps = { + employee?: Person; + action?: Action; + className?: string; +}; + +const initialState = { + validationErrors: {}, +} as ActionState; + +const EmployeeForm: React.FC = ({ employee, action, className }) => { + // @ts-ignore + const [state, formAction] = useFormState(action, initialState as unknown as void); + + return ( +
+
+ {employee +
+
+
+ + + + + +
+
+ + + +
+
+
+ +
+
+ ); +}; + +export default EmployeeForm; diff --git a/steps/03.01-server-components/src/components/ExpensesDetails.tsx b/steps/03.01-server-components/src/components/ExpensesDetails.tsx new file mode 100644 index 0000000..f59b51a --- /dev/null +++ b/steps/03.01-server-components/src/components/ExpensesDetails.tsx @@ -0,0 +1,56 @@ +import { Expense } from '@/types'; +import Paper from './Paper'; + +type ExpenseDetailsRowProps = { + label: string; + value: string; +}; + +const ExpenseDetailsRow: React.FC = ({ label, value }) => ( +
+ {label} + {value} +
+); + +type ExpenseDetailsProps = { + expense: Expense; +}; + +const ExpenseDetails: React.FC = ({ expense }) => ( + <> +
+
+

Information

+ + + + + +
+
+

Workflow

+ + + + + +
+
+
+
+

Amount

+ + + + + +
+
+ +); + +export default ExpenseDetails; diff --git a/steps/03.01-server-components/src/components/ExpensesTable.tsx b/steps/03.01-server-components/src/components/ExpensesTable.tsx new file mode 100644 index 0000000..1b2e240 --- /dev/null +++ b/steps/03.01-server-components/src/components/ExpensesTable.tsx @@ -0,0 +1,61 @@ +'use client'; + +import { Expense } from '@/types'; +import clsx from 'clsx'; +import { useRouter } from 'next/navigation'; + +type ExpensesTableProps = { + expenses: Array; +}; + +const ExpensesTable: React.FC = ({ expenses }) => { + const router = useRouter(); + + const handleClick = (expenseId: string) => () => { + router.push(`/expenses/${expenseId}`); + }; + + return ( + + + + + + + + + + + {expenses.map((expense, index) => ( + + + + + + + ))} + +
+ Label + + Creation date + + Category + + Price +
{expense.label}{new Date(expense.creationDate).toLocaleDateString()}{expense.category} + {expense.price.priceIncludingTax} {expense.price.currency} +
+ ); +}; + +export default ExpensesTable; diff --git a/steps/03.01-server-components/src/components/Icons/ArrowLeft.tsx b/steps/03.01-server-components/src/components/Icons/ArrowLeft.tsx new file mode 100644 index 0000000..1cc10c7 --- /dev/null +++ b/steps/03.01-server-components/src/components/Icons/ArrowLeft.tsx @@ -0,0 +1,25 @@ +type ArrowLeftProps = { + className?: string; +}; + +const ArrowLeft: React.FC = ({ className }) => ( + +); + +export default ArrowLeft; diff --git a/steps/03.01-server-components/src/components/Icons/Eye.tsx b/steps/03.01-server-components/src/components/Icons/Eye.tsx new file mode 100644 index 0000000..04beb91 --- /dev/null +++ b/steps/03.01-server-components/src/components/Icons/Eye.tsx @@ -0,0 +1,11 @@ +type EyeProps = { + className?: string; +}; + +const Eye: React.FC = ({ className }) => ( + + + +); + +export default Eye; diff --git a/steps/03.01-server-components/src/components/Icons/Loader.tsx b/steps/03.01-server-components/src/components/Icons/Loader.tsx new file mode 100644 index 0000000..9c81994 --- /dev/null +++ b/steps/03.01-server-components/src/components/Icons/Loader.tsx @@ -0,0 +1,25 @@ +type LoaderProps = { + className?: string; +}; + +const Loader: React.FC = ({ className }) => ( + +); + +export default Loader; diff --git a/steps/03.01-server-components/src/components/NavigationItem.tsx b/steps/03.01-server-components/src/components/NavigationItem.tsx new file mode 100644 index 0000000..9f68e10 --- /dev/null +++ b/steps/03.01-server-components/src/components/NavigationItem.tsx @@ -0,0 +1,30 @@ +'use client'; + +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; + +import clsx from 'clsx'; + +type NavigationItemsProps = { + href: string; + children: React.ReactNode; +}; + +const NavigationItem: React.FC = ({ href, children }) => { + const pathname = usePathname(); + + return ( + + {children} + + ); +}; + +export default NavigationItem; diff --git a/steps/03.01-server-components/src/components/NavigationMenu.tsx b/steps/03.01-server-components/src/components/NavigationMenu.tsx new file mode 100644 index 0000000..4b778c6 --- /dev/null +++ b/steps/03.01-server-components/src/components/NavigationMenu.tsx @@ -0,0 +1,21 @@ +import NavigationItem from './NavigationItem'; + +const NavigationMenu = () => { + return ( + + ); +}; + +export default NavigationMenu; diff --git a/steps/03.01-server-components/src/components/PageTitle.tsx b/steps/03.01-server-components/src/components/PageTitle.tsx new file mode 100644 index 0000000..217fe69 --- /dev/null +++ b/steps/03.01-server-components/src/components/PageTitle.tsx @@ -0,0 +1,25 @@ +import Link from 'next/link'; + +import ArrowLeft from './Icons/ArrowLeft'; + +type PageTitleProps = { + children: React.ReactNode; + backHref?: string; +}; + +const PageTitle: React.FC = ({ children, backHref }) => ( +
+ {backHref && ( + + + Go back + + )} +

{children}

+
+); + +export default PageTitle; diff --git a/steps/03.01-server-components/src/components/Pagination.tsx b/steps/03.01-server-components/src/components/Pagination.tsx new file mode 100644 index 0000000..be9a43a --- /dev/null +++ b/steps/03.01-server-components/src/components/Pagination.tsx @@ -0,0 +1,95 @@ +'use client'; + +import clsx from 'clsx'; +import Link from 'next/link'; +import { usePathname, useSearchParams } from 'next/navigation'; + +type PaginationProps = { + totalPages: number; + className?: string; +}; + +type PaginationShortcutProps = { + href: string; + disabled?: boolean; + className?: string; + children: React.ReactNode; +}; + +const PaginationShortcut: React.FC = ({ href, disabled, className, children }) => { + const classNames = clsx( + 'block text-center px-3 py-2 ms-0 border bg-white dark:bg-slate-900', + className, + !disabled && + 'hover:bg-gray-100 hover:text-gray-700 text-gray-500 border-gray-300 dark:text-white dark:border-gray-700 dark:hover:bg-slate-950 dark:hover:text-white', + disabled && 'text-gray-300 border-gray-200 dark:text-gray-500 dark:border-gray-600' + ); + + if (disabled) return
{children}
; + + return ( + + {children} + + ); +}; + +const Pagination: React.FC = ({ totalPages, className }) => { + const params = useSearchParams(); + const pathname = usePathname(); + + const currentPage = Number(params.get('page')) || 1; + + const getPageUrl = (page: number): string => { + const newParams = new URLSearchParams(params); + newParams.set('page', page.toString()); + return `${pathname}?${newParams.toString()}`; + }; + + return ( + + ); +}; + +export default Pagination; diff --git a/steps/03.01-server-components/src/components/Paper.tsx b/steps/03.01-server-components/src/components/Paper.tsx new file mode 100644 index 0000000..8ba656e --- /dev/null +++ b/steps/03.01-server-components/src/components/Paper.tsx @@ -0,0 +1,14 @@ +import clsx from 'clsx'; + +type PaperProps = React.HTMLAttributes & { + children: React.ReactNode; + rounded?: boolean; +}; + +const Paper: React.FC = ({ children, rounded = true, ...restProps }) => ( +
+ {children} +
+); + +export default Paper; diff --git a/steps/03.01-server-components/src/components/PersonCard.tsx b/steps/03.01-server-components/src/components/PersonCard.tsx new file mode 100644 index 0000000..b38ee53 --- /dev/null +++ b/steps/03.01-server-components/src/components/PersonCard.tsx @@ -0,0 +1,43 @@ +import Image from 'next/image'; + +import { Person } from '@/types'; + +import placeholderImage from '@/assets/images/profile-placeholder.jpg'; + +type PersonCardProps = React.HTMLAttributes & { + person: Person; + actions?: React.ReactNode; + compact?: boolean; +}; + +const PersonCard: React.FC = ({ person, actions, className, compact = false }) => { + return ( +
+
+ {`Picture + + {person.firstname} {person.lastname} + + {person.position} +
+ + {!compact && ( +
+ {person.phone} + {person.email} + {person.manager && {person.manager}} +
+ )} + + {actions &&
{actions}
} +
+ ); +}; + +export default PersonCard; diff --git a/steps/03.01-server-components/src/components/Search.tsx b/steps/03.01-server-components/src/components/Search.tsx new file mode 100644 index 0000000..98fe032 --- /dev/null +++ b/steps/03.01-server-components/src/components/Search.tsx @@ -0,0 +1,43 @@ +'use client'; + +import { debounce } from '@/functions/timing'; +import clsx from 'clsx'; +import { usePathname, useRouter, useSearchParams } from 'next/navigation'; + +const Search = ({ ...restProps }) => { + const router = useRouter(); + const params = useSearchParams(); + const pathname = usePathname(); + + const handleChange = debounce((event: React.ChangeEvent) => { + const value = event.target?.value; + const newParams = new URLSearchParams(params); + newParams.delete('page'); + if (value) newParams.set('search', value); + else newParams.delete('search'); + router.replace(`${pathname}?${newParams.toString()}`); + }, 200); + + return ( + <> + + + + ); +}; + +export default Search; diff --git a/steps/03.01-server-components/src/components/TextField.tsx b/steps/03.01-server-components/src/components/TextField.tsx new file mode 100644 index 0000000..d8061e9 --- /dev/null +++ b/steps/03.01-server-components/src/components/TextField.tsx @@ -0,0 +1,31 @@ +import clsx from 'clsx'; + +type TextFieldProps = React.InputHTMLAttributes & { + label: string; + id: string; + type?: string; + className?: string; + errorMessages?: Array; +}; + +const TextField: React.FC = ({ label, id, type = 'text', className, errorMessages, ...restProps }) => { + return ( +
+ + + {errorMessages?.length &&

{errorMessages[0]}

} +
+ ); +}; + +export default TextField; diff --git a/steps/03.01-server-components/src/data/employees.json b/steps/03.01-server-components/src/data/employees.json new file mode 100644 index 0000000..5f5342e --- /dev/null +++ b/steps/03.01-server-components/src/data/employees.json @@ -0,0 +1,152 @@ +[ + { + "id": "5763cd4d9d2a4f259b53c901", + "photo": "/portraits/women/85.jpg", + "firstname": "Leanne", + "lastname": "Woodard", + "position": "Developer", + "entryDate": "27/10/2015", + "birthDate": "02/01/1974", + "gender": "f", + "email": "woodard.l@acme.com", + "phone": "0784112248", + "isManager": false, + "manager": "Erika", + "managerId": "5763cd4d3b57c672861bfa1f" + }, + { + "id": "5763cd4d51fdb6588742f99e", + "photo": "/portraits/men/56.jpg", + "firstname": "Castaneda", + "lastname": "Salinas", + "position": "Developer", + "entryDate": "04/10/2015", + "birthDate": "22/01/1963", + "gender": "m", + "email": "salinas.c@acme.com", + "phone": "0145652522", + "isManager": false, + "manager": "Erika", + "managerId": "5763cd4d3b57c672861bfa1f" + }, + { + "id": "5763cd4dba6362a3f92c954e", + "photo": "/portraits/women/24.jpg", + "firstname": "Phyllis", + "lastname": "Donovan", + "position": "Sales", + "entryDate": "30/03/2015", + "birthDate": "30/11/1951", + "gender": "f", + "email": "donovan.p@acme.com", + "phone": "0685230125", + "isManager": false, + "manager": "Erika", + "managerId": "5763cd4d3b57c672861bfa1f" + }, + { + "id": "5763cd4d3b57c672861bfa1f", + "photo": "/portraits/women/65.jpg", + "firstname": "Erika", + "lastname": "Guzman", + "position": "Product Owner", + "entryDate": "13/05/2016", + "birthDate": "19/03/1962", + "gender": "f", + "email": "guzman.e@acme.com", + "phone": "0678412587", + "isManager": true, + "manager": "Mercedes", + "managerId": "5763cd4d979b62a209809160" + }, + { + "id": "5763cd4d5fc36e4f842ca5a9", + "photo": "/portraits/men/30.jpg", + "firstname": "Moody", + "lastname": "Prince", + "position": "Developer", + "entryDate": "28/09/2015", + "birthDate": "15/04/1971", + "gender": "m", + "email": "prince.m@acme.com", + "phone": "0662589632", + "isManager": false, + "manager": "Mercedes", + "managerId": "5763cd4d979b62a209809160" + }, + { + "id": "5763cd4d979b62a209809160", + "photo": "/portraits/women/8.jpg", + "firstname": "Mercedes", + "lastname": "Hebert", + "position": "Product Owner", + "entryDate": "02/01/2016", + "birthDate": "20/07/1947", + "gender": "f", + "email": "hebert.m@acme.com", + "phone": "0125878522", + "isManager": true, + "manager": "Mclaughlin", + "managerId": "5763cd4dfa6f96cd26c65787" + }, + { + "id": "5763cd4d15e6c2c28b70f2e8", + "photo": "/portraits/men/86.jpg", + "firstname": "Howell", + "lastname": "Mcknight", + "position": "Sales", + "entryDate": "26/09/2015", + "birthDate": "18/07/1979", + "gender": "m", + "email": "mcknight.h@acme.com", + "phone": "0456987425", + "isManager": false, + "manager": "Mclaughlin", + "managerId": "5763cd4dfa6f96cd26c65787" + }, + { + "id": "5763cd4d5d6ad8dfc6c34883", + "photo": "/portraits/women/93.jpg", + "firstname": "Lizzie", + "lastname": "Morris", + "position": "Human Resources", + "entryDate": "03/05/2016", + "birthDate": "15/11/1981", + "gender": "f", + "email": "morris.l@acme.com", + "phone": "0662259988", + "isManager": false, + "manager": "Mclaughlin", + "managerId": "5763cd4dfa6f96cd26c65787" + }, + { + "id": "5763cd4dc378a38ecd387737", + "photo": "/portraits/men/34.jpg", + "firstname": "Roy", + "lastname": "Nielsen", + "position": "Sales", + "entryDate": "17/05/2016", + "birthDate": "21/10/1951", + "gender": "m", + "email": "nielsen.r@acme.com", + "phone": "0755669551", + "isManager": false, + "manager": "Mclaughlin", + "managerId": "5763cd4dfa6f96cd26c65787" + }, + { + "id": "5763cd4dfa6f96cd26c65787", + "photo": "/portraits/men/78.jpg", + "firstname": "Mclaughlin", + "lastname": "Cochran", + "position": "Director", + "entryDate": "11/04/2016", + "birthDate": "19/03/1973", + "gender": "m", + "email": "cochran.m@acme.com", + "phone": "0266334856", + "isManager": true, + "manager": "", + "managerId": "" + } +] diff --git a/steps/03.01-server-components/src/data/expenses.json b/steps/03.01-server-components/src/data/expenses.json new file mode 100644 index 0000000..0d096dc --- /dev/null +++ b/steps/03.01-server-components/src/data/expenses.json @@ -0,0 +1,342 @@ +[ + { + "id": "0475830f-a563-44e0-8c5c-6d829c11a132", + "employeeId": "5763cd4d51fdb6588742f99e", + "price": { + "priceIncludingTax": 120.50, + "taxAmount": 20.50, + "priceExcludingTax": 100, + "currency": "EUR" + }, + "label": "Business Lunch", + "description": "Lunch with a client to discuss a new project.", + "category": "Meals", + "receiptLink": "https://example.com/receipt1.pdf", + "status": "approved", + "creationDate": "2024-03-15T09:30:00Z", + "updateDate": "2024-03-18T14:45:00Z" + }, + { + "id": "a2e8b2c4-99d8-4c13-9a9c-0a8a68623260", + "employeeId": "5763cd4d51fdb6588742f99e", + "price": { + "priceIncludingTax": 55.20, + "taxAmount": 7.20, + "priceExcludingTax": 48, + "currency": "USD" + }, + "label": "Office Supplies", + "description": "Purchase of paper, pens, etc.", + "category": "Supplies", + "receiptLink": "https://example.com/receipt2.jpg", + "status": "created", + "creationDate": "2024-05-02T11:20:00Z", + "updateDate": "2024-05-02T11:20:00Z" + }, + { + "id": "3d3fb561-0d9c-4021-8285-d2f49c40c47d", + "employeeId": "5763cd4d51fdb6588742f99e", + "price": { + "priceIncludingTax": 250, + "taxAmount": 0, + "priceExcludingTax": 250, + "currency": "EUR" + }, + "label": "Plane Ticket", + "description": "Business trip to Berlin.", + "category": "Travel", + "receiptLink": "https://example.com/receipt3.png", + "status": "declined", + "creationDate": "2024-04-10T16:45:00Z", + "updateDate": "2024-04-12T09:30:00Z" + }, + { + "id": "4c41689c-e5f5-4029-a181-38c498e7a82b", + "employeeId": "5763cd4d5fc36e4f842ca5a9", + "price": { + "priceIncludingTax": 80.00, + "taxAmount": 13.33, + "priceExcludingTax": 66.67, + "currency": "USD" + }, + "label": "Hotel", + "description": "Hotel night in London.", + "category": "Accommodation", + "receiptLink": "https://example.com/receipt4.pdf", + "status": "approved", + "creationDate": "2024-06-20T08:15:00Z", + "updateDate": "2024-06-22T10:30:00Z" + }, + { + "id": "871030e0-e485-41d5-882c-30201429a23f", + "employeeId": "5763cd4d5fc36e4f842ca5a9", + "price": { + "priceIncludingTax": 35.75, + "taxAmount": 5.75, + "priceExcludingTax": 30, + "currency": "EUR" + }, + "label": "Taxi Fare", + "description": "Ride from the airport to the office.", + "category": "Transportation", + "receiptLink": "https://example.com/receipt5.jpg", + "status": "submitted", + "creationDate": "2024-07-05T19:00:00Z", + "updateDate": "2024-07-05T19:00:00Z" + }, + { + "id": "e38e7368-8686-4211-a6a2-844806839a4c", + "employeeId": "5763cd4d979b62a209809160", + "price": { + "priceIncludingTax": 180.00, + "taxAmount": 30.00, + "priceExcludingTax": 150, + "currency": "USD" + }, + "label": "Car Rental", + "description": "Car rental for a week.", + "category": "Transportation", + "receiptLink": "https://example.com/receipt6.png", + "status": "approved", + "creationDate": "2024-02-28T13:40:00Z", + "updateDate": "2024-03-02T11:15:00Z" + }, + { + "id": "90432a7b-3009-491a-832b-a4622721a490", + "employeeId": "5763cd4d15e6c2c28b70f2e8", + "price": { + "priceIncludingTax": 65.00, + "taxAmount": 10.83, + "priceExcludingTax": 54.17, + "currency": "USD" + }, + "label": "Client Meal", + "description": "Dinner with a potential client.", + "category": "Meals", + "receiptLink": "https://example.com/receipt7.pdf", + "status": "in_review", + "creationDate": "2024-07-18T20:30:00Z", + "updateDate": "2024-07-19T09:00:00Z" + }, + { + "id": "a88a026a-e928-48b6-8a1d-90d980e7a423", + "employeeId": "5763cd4d5d6ad8dfc6c34883", + "price": { + "priceIncludingTax": 95.99, + "taxAmount": 15.99, + "priceExcludingTax": 80, + "currency": "EUR" + }, + "label": "Software", + "description": "Monthly subscription to project management software.", + "category": "Software", + "receiptLink": "https://example.com/receipt8.jpg", + "status": "approved", + "creationDate": "2024-05-10T10:00:00Z", + "updateDate": "2024-05-11T14:20:00Z" + }, + { + "id": "21e5615a-9c2a-40f2-a489-0c2f4a866098", + "employeeId": "5763cd4dc378a38ecd387737", + "price": { + "priceIncludingTax": 25.50, + "taxAmount": 4.25, + "priceExcludingTax": 21.25, + "currency": "USD" + }, + "label": "Parking Fees", + "description": "Parking fees at the airport.", + "category": "Transportation", + "receiptLink": "https://example.com/receipt9.png", + "status": "created", + "creationDate": "2024-07-30T17:45:00Z", + "updateDate": "2024-07-30T17:45:00Z" + }, + { + "id": "5200c69a-e387-4292-9282-98e66878524c", + "employeeId": "5763cd4dfa6f96cd26c65787", + "price": { + "priceIncludingTax": 150.00, + "taxAmount": 25.00, + "priceExcludingTax": 125, + "currency": "USD" + }, + "label": "Training", + "description": "Participation in professional training.", + "category": "Training", + "receiptLink": "https://example.com/receipt10.pdf", + "status": "approved", + "creationDate": "2024-04-05T09:00:00Z", + "updateDate": "2024-04-07T11:30:00Z" + }, + { + "id": "9b60a0a2-d2cf-41ab-a8a6-690080e77898", + "employeeId": "5763cd4d9d2a4f259b53c901", + "price": { + "priceIncludingTax": 75.20, + "taxAmount": 12.53, + "priceExcludingTax": 62.67, + "currency": "EUR" + }, + "label": "Office Supplies", + "description": "Purchase of office supplies.", + "category": "Supplies", + "receiptLink": "https://example.com/receipt11.jpg", + "status": "approved", + "creationDate": "2024-06-12T14:20:00Z", + "updateDate": "2024-06-14T10:15:00Z" + }, + { + "id": "8478041f-7a41-46f0-9158-34759c409873", + "employeeId": "5763cd4d51fdb6588742f99e", + "price": { + "priceIncludingTax": 280.00, + "taxAmount": 46.67, + "priceExcludingTax": 233.33, + "currency": "USD" + }, + "label": "Plane Ticket", + "description": "Business trip to New York.", + "category": "Travel", + "receiptLink": "https://example.com/receipt12.png", + "status": "submitted", + "creationDate": "2024-07-25T11:30:00Z", + "updateDate": "2024-07-25T11:30:00Z" + }, + { + "id": "4902854f-2049-4498-9597-74823829501f", + "employeeId": "5763cd4dba6362a3f92c954e", + "price": { + "priceIncludingTax": 95.00, + "taxAmount": 15.83, + "priceExcludingTax": 79.17, + "currency": "EUR" + }, + "label": "Hotel", + "description": "Hotel night in Paris.", + "category": "Accommodation", + "receiptLink": "https://example.com/receipt13.pdf", + "status": "in_review", + "creationDate": "2024-08-01T08:45:00Z", + "updateDate": "2024-08-02T10:00:00Z" + }, + { + "id": "4309573f-8903-4a42-a095-839529490582", + "employeeId": "5763cd4d3b57c672861bfa1f", + "price": { + "priceIncludingTax": 42.50, + "taxAmount": 7.08, + "priceExcludingTax": 35.42, + "currency": "EUR" + }, + "label": "Taxi Fare", + "description": "Ride from the station to the conference venue.", + "category": "Transportation", + "receiptLink": "https://example.com/receipt14.jpg", + "status": "approved", + "creationDate": "2024-05-20T18:30:00Z", + "updateDate": "2024-05-22T09:15:00Z" + }, + { + "id": "3028759f-9023-4a83-a785-928375928375", + "employeeId": "5763cd4d5fc36e4f842ca5a9", + "price": { + "priceIncludingTax": 190.00, + "taxAmount": 31.67, + "priceExcludingTax": 158.33, + "currency": "USD" + }, + "label": "Car Rental", + "description": "Weekend car rental.", + "category": "Transportation", + "receiptLink": "https://example.com/receipt15.png", + "status": "created", + "creationDate": "2024-07-28T12:00:00Z", + "updateDate": "2024-07-28T12:00:00Z" + }, + { + "id": "92837592-8375-9283-7592-837592837592", + "employeeId": "5763cd4d979b62a209809160", + "price": { + "priceIncludingTax": 70.00, + "taxAmount": 11.67, + "priceExcludingTax": 58.33, + "currency": "USD" + }, + "label": "Client Meal", + "description": "Lunch with a client to discuss a project.", + "category": "Meals", + "receiptLink": "https://example.com/receipt16.pdf", + "status": "declined", + "creationDate": "2024-03-08T13:15:00Z", + "updateDate": "2024-03-10T09:30:00Z" + }, + { + "id": "85930583-0385-9305-8303-859305830385", + "employeeId": "5763cd4d15e6c2c28b70f2e8", + "price": { + "priceIncludingTax": 110.50, + "taxAmount": 18.42, + "priceExcludingTax": 92.08, + "currency": "EUR" + }, + "label": "Software", + "description": "Purchase of a license for design software.", + "category": "Software", + "receiptLink": "https://example.com/receipt17.jpg", + "status": "approved", + "creationDate": "2024-06-05T10:45:00Z", + "updateDate": "2024-06-07T14:30:00Z" + }, + { + "id": "03958305-8305-9385-0385-930583059385", + "employeeId": "5763cd4d5d6ad8dfc6c34883", + "price": { + "priceIncludingTax": 35.00, + "taxAmount": 5.83, + "priceExcludingTax": 29.17, + "currency": "USD" + }, + "label": "Parking Fees", + "description": "Parking fees at the conference center.", + "category": "Transportation", + "receiptLink": "https://example.com/receipt18.png", + "status": "submitted", + "creationDate": "2024-07-15T16:20:00Z", + "updateDate": "2024-07-15T16:20:00Z" + }, + { + "id": "93850395-8503-9585-0395-850395850395", + "employeeId": "5763cd4dc378a38ecd387737", + "price": { + "priceIncludingTax": 165.00, + "taxAmount": 27.50, + "priceExcludingTax": 137.50, + "currency": "USD" + }, + "label": "Training", + "description": "Registration for a webinar on digital marketing.", + "category": "Training", + "receiptLink": "https://example.com/receipt19.pdf", + "status": "in_review", + "creationDate": "2024-07-10T09:00:00Z", + "updateDate": "2024-07-11T14:30:00Z" + }, + { + "id": "85039585-0395-8503-9585-039585039585", + "employeeId": "5763cd4dfa6f96cd26c65787", + "price": { + "priceIncludingTax": 82.75, + "taxAmount": 13.79, + "priceExcludingTax": 68.96, + "currency": "EUR" + }, + "label": "Office Supplies", + "description": "Purchase of ink cartridges for the printer.", + "category": "Supplies", + "receiptLink": "https://example.com/receipt20.jpg", + "status": "approved", + "creationDate": "2024-06-28T11:45:00Z", + "updateDate": "2024-06-30T09:15:00Z" + } +] \ No newline at end of file diff --git a/steps/03.01-server-components/src/functions/timing.ts b/steps/03.01-server-components/src/functions/timing.ts new file mode 100644 index 0000000..3b8c6f3 --- /dev/null +++ b/steps/03.01-server-components/src/functions/timing.ts @@ -0,0 +1,7 @@ +export const debounce = (fn: Function, ms = 300) => { + let timeoutId: ReturnType; + return function (this: any, ...args: any[]) { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => fn.apply(this, args), ms); + }; +}; diff --git a/steps/03.01-server-components/src/styles/global.css b/steps/03.01-server-components/src/styles/global.css new file mode 100644 index 0000000..f77ed90 --- /dev/null +++ b/steps/03.01-server-components/src/styles/global.css @@ -0,0 +1,41 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --color-bg-global: #e9effc; + --color-bg-primary: #ffffff; + --color-bg-secondary: #f5f5f5; + --color-text-primary: #000000; + + --spacing-sm: 0.5rem; + --spacing-md: 0.75rem; + --spacing-lg: 1rem; + --spacing-xl: 1.5rem; +} + +/* Headings */ + +.heading1 { + font-size: 2rem; + font-weight: bold; + margin-bottom: var(--spacing-lg); +} + +.heading2 { + font-size: 1.5rem; + font-weight: bold; + margin-bottom: var(--spacing-lg); +} + +.heading3 { + font-size: 1.125rem; + font-weight: bold; + margin-bottom: var(--spacing-lg); +} + +.heading4 { + font-size: 1rem; + font-weight: bold; + margin-bottom: var(--spacing-lg); +} diff --git a/steps/03.01-server-components/src/types.ts b/steps/03.01-server-components/src/types.ts new file mode 100644 index 0000000..82cffb5 --- /dev/null +++ b/steps/03.01-server-components/src/types.ts @@ -0,0 +1,39 @@ +export type Person = { + id: string; + photo?: string; + firstname: string; + lastname: string; + position: string; + entryDate: string; + birthDate: string; + gender: string; + email: string; + phone: string; + isManager: boolean; + manager?: string; + managerId?: string; +}; + +export type Expense = { + id: string; + employeeId: string; + price: { + priceIncludingTax: number; + taxAmount: number; + priceExcludingTax: number; + currency: string; + }; + label: string; + description: string; + category: string; + receiptLink: string; + status: 'approved' | 'created' | 'declined'; + creationDate: string; + updateDate: string; +}; + +export type PaginationAttributes = { + per_page?: number; + page: number; + total_pages: number; +}; diff --git a/steps/03.01-server-components/tailwind.config.js b/steps/03.01-server-components/tailwind.config.js new file mode 100644 index 0000000..eaa361c --- /dev/null +++ b/steps/03.01-server-components/tailwind.config.js @@ -0,0 +1,9 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ['./src/**/*.{js,ts,jsx,tsx,mdx}'], + darkMode: 'selector', + theme: { + extend: {}, + }, + plugins: [], +}; diff --git a/steps/03.01-server-components/tsconfig.json b/steps/03.01-server-components/tsconfig.json new file mode 100644 index 0000000..7b28589 --- /dev/null +++ b/steps/03.01-server-components/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/steps/03.02-composition-solution/.env.example b/steps/03.02-composition-solution/.env.example new file mode 100644 index 0000000..1ebabff --- /dev/null +++ b/steps/03.02-composition-solution/.env.example @@ -0,0 +1,2 @@ +API_BASE_URL=http://localhost:3001 +API_KEY=XXXX diff --git a/steps/03.02-composition-solution/.eslintrc.json b/steps/03.02-composition-solution/.eslintrc.json new file mode 100644 index 0000000..bffb357 --- /dev/null +++ b/steps/03.02-composition-solution/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/steps/03.02-composition-solution/.gitignore b/steps/03.02-composition-solution/.gitignore new file mode 100644 index 0000000..fd3dbb5 --- /dev/null +++ b/steps/03.02-composition-solution/.gitignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/steps/03.02-composition-solution/README.md b/steps/03.02-composition-solution/README.md new file mode 100644 index 0000000..c7109d2 --- /dev/null +++ b/steps/03.02-composition-solution/README.md @@ -0,0 +1 @@ +# 03.02 - Server Components - Composition diff --git a/steps/03.02-composition-solution/next.config.mjs b/steps/03.02-composition-solution/next.config.mjs new file mode 100644 index 0000000..16343f6 --- /dev/null +++ b/steps/03.02-composition-solution/next.config.mjs @@ -0,0 +1,15 @@ +const apiUrl = new URL(process.env.API_BASE_URL || 'http://localhost:3001'); + +/** @type {import('next').NextConfig} */ +const nextConfig = { + images: { + remotePatterns: [ + { + hostname: apiUrl.hostname, + port: apiUrl.port, + }, + ], + }, +}; + +export default nextConfig; diff --git a/steps/03.02-composition-solution/package.json b/steps/03.02-composition-solution/package.json new file mode 100644 index 0000000..741fbc9 --- /dev/null +++ b/steps/03.02-composition-solution/package.json @@ -0,0 +1,38 @@ +{ + "name": "03.02-solution", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "bright": "^0.8.5", + "clsx": "^2.1.1", + "jose": "^5.6.3", + "jsonwebtoken": "^9.0.2", + "next": "14.2.5", + "react": "^18", + "react-dom": "^18", + "react-error-boundary": "^4.0.13", + "rehype-sanitize": "^6.0.0", + "rehype-stringify": "^10.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.0", + "server-only": "^0.0.1", + "showdown": "^2.1.0", + "unified": "^11.0.5" + }, + "devDependencies": { + "@types/jsonwebtoken": "^9.0.6", + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "@types/showdown": "^2.0.6", + "eslint": "^8", + "eslint-config-next": "14.2.5", + "typescript": "^5" + } +} \ No newline at end of file diff --git a/steps/03.02-composition-solution/postcss.config.js b/steps/03.02-composition-solution/postcss.config.js new file mode 100644 index 0000000..33ad091 --- /dev/null +++ b/steps/03.02-composition-solution/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/steps/03.02-composition-solution/public/next.svg b/steps/03.02-composition-solution/public/next.svg new file mode 100644 index 0000000..5174b28 --- /dev/null +++ b/steps/03.02-composition-solution/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/steps/03.02-composition-solution/public/portraits/men/30.jpg b/steps/03.02-composition-solution/public/portraits/men/30.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d04b7a2669245212620be0cbb17922fd1cb3cbfe GIT binary patch literal 4349 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 w3$0jQtjS=6rPW;hG!!51^p@HCR)|Jncu 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 literal 0 HcmV?d00001 diff --git a/steps/03.02-composition-solution/public/portraits/men/78.jpg b/steps/03.02-composition-solution/public/portraits/men/78.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6438e80b9b56fcf7e6e0e9a02eb8cc54a53c91aa GIT binary patch literal 4643 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 Y-IZTg@->$i4XtycyzDV!v4pAr0qF|a9RL6T literal 0 HcmV?d00001 diff --git a/steps/03.02-composition-solution/public/portraits/men/86.jpg b/steps/03.02-composition-solution/public/portraits/men/86.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9358491105b401a359ef698035ab9b05b84e0457 GIT binary patch literal 5433 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 Kdoq>(yYN42Z*!dh literal 0 HcmV?d00001 diff --git a/steps/03.02-composition-solution/public/portraits/women/65.jpg b/steps/03.02-composition-solution/public/portraits/women/65.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3cab57987a6ff10c5639fad656fb0fc77ba569c9 GIT binary patch literal 5972 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 C^!eWa literal 0 HcmV?d00001 diff --git a/steps/03.02-composition-solution/public/portraits/women/85.jpg b/steps/03.02-composition-solution/public/portraits/women/85.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0a900f9e8874eddf5978f08e6de03815f23bb1ea GIT binary patch literal 3912 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 literal 0 HcmV?d00001 diff --git a/steps/03.02-composition-solution/public/portraits/women/93.jpg b/steps/03.02-composition-solution/public/portraits/women/93.jpg new file mode 100644 index 0000000000000000000000000000000000000000..81ea0613898f679ad77f8e4563a93be893f38a07 GIT binary patch literal 4871 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 \ No newline at end of file diff --git a/steps/03.02-composition-solution/src/app/(auth)/layout.tsx b/steps/03.02-composition-solution/src/app/(auth)/layout.tsx new file mode 100644 index 0000000..cac31a7 --- /dev/null +++ b/steps/03.02-composition-solution/src/app/(auth)/layout.tsx @@ -0,0 +1,12 @@ +type AuthLayoutProps = { + children: React.ReactNode; +}; + +const AuthLayout: React.FC = ({ children }) => ( +
+
+
{children}
+
+); + +export default AuthLayout; diff --git a/steps/03.02-composition-solution/src/app/(auth)/login/page.tsx b/steps/03.02-composition-solution/src/app/(auth)/login/page.tsx new file mode 100644 index 0000000..3ae0596 --- /dev/null +++ b/steps/03.02-composition-solution/src/app/(auth)/login/page.tsx @@ -0,0 +1,30 @@ +import { Metadata } from 'next'; + +import TextField from '@/components/TextField'; +import Button from '@/components/Button'; + +export const metadata: Metadata = { + title: 'SFEIR People | Login', +}; + +const LoginPage = () => { + return ( +
+

Welcome !

+ + + + + ); +}; + +export default LoginPage; diff --git a/steps/03.02-composition-solution/src/app/(dashboard)/employees/[id]/edit/page.tsx b/steps/03.02-composition-solution/src/app/(dashboard)/employees/[id]/edit/page.tsx new file mode 100644 index 0000000..9d29dc0 --- /dev/null +++ b/steps/03.02-composition-solution/src/app/(dashboard)/employees/[id]/edit/page.tsx @@ -0,0 +1,24 @@ +import EmployeeForm from '@/components/EmployeeForm'; +import PageTitle from '@/components/PageTitle'; + +import employeesData from '@/data/employees.json'; + +const EmployeeDetail = async ({ params }: { params: { id: string } }) => { + const employee = employeesData.find((employee) => employee.id === params.id); + + if (!employee) return Single Employee - Not found; + + return ( + <> + + Single Employee - {employee.firstname} {employee.lastname} | Edit + + +
+ +
+ + ); +}; + +export default EmployeeDetail; diff --git a/steps/03.02-composition-solution/src/app/(dashboard)/employees/[id]/page.tsx b/steps/03.02-composition-solution/src/app/(dashboard)/employees/[id]/page.tsx new file mode 100644 index 0000000..a498c63 --- /dev/null +++ b/steps/03.02-composition-solution/src/app/(dashboard)/employees/[id]/page.tsx @@ -0,0 +1,21 @@ +import PageTitle from '@/components/PageTitle'; +import PersonCard from '@/components/PersonCard'; + +import employeesData from '@/data/employees.json'; + +const EmployeeDetail = async ({ params }: { params: { id: string } }) => { + const employee = employeesData.find((employee) => employee.id === params.id); + + if (!employee) return Single Employee - Not found; + + return ( + <> + + Single Employee - {employee.firstname} {employee.lastname} + + + + ); +}; + +export default EmployeeDetail; diff --git a/steps/03.02-composition-solution/src/app/(dashboard)/employees/new/page.tsx b/steps/03.02-composition-solution/src/app/(dashboard)/employees/new/page.tsx new file mode 100644 index 0000000..f1e5157 --- /dev/null +++ b/steps/03.02-composition-solution/src/app/(dashboard)/employees/new/page.tsx @@ -0,0 +1,18 @@ +import EmployeeForm from '@/components/EmployeeForm'; +import PageTitle from '@/components/PageTitle'; + +const EmployeeDetail = async () => { + return ( + <> + + Employees | Create + + +
+ +
+ + ); +}; + +export default EmployeeDetail; diff --git a/steps/03.02-composition-solution/src/app/(dashboard)/employees/page.tsx b/steps/03.02-composition-solution/src/app/(dashboard)/employees/page.tsx new file mode 100644 index 0000000..2a356a7 --- /dev/null +++ b/steps/03.02-composition-solution/src/app/(dashboard)/employees/page.tsx @@ -0,0 +1,47 @@ +import Link from 'next/link'; + +import Button from '@/components/Button'; +import PageTitle from '@/components/PageTitle'; +import PersonCard from '@/components/PersonCard'; +import Search from '@/components/Search'; + +import employeesData from '@/data/employees.json'; + +const Employees = async ({ searchParams }: { searchParams: { search?: string } }) => { + const search = searchParams.search || ''; + const employees = employeesData.filter((employee) => + `${employee.firstname} ${employee.lastname}`.toLowerCase().includes(search.toLowerCase()) + ); + + return ( +
+ Employees +
+ + +
+
+ {employees?.map((employee) => ( + + + +
+ } + /> + ))} +
+ + ); +}; + +export default Employees; diff --git a/steps/03.02-composition-solution/src/app/(dashboard)/expenses/[id]/page.tsx b/steps/03.02-composition-solution/src/app/(dashboard)/expenses/[id]/page.tsx new file mode 100644 index 0000000..bda58e2 --- /dev/null +++ b/steps/03.02-composition-solution/src/app/(dashboard)/expenses/[id]/page.tsx @@ -0,0 +1,18 @@ +import ExpenseDetails from '@/components/ExpensesDetails'; +import PageTitle from '@/components/PageTitle'; + +import expensesData from '@/data/expenses.json'; +import { Expense } from '@/types'; + +const SingleExpense = ({ params }: { params: { id: string } }) => { + const expense = expensesData.find((expense) => expense.id === params.id); + + return ( + <> + Single Expense - {expense?.label || 'Not found'} + {expense && } + + ); +}; + +export default SingleExpense; diff --git a/steps/03.02-composition-solution/src/app/(dashboard)/expenses/page.tsx b/steps/03.02-composition-solution/src/app/(dashboard)/expenses/page.tsx new file mode 100644 index 0000000..55d1d65 --- /dev/null +++ b/steps/03.02-composition-solution/src/app/(dashboard)/expenses/page.tsx @@ -0,0 +1,17 @@ +import ExpensesTable from '@/components/ExpensesTable'; +import PageTitle from '@/components/PageTitle'; + +import { Expense } from '@/types'; + +import expensesData from '@/data/expenses.json'; + +const Expenses = async () => { + return ( + <> + Expenses + } /> + + ); +}; + +export default Expenses; diff --git a/steps/03.02-composition-solution/src/app/(dashboard)/layout.tsx b/steps/03.02-composition-solution/src/app/(dashboard)/layout.tsx new file mode 100644 index 0000000..5e00f8c --- /dev/null +++ b/steps/03.02-composition-solution/src/app/(dashboard)/layout.tsx @@ -0,0 +1,36 @@ +import { Metadata } from 'next'; +import Link from 'next/link'; + +import { promises as fs } from 'fs'; +import path from 'path'; + +import NavigationMenu from '@/components/NavigationMenu'; + +import Logo from '@/components/Logo'; + +type DashboardLayoutProps = { children: React.ReactNode }; + +export const metadata: Metadata = { + title: 'SFEIR People | Dashboard', +}; + +const DashboardLayout: React.FC = async ({ children }) => { + const packageJsonPath = path.join(process.cwd(), 'package.json'); + const packageJsonContent = await fs.readFile(packageJsonPath, 'utf-8'); + const packageJson = JSON.parse(packageJsonContent); + + return ( +
+
+ + + + +
Version: {packageJson.version}
+
+
{children}
+
+ ); +}; + +export default DashboardLayout; diff --git a/steps/03.02-composition-solution/src/app/(dashboard)/page.tsx b/steps/03.02-composition-solution/src/app/(dashboard)/page.tsx new file mode 100644 index 0000000..f581ebb --- /dev/null +++ b/steps/03.02-composition-solution/src/app/(dashboard)/page.tsx @@ -0,0 +1,11 @@ +import PageTitle from '@/components/PageTitle'; + +const HomePage = () => { + return ( + <> + SFEIR People + + ); +}; + +export default HomePage; diff --git a/steps/03.02-composition-solution/src/app/layout.tsx b/steps/03.02-composition-solution/src/app/layout.tsx new file mode 100644 index 0000000..fca9a4d --- /dev/null +++ b/steps/03.02-composition-solution/src/app/layout.tsx @@ -0,0 +1,24 @@ +import type { Metadata } from 'next'; +import { Inter } from 'next/font/google'; + +const inter = Inter({ subsets: ['latin'] }); + +import '@/styles/global.css'; +import ThemeProvider from '@/components/ThemeProvider'; + +export const metadata: Metadata = { + title: 'SFEIR People', + description: 'SFEIR People dashboard application', +}; + +const RootLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => { + return ( + + + {children} + + + ); +}; + +export default RootLayout; diff --git a/steps/03.02-composition-solution/src/assets/images/profile-placeholder.jpg b/steps/03.02-composition-solution/src/assets/images/profile-placeholder.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6fa00ea6c9e371e542006bb4bd69ae5e6922a324 GIT binary patch literal 11940 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 m>aIBzX~|65y0*1u%UVegt+JNI)Z4`imH;jI + + + + + + + + diff --git a/steps/03.02-composition-solution/src/assets/svg/logoDark.svg b/steps/03.02-composition-solution/src/assets/svg/logoDark.svg new file mode 100644 index 0000000..06eb6ed --- /dev/null +++ b/steps/03.02-composition-solution/src/assets/svg/logoDark.svg @@ -0,0 +1,28 @@ + + + + + + + + + diff --git a/steps/03.02-composition-solution/src/components/Alert.tsx b/steps/03.02-composition-solution/src/components/Alert.tsx new file mode 100644 index 0000000..1c90895 --- /dev/null +++ b/steps/03.02-composition-solution/src/components/Alert.tsx @@ -0,0 +1,17 @@ +import clsx from 'clsx'; + +type AlertProps = { + children: React.ReactNode; + className?: string; +}; + +const Alert: React.FC = ({ children, className }) => ( +
+ {children} +
+); + +export default Alert; diff --git a/steps/03.02-composition-solution/src/components/Button.tsx b/steps/03.02-composition-solution/src/components/Button.tsx new file mode 100644 index 0000000..2cee623 --- /dev/null +++ b/steps/03.02-composition-solution/src/components/Button.tsx @@ -0,0 +1,34 @@ +import clsx from 'clsx'; + +export type ButtonProps = { + children: React.ReactNode; + className?: string; + variant?: 'primary' | 'secondary'; + component?: C; +} & Omit, 'className' | 'variant'>; + +const classNames = { + primary: 'inline-block text-white bg-blue-700 hover:bg-blue-800 font-medium rounded-lg text-sm px-5 py-2.5', + secondary: [ + 'inline-block py-2.5 px-5 text-sm font-medium text-slate-900 bg-white rounded-lg border border-gray-200', + 'hover:bg-gray-100 hover:text-blue-700', + 'dark:bg-slate-900 dark:text-white dark:hover:bg-slate-950 dark:hover:text-blue-200 dark:hover:border-blue-200', + ].join(' '), +}; + +const Button = ({ + children, + className, + variant = 'secondary', + component, + ...restProps +}: ButtonProps) => { + const Component = component || 'button'; + return ( + + {children} + + ); +}; + +export default Button; diff --git a/steps/03.02-composition-solution/src/components/EmployeeForm.tsx b/steps/03.02-composition-solution/src/components/EmployeeForm.tsx new file mode 100644 index 0000000..dc15055 --- /dev/null +++ b/steps/03.02-composition-solution/src/components/EmployeeForm.tsx @@ -0,0 +1,113 @@ +'use client'; + +import Image from 'next/image'; +import TextField from '@/components/TextField'; +import { Person } from '@/types'; +import { useFormState } from 'react-dom'; + +import placeholderImage from '@/assets/images/profile-placeholder.jpg'; +import Button from './Button'; + +type ActionState = { + validationErrors?: { [key: string]: Array }; +}; + +type Action = (id: string, formData: FormData) => Promise; + +type EmployeeFormProps = { + employee?: Person; + action?: Action; + className?: string; +}; + +const initialState = { + validationErrors: {}, +} as ActionState; + +const EmployeeForm: React.FC = ({ employee, action, className }) => { + // @ts-ignore + const [state, formAction] = useFormState(action, initialState as unknown as void); + + return ( +
+
+ {employee +
+
+
+ + + + + +
+
+ + + +
+
+
+ +
+
+ ); +}; + +export default EmployeeForm; diff --git a/steps/03.02-composition-solution/src/components/ExpensesDetails.tsx b/steps/03.02-composition-solution/src/components/ExpensesDetails.tsx new file mode 100644 index 0000000..f59b51a --- /dev/null +++ b/steps/03.02-composition-solution/src/components/ExpensesDetails.tsx @@ -0,0 +1,56 @@ +import { Expense } from '@/types'; +import Paper from './Paper'; + +type ExpenseDetailsRowProps = { + label: string; + value: string; +}; + +const ExpenseDetailsRow: React.FC = ({ label, value }) => ( +
+ {label} + {value} +
+); + +type ExpenseDetailsProps = { + expense: Expense; +}; + +const ExpenseDetails: React.FC = ({ expense }) => ( + <> +
+
+

Information

+ + + + + +
+
+

Workflow

+ + + + + +
+
+
+
+

Amount

+ + + + + +
+
+ +); + +export default ExpenseDetails; diff --git a/steps/03.02-composition-solution/src/components/ExpensesTable.tsx b/steps/03.02-composition-solution/src/components/ExpensesTable.tsx new file mode 100644 index 0000000..029c13e --- /dev/null +++ b/steps/03.02-composition-solution/src/components/ExpensesTable.tsx @@ -0,0 +1,51 @@ +import { Expense } from '@/types'; +import clsx from 'clsx'; +import ExpensesTableRow from './ExpensesTableRow'; + +type ExpensesTableProps = { + expenses: Array; +}; + +const ExpensesTable: React.FC = ({ expenses }) => { + return ( + + + + + + + + + + + {expenses.map((expense, index) => ( + + + + + + + ))} + +
+ Label + + Creation date + + Category + + Price +
{expense.label}{new Date(expense.creationDate).toLocaleDateString()}{expense.category} + {expense.price.priceIncludingTax} {expense.price.currency} +
+ ); +}; + +export default ExpensesTable; diff --git a/steps/03.02-composition-solution/src/components/ExpensesTableRow.tsx b/steps/03.02-composition-solution/src/components/ExpensesTableRow.tsx new file mode 100644 index 0000000..3691c1d --- /dev/null +++ b/steps/03.02-composition-solution/src/components/ExpensesTableRow.tsx @@ -0,0 +1,31 @@ +'use client'; + +import { Expense } from '@/types'; +import { useRouter } from 'next/navigation'; + +type ExpensesTableRowProps = { + expense: Expense; + className: string; + children: React.ReactNode; +}; + +const ExpensesTableRow: React.FC = ({ expense, className, children }) => { + const router = useRouter(); + + const handleClick = (expenseId: string) => () => { + router.push(`/expenses/${expenseId}`); + }; + + return ( + + {children} + + ); +}; + +export default ExpensesTableRow; diff --git a/steps/03.02-composition-solution/src/components/Icons/ArrowLeft.tsx b/steps/03.02-composition-solution/src/components/Icons/ArrowLeft.tsx new file mode 100644 index 0000000..1cc10c7 --- /dev/null +++ b/steps/03.02-composition-solution/src/components/Icons/ArrowLeft.tsx @@ -0,0 +1,25 @@ +type ArrowLeftProps = { + className?: string; +}; + +const ArrowLeft: React.FC = ({ className }) => ( + +); + +export default ArrowLeft; diff --git a/steps/03.02-composition-solution/src/components/Icons/Eye.tsx b/steps/03.02-composition-solution/src/components/Icons/Eye.tsx new file mode 100644 index 0000000..04beb91 --- /dev/null +++ b/steps/03.02-composition-solution/src/components/Icons/Eye.tsx @@ -0,0 +1,11 @@ +type EyeProps = { + className?: string; +}; + +const Eye: React.FC = ({ className }) => ( + + + +); + +export default Eye; diff --git a/steps/03.02-composition-solution/src/components/Icons/Loader.tsx b/steps/03.02-composition-solution/src/components/Icons/Loader.tsx new file mode 100644 index 0000000..9c81994 --- /dev/null +++ b/steps/03.02-composition-solution/src/components/Icons/Loader.tsx @@ -0,0 +1,25 @@ +type LoaderProps = { + className?: string; +}; + +const Loader: React.FC = ({ className }) => ( + +); + +export default Loader; diff --git a/steps/03.02-composition-solution/src/components/Logo.tsx b/steps/03.02-composition-solution/src/components/Logo.tsx new file mode 100644 index 0000000..e7a3eda --- /dev/null +++ b/steps/03.02-composition-solution/src/components/Logo.tsx @@ -0,0 +1,17 @@ +'use client'; + +import { useContext } from 'react'; + +import { ThemeContext } from './ThemeProvider'; +import Image from 'next/image'; + +import logo from '@/assets/svg/logo.svg'; +import logoDark from '@/assets/svg/logoDark.svg'; + +const Logo = () => { + const theme = useContext(ThemeContext); + + return People logo; +}; + +export default Logo; diff --git a/steps/03.02-composition-solution/src/components/NavigationItem.tsx b/steps/03.02-composition-solution/src/components/NavigationItem.tsx new file mode 100644 index 0000000..9f68e10 --- /dev/null +++ b/steps/03.02-composition-solution/src/components/NavigationItem.tsx @@ -0,0 +1,30 @@ +'use client'; + +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; + +import clsx from 'clsx'; + +type NavigationItemsProps = { + href: string; + children: React.ReactNode; +}; + +const NavigationItem: React.FC = ({ href, children }) => { + const pathname = usePathname(); + + return ( + + {children} + + ); +}; + +export default NavigationItem; diff --git a/steps/03.02-composition-solution/src/components/NavigationMenu.tsx b/steps/03.02-composition-solution/src/components/NavigationMenu.tsx new file mode 100644 index 0000000..4b778c6 --- /dev/null +++ b/steps/03.02-composition-solution/src/components/NavigationMenu.tsx @@ -0,0 +1,21 @@ +import NavigationItem from './NavigationItem'; + +const NavigationMenu = () => { + return ( + + ); +}; + +export default NavigationMenu; diff --git a/steps/03.02-composition-solution/src/components/PageTitle.tsx b/steps/03.02-composition-solution/src/components/PageTitle.tsx new file mode 100644 index 0000000..217fe69 --- /dev/null +++ b/steps/03.02-composition-solution/src/components/PageTitle.tsx @@ -0,0 +1,25 @@ +import Link from 'next/link'; + +import ArrowLeft from './Icons/ArrowLeft'; + +type PageTitleProps = { + children: React.ReactNode; + backHref?: string; +}; + +const PageTitle: React.FC = ({ children, backHref }) => ( +
+ {backHref && ( + + + Go back + + )} +

{children}

+
+); + +export default PageTitle; diff --git a/steps/03.02-composition-solution/src/components/Pagination.tsx b/steps/03.02-composition-solution/src/components/Pagination.tsx new file mode 100644 index 0000000..be9a43a --- /dev/null +++ b/steps/03.02-composition-solution/src/components/Pagination.tsx @@ -0,0 +1,95 @@ +'use client'; + +import clsx from 'clsx'; +import Link from 'next/link'; +import { usePathname, useSearchParams } from 'next/navigation'; + +type PaginationProps = { + totalPages: number; + className?: string; +}; + +type PaginationShortcutProps = { + href: string; + disabled?: boolean; + className?: string; + children: React.ReactNode; +}; + +const PaginationShortcut: React.FC = ({ href, disabled, className, children }) => { + const classNames = clsx( + 'block text-center px-3 py-2 ms-0 border bg-white dark:bg-slate-900', + className, + !disabled && + 'hover:bg-gray-100 hover:text-gray-700 text-gray-500 border-gray-300 dark:text-white dark:border-gray-700 dark:hover:bg-slate-950 dark:hover:text-white', + disabled && 'text-gray-300 border-gray-200 dark:text-gray-500 dark:border-gray-600' + ); + + if (disabled) return
{children}
; + + return ( + + {children} + + ); +}; + +const Pagination: React.FC = ({ totalPages, className }) => { + const params = useSearchParams(); + const pathname = usePathname(); + + const currentPage = Number(params.get('page')) || 1; + + const getPageUrl = (page: number): string => { + const newParams = new URLSearchParams(params); + newParams.set('page', page.toString()); + return `${pathname}?${newParams.toString()}`; + }; + + return ( + + ); +}; + +export default Pagination; diff --git a/steps/03.02-composition-solution/src/components/Paper.tsx b/steps/03.02-composition-solution/src/components/Paper.tsx new file mode 100644 index 0000000..8ba656e --- /dev/null +++ b/steps/03.02-composition-solution/src/components/Paper.tsx @@ -0,0 +1,14 @@ +import clsx from 'clsx'; + +type PaperProps = React.HTMLAttributes & { + children: React.ReactNode; + rounded?: boolean; +}; + +const Paper: React.FC = ({ children, rounded = true, ...restProps }) => ( +
+ {children} +
+); + +export default Paper; diff --git a/steps/03.02-composition-solution/src/components/PersonCard.tsx b/steps/03.02-composition-solution/src/components/PersonCard.tsx new file mode 100644 index 0000000..b38ee53 --- /dev/null +++ b/steps/03.02-composition-solution/src/components/PersonCard.tsx @@ -0,0 +1,43 @@ +import Image from 'next/image'; + +import { Person } from '@/types'; + +import placeholderImage from '@/assets/images/profile-placeholder.jpg'; + +type PersonCardProps = React.HTMLAttributes & { + person: Person; + actions?: React.ReactNode; + compact?: boolean; +}; + +const PersonCard: React.FC = ({ person, actions, className, compact = false }) => { + return ( +
+
+ {`Picture + + {person.firstname} {person.lastname} + + {person.position} +
+ + {!compact && ( +
+ {person.phone} + {person.email} + {person.manager && {person.manager}} +
+ )} + + {actions &&
{actions}
} +
+ ); +}; + +export default PersonCard; diff --git a/steps/03.02-composition-solution/src/components/Search.tsx b/steps/03.02-composition-solution/src/components/Search.tsx new file mode 100644 index 0000000..98fe032 --- /dev/null +++ b/steps/03.02-composition-solution/src/components/Search.tsx @@ -0,0 +1,43 @@ +'use client'; + +import { debounce } from '@/functions/timing'; +import clsx from 'clsx'; +import { usePathname, useRouter, useSearchParams } from 'next/navigation'; + +const Search = ({ ...restProps }) => { + const router = useRouter(); + const params = useSearchParams(); + const pathname = usePathname(); + + const handleChange = debounce((event: React.ChangeEvent) => { + const value = event.target?.value; + const newParams = new URLSearchParams(params); + newParams.delete('page'); + if (value) newParams.set('search', value); + else newParams.delete('search'); + router.replace(`${pathname}?${newParams.toString()}`); + }, 200); + + return ( + <> + + + + ); +}; + +export default Search; diff --git a/steps/03.02-composition-solution/src/components/TextField.tsx b/steps/03.02-composition-solution/src/components/TextField.tsx new file mode 100644 index 0000000..d8061e9 --- /dev/null +++ b/steps/03.02-composition-solution/src/components/TextField.tsx @@ -0,0 +1,31 @@ +import clsx from 'clsx'; + +type TextFieldProps = React.InputHTMLAttributes & { + label: string; + id: string; + type?: string; + className?: string; + errorMessages?: Array; +}; + +const TextField: React.FC = ({ label, id, type = 'text', className, errorMessages, ...restProps }) => { + return ( +
+ + + {errorMessages?.length &&

{errorMessages[0]}

} +
+ ); +}; + +export default TextField; diff --git a/steps/03.02-composition-solution/src/components/ThemeProvider.tsx b/steps/03.02-composition-solution/src/components/ThemeProvider.tsx new file mode 100644 index 0000000..9081b72 --- /dev/null +++ b/steps/03.02-composition-solution/src/components/ThemeProvider.tsx @@ -0,0 +1,31 @@ +'use client'; + +import { createContext, useEffect, useState } from 'react'; + +export const ThemeContext = createContext('light'); + +type ThemeProviderProps = { + children: React.ReactNode; +}; + +const ThemeProvider: React.FC = ({ children }) => { + const [theme, setTheme] = useState('light'); + + useEffect(() => { + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (event) => { + if (event.matches) { + setTheme('dark'); + } else { + setTheme('light'); + } + }); + + if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { + setTheme('dark'); + } + }, []); + + return {children}; +}; + +export default ThemeProvider; diff --git a/steps/03.02-composition-solution/src/data/employees.json b/steps/03.02-composition-solution/src/data/employees.json new file mode 100644 index 0000000..5f5342e --- /dev/null +++ b/steps/03.02-composition-solution/src/data/employees.json @@ -0,0 +1,152 @@ +[ + { + "id": "5763cd4d9d2a4f259b53c901", + "photo": "/portraits/women/85.jpg", + "firstname": "Leanne", + "lastname": "Woodard", + "position": "Developer", + "entryDate": "27/10/2015", + "birthDate": "02/01/1974", + "gender": "f", + "email": "woodard.l@acme.com", + "phone": "0784112248", + "isManager": false, + "manager": "Erika", + "managerId": "5763cd4d3b57c672861bfa1f" + }, + { + "id": "5763cd4d51fdb6588742f99e", + "photo": "/portraits/men/56.jpg", + "firstname": "Castaneda", + "lastname": "Salinas", + "position": "Developer", + "entryDate": "04/10/2015", + "birthDate": "22/01/1963", + "gender": "m", + "email": "salinas.c@acme.com", + "phone": "0145652522", + "isManager": false, + "manager": "Erika", + "managerId": "5763cd4d3b57c672861bfa1f" + }, + { + "id": "5763cd4dba6362a3f92c954e", + "photo": "/portraits/women/24.jpg", + "firstname": "Phyllis", + "lastname": "Donovan", + "position": "Sales", + "entryDate": "30/03/2015", + "birthDate": "30/11/1951", + "gender": "f", + "email": "donovan.p@acme.com", + "phone": "0685230125", + "isManager": false, + "manager": "Erika", + "managerId": "5763cd4d3b57c672861bfa1f" + }, + { + "id": "5763cd4d3b57c672861bfa1f", + "photo": "/portraits/women/65.jpg", + "firstname": "Erika", + "lastname": "Guzman", + "position": "Product Owner", + "entryDate": "13/05/2016", + "birthDate": "19/03/1962", + "gender": "f", + "email": "guzman.e@acme.com", + "phone": "0678412587", + "isManager": true, + "manager": "Mercedes", + "managerId": "5763cd4d979b62a209809160" + }, + { + "id": "5763cd4d5fc36e4f842ca5a9", + "photo": "/portraits/men/30.jpg", + "firstname": "Moody", + "lastname": "Prince", + "position": "Developer", + "entryDate": "28/09/2015", + "birthDate": "15/04/1971", + "gender": "m", + "email": "prince.m@acme.com", + "phone": "0662589632", + "isManager": false, + "manager": "Mercedes", + "managerId": "5763cd4d979b62a209809160" + }, + { + "id": "5763cd4d979b62a209809160", + "photo": "/portraits/women/8.jpg", + "firstname": "Mercedes", + "lastname": "Hebert", + "position": "Product Owner", + "entryDate": "02/01/2016", + "birthDate": "20/07/1947", + "gender": "f", + "email": "hebert.m@acme.com", + "phone": "0125878522", + "isManager": true, + "manager": "Mclaughlin", + "managerId": "5763cd4dfa6f96cd26c65787" + }, + { + "id": "5763cd4d15e6c2c28b70f2e8", + "photo": "/portraits/men/86.jpg", + "firstname": "Howell", + "lastname": "Mcknight", + "position": "Sales", + "entryDate": "26/09/2015", + "birthDate": "18/07/1979", + "gender": "m", + "email": "mcknight.h@acme.com", + "phone": "0456987425", + "isManager": false, + "manager": "Mclaughlin", + "managerId": "5763cd4dfa6f96cd26c65787" + }, + { + "id": "5763cd4d5d6ad8dfc6c34883", + "photo": "/portraits/women/93.jpg", + "firstname": "Lizzie", + "lastname": "Morris", + "position": "Human Resources", + "entryDate": "03/05/2016", + "birthDate": "15/11/1981", + "gender": "f", + "email": "morris.l@acme.com", + "phone": "0662259988", + "isManager": false, + "manager": "Mclaughlin", + "managerId": "5763cd4dfa6f96cd26c65787" + }, + { + "id": "5763cd4dc378a38ecd387737", + "photo": "/portraits/men/34.jpg", + "firstname": "Roy", + "lastname": "Nielsen", + "position": "Sales", + "entryDate": "17/05/2016", + "birthDate": "21/10/1951", + "gender": "m", + "email": "nielsen.r@acme.com", + "phone": "0755669551", + "isManager": false, + "manager": "Mclaughlin", + "managerId": "5763cd4dfa6f96cd26c65787" + }, + { + "id": "5763cd4dfa6f96cd26c65787", + "photo": "/portraits/men/78.jpg", + "firstname": "Mclaughlin", + "lastname": "Cochran", + "position": "Director", + "entryDate": "11/04/2016", + "birthDate": "19/03/1973", + "gender": "m", + "email": "cochran.m@acme.com", + "phone": "0266334856", + "isManager": true, + "manager": "", + "managerId": "" + } +] diff --git a/steps/03.02-composition-solution/src/data/expenses.json b/steps/03.02-composition-solution/src/data/expenses.json new file mode 100644 index 0000000..0d096dc --- /dev/null +++ b/steps/03.02-composition-solution/src/data/expenses.json @@ -0,0 +1,342 @@ +[ + { + "id": "0475830f-a563-44e0-8c5c-6d829c11a132", + "employeeId": "5763cd4d51fdb6588742f99e", + "price": { + "priceIncludingTax": 120.50, + "taxAmount": 20.50, + "priceExcludingTax": 100, + "currency": "EUR" + }, + "label": "Business Lunch", + "description": "Lunch with a client to discuss a new project.", + "category": "Meals", + "receiptLink": "https://example.com/receipt1.pdf", + "status": "approved", + "creationDate": "2024-03-15T09:30:00Z", + "updateDate": "2024-03-18T14:45:00Z" + }, + { + "id": "a2e8b2c4-99d8-4c13-9a9c-0a8a68623260", + "employeeId": "5763cd4d51fdb6588742f99e", + "price": { + "priceIncludingTax": 55.20, + "taxAmount": 7.20, + "priceExcludingTax": 48, + "currency": "USD" + }, + "label": "Office Supplies", + "description": "Purchase of paper, pens, etc.", + "category": "Supplies", + "receiptLink": "https://example.com/receipt2.jpg", + "status": "created", + "creationDate": "2024-05-02T11:20:00Z", + "updateDate": "2024-05-02T11:20:00Z" + }, + { + "id": "3d3fb561-0d9c-4021-8285-d2f49c40c47d", + "employeeId": "5763cd4d51fdb6588742f99e", + "price": { + "priceIncludingTax": 250, + "taxAmount": 0, + "priceExcludingTax": 250, + "currency": "EUR" + }, + "label": "Plane Ticket", + "description": "Business trip to Berlin.", + "category": "Travel", + "receiptLink": "https://example.com/receipt3.png", + "status": "declined", + "creationDate": "2024-04-10T16:45:00Z", + "updateDate": "2024-04-12T09:30:00Z" + }, + { + "id": "4c41689c-e5f5-4029-a181-38c498e7a82b", + "employeeId": "5763cd4d5fc36e4f842ca5a9", + "price": { + "priceIncludingTax": 80.00, + "taxAmount": 13.33, + "priceExcludingTax": 66.67, + "currency": "USD" + }, + "label": "Hotel", + "description": "Hotel night in London.", + "category": "Accommodation", + "receiptLink": "https://example.com/receipt4.pdf", + "status": "approved", + "creationDate": "2024-06-20T08:15:00Z", + "updateDate": "2024-06-22T10:30:00Z" + }, + { + "id": "871030e0-e485-41d5-882c-30201429a23f", + "employeeId": "5763cd4d5fc36e4f842ca5a9", + "price": { + "priceIncludingTax": 35.75, + "taxAmount": 5.75, + "priceExcludingTax": 30, + "currency": "EUR" + }, + "label": "Taxi Fare", + "description": "Ride from the airport to the office.", + "category": "Transportation", + "receiptLink": "https://example.com/receipt5.jpg", + "status": "submitted", + "creationDate": "2024-07-05T19:00:00Z", + "updateDate": "2024-07-05T19:00:00Z" + }, + { + "id": "e38e7368-8686-4211-a6a2-844806839a4c", + "employeeId": "5763cd4d979b62a209809160", + "price": { + "priceIncludingTax": 180.00, + "taxAmount": 30.00, + "priceExcludingTax": 150, + "currency": "USD" + }, + "label": "Car Rental", + "description": "Car rental for a week.", + "category": "Transportation", + "receiptLink": "https://example.com/receipt6.png", + "status": "approved", + "creationDate": "2024-02-28T13:40:00Z", + "updateDate": "2024-03-02T11:15:00Z" + }, + { + "id": "90432a7b-3009-491a-832b-a4622721a490", + "employeeId": "5763cd4d15e6c2c28b70f2e8", + "price": { + "priceIncludingTax": 65.00, + "taxAmount": 10.83, + "priceExcludingTax": 54.17, + "currency": "USD" + }, + "label": "Client Meal", + "description": "Dinner with a potential client.", + "category": "Meals", + "receiptLink": "https://example.com/receipt7.pdf", + "status": "in_review", + "creationDate": "2024-07-18T20:30:00Z", + "updateDate": "2024-07-19T09:00:00Z" + }, + { + "id": "a88a026a-e928-48b6-8a1d-90d980e7a423", + "employeeId": "5763cd4d5d6ad8dfc6c34883", + "price": { + "priceIncludingTax": 95.99, + "taxAmount": 15.99, + "priceExcludingTax": 80, + "currency": "EUR" + }, + "label": "Software", + "description": "Monthly subscription to project management software.", + "category": "Software", + "receiptLink": "https://example.com/receipt8.jpg", + "status": "approved", + "creationDate": "2024-05-10T10:00:00Z", + "updateDate": "2024-05-11T14:20:00Z" + }, + { + "id": "21e5615a-9c2a-40f2-a489-0c2f4a866098", + "employeeId": "5763cd4dc378a38ecd387737", + "price": { + "priceIncludingTax": 25.50, + "taxAmount": 4.25, + "priceExcludingTax": 21.25, + "currency": "USD" + }, + "label": "Parking Fees", + "description": "Parking fees at the airport.", + "category": "Transportation", + "receiptLink": "https://example.com/receipt9.png", + "status": "created", + "creationDate": "2024-07-30T17:45:00Z", + "updateDate": "2024-07-30T17:45:00Z" + }, + { + "id": "5200c69a-e387-4292-9282-98e66878524c", + "employeeId": "5763cd4dfa6f96cd26c65787", + "price": { + "priceIncludingTax": 150.00, + "taxAmount": 25.00, + "priceExcludingTax": 125, + "currency": "USD" + }, + "label": "Training", + "description": "Participation in professional training.", + "category": "Training", + "receiptLink": "https://example.com/receipt10.pdf", + "status": "approved", + "creationDate": "2024-04-05T09:00:00Z", + "updateDate": "2024-04-07T11:30:00Z" + }, + { + "id": "9b60a0a2-d2cf-41ab-a8a6-690080e77898", + "employeeId": "5763cd4d9d2a4f259b53c901", + "price": { + "priceIncludingTax": 75.20, + "taxAmount": 12.53, + "priceExcludingTax": 62.67, + "currency": "EUR" + }, + "label": "Office Supplies", + "description": "Purchase of office supplies.", + "category": "Supplies", + "receiptLink": "https://example.com/receipt11.jpg", + "status": "approved", + "creationDate": "2024-06-12T14:20:00Z", + "updateDate": "2024-06-14T10:15:00Z" + }, + { + "id": "8478041f-7a41-46f0-9158-34759c409873", + "employeeId": "5763cd4d51fdb6588742f99e", + "price": { + "priceIncludingTax": 280.00, + "taxAmount": 46.67, + "priceExcludingTax": 233.33, + "currency": "USD" + }, + "label": "Plane Ticket", + "description": "Business trip to New York.", + "category": "Travel", + "receiptLink": "https://example.com/receipt12.png", + "status": "submitted", + "creationDate": "2024-07-25T11:30:00Z", + "updateDate": "2024-07-25T11:30:00Z" + }, + { + "id": "4902854f-2049-4498-9597-74823829501f", + "employeeId": "5763cd4dba6362a3f92c954e", + "price": { + "priceIncludingTax": 95.00, + "taxAmount": 15.83, + "priceExcludingTax": 79.17, + "currency": "EUR" + }, + "label": "Hotel", + "description": "Hotel night in Paris.", + "category": "Accommodation", + "receiptLink": "https://example.com/receipt13.pdf", + "status": "in_review", + "creationDate": "2024-08-01T08:45:00Z", + "updateDate": "2024-08-02T10:00:00Z" + }, + { + "id": "4309573f-8903-4a42-a095-839529490582", + "employeeId": "5763cd4d3b57c672861bfa1f", + "price": { + "priceIncludingTax": 42.50, + "taxAmount": 7.08, + "priceExcludingTax": 35.42, + "currency": "EUR" + }, + "label": "Taxi Fare", + "description": "Ride from the station to the conference venue.", + "category": "Transportation", + "receiptLink": "https://example.com/receipt14.jpg", + "status": "approved", + "creationDate": "2024-05-20T18:30:00Z", + "updateDate": "2024-05-22T09:15:00Z" + }, + { + "id": "3028759f-9023-4a83-a785-928375928375", + "employeeId": "5763cd4d5fc36e4f842ca5a9", + "price": { + "priceIncludingTax": 190.00, + "taxAmount": 31.67, + "priceExcludingTax": 158.33, + "currency": "USD" + }, + "label": "Car Rental", + "description": "Weekend car rental.", + "category": "Transportation", + "receiptLink": "https://example.com/receipt15.png", + "status": "created", + "creationDate": "2024-07-28T12:00:00Z", + "updateDate": "2024-07-28T12:00:00Z" + }, + { + "id": "92837592-8375-9283-7592-837592837592", + "employeeId": "5763cd4d979b62a209809160", + "price": { + "priceIncludingTax": 70.00, + "taxAmount": 11.67, + "priceExcludingTax": 58.33, + "currency": "USD" + }, + "label": "Client Meal", + "description": "Lunch with a client to discuss a project.", + "category": "Meals", + "receiptLink": "https://example.com/receipt16.pdf", + "status": "declined", + "creationDate": "2024-03-08T13:15:00Z", + "updateDate": "2024-03-10T09:30:00Z" + }, + { + "id": "85930583-0385-9305-8303-859305830385", + "employeeId": "5763cd4d15e6c2c28b70f2e8", + "price": { + "priceIncludingTax": 110.50, + "taxAmount": 18.42, + "priceExcludingTax": 92.08, + "currency": "EUR" + }, + "label": "Software", + "description": "Purchase of a license for design software.", + "category": "Software", + "receiptLink": "https://example.com/receipt17.jpg", + "status": "approved", + "creationDate": "2024-06-05T10:45:00Z", + "updateDate": "2024-06-07T14:30:00Z" + }, + { + "id": "03958305-8305-9385-0385-930583059385", + "employeeId": "5763cd4d5d6ad8dfc6c34883", + "price": { + "priceIncludingTax": 35.00, + "taxAmount": 5.83, + "priceExcludingTax": 29.17, + "currency": "USD" + }, + "label": "Parking Fees", + "description": "Parking fees at the conference center.", + "category": "Transportation", + "receiptLink": "https://example.com/receipt18.png", + "status": "submitted", + "creationDate": "2024-07-15T16:20:00Z", + "updateDate": "2024-07-15T16:20:00Z" + }, + { + "id": "93850395-8503-9585-0395-850395850395", + "employeeId": "5763cd4dc378a38ecd387737", + "price": { + "priceIncludingTax": 165.00, + "taxAmount": 27.50, + "priceExcludingTax": 137.50, + "currency": "USD" + }, + "label": "Training", + "description": "Registration for a webinar on digital marketing.", + "category": "Training", + "receiptLink": "https://example.com/receipt19.pdf", + "status": "in_review", + "creationDate": "2024-07-10T09:00:00Z", + "updateDate": "2024-07-11T14:30:00Z" + }, + { + "id": "85039585-0395-8503-9585-039585039585", + "employeeId": "5763cd4dfa6f96cd26c65787", + "price": { + "priceIncludingTax": 82.75, + "taxAmount": 13.79, + "priceExcludingTax": 68.96, + "currency": "EUR" + }, + "label": "Office Supplies", + "description": "Purchase of ink cartridges for the printer.", + "category": "Supplies", + "receiptLink": "https://example.com/receipt20.jpg", + "status": "approved", + "creationDate": "2024-06-28T11:45:00Z", + "updateDate": "2024-06-30T09:15:00Z" + } +] \ No newline at end of file diff --git a/steps/03.02-composition-solution/src/functions/timing.ts b/steps/03.02-composition-solution/src/functions/timing.ts new file mode 100644 index 0000000..3b8c6f3 --- /dev/null +++ b/steps/03.02-composition-solution/src/functions/timing.ts @@ -0,0 +1,7 @@ +export const debounce = (fn: Function, ms = 300) => { + let timeoutId: ReturnType; + return function (this: any, ...args: any[]) { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => fn.apply(this, args), ms); + }; +}; diff --git a/steps/03.02-composition-solution/src/styles/global.css b/steps/03.02-composition-solution/src/styles/global.css new file mode 100644 index 0000000..f77ed90 --- /dev/null +++ b/steps/03.02-composition-solution/src/styles/global.css @@ -0,0 +1,41 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --color-bg-global: #e9effc; + --color-bg-primary: #ffffff; + --color-bg-secondary: #f5f5f5; + --color-text-primary: #000000; + + --spacing-sm: 0.5rem; + --spacing-md: 0.75rem; + --spacing-lg: 1rem; + --spacing-xl: 1.5rem; +} + +/* Headings */ + +.heading1 { + font-size: 2rem; + font-weight: bold; + margin-bottom: var(--spacing-lg); +} + +.heading2 { + font-size: 1.5rem; + font-weight: bold; + margin-bottom: var(--spacing-lg); +} + +.heading3 { + font-size: 1.125rem; + font-weight: bold; + margin-bottom: var(--spacing-lg); +} + +.heading4 { + font-size: 1rem; + font-weight: bold; + margin-bottom: var(--spacing-lg); +} diff --git a/steps/03.02-composition-solution/src/types.ts b/steps/03.02-composition-solution/src/types.ts new file mode 100644 index 0000000..82cffb5 --- /dev/null +++ b/steps/03.02-composition-solution/src/types.ts @@ -0,0 +1,39 @@ +export type Person = { + id: string; + photo?: string; + firstname: string; + lastname: string; + position: string; + entryDate: string; + birthDate: string; + gender: string; + email: string; + phone: string; + isManager: boolean; + manager?: string; + managerId?: string; +}; + +export type Expense = { + id: string; + employeeId: string; + price: { + priceIncludingTax: number; + taxAmount: number; + priceExcludingTax: number; + currency: string; + }; + label: string; + description: string; + category: string; + receiptLink: string; + status: 'approved' | 'created' | 'declined'; + creationDate: string; + updateDate: string; +}; + +export type PaginationAttributes = { + per_page?: number; + page: number; + total_pages: number; +}; diff --git a/steps/03.02-composition-solution/tailwind.config.js b/steps/03.02-composition-solution/tailwind.config.js new file mode 100644 index 0000000..a5a0b69 --- /dev/null +++ b/steps/03.02-composition-solution/tailwind.config.js @@ -0,0 +1,8 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ['./src/**/*.{js,ts,jsx,tsx,mdx}'], + theme: { + extend: {}, + }, + plugins: [], +}; diff --git a/steps/03.02-composition-solution/tsconfig.json b/steps/03.02-composition-solution/tsconfig.json new file mode 100644 index 0000000..7b28589 --- /dev/null +++ b/steps/03.02-composition-solution/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/steps/03.02-composition/.env.example b/steps/03.02-composition/.env.example new file mode 100644 index 0000000..1ebabff --- /dev/null +++ b/steps/03.02-composition/.env.example @@ -0,0 +1,2 @@ +API_BASE_URL=http://localhost:3001 +API_KEY=XXXX diff --git a/steps/03.02-composition/.eslintrc.json b/steps/03.02-composition/.eslintrc.json new file mode 100644 index 0000000..bffb357 --- /dev/null +++ b/steps/03.02-composition/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/steps/03.02-composition/.gitignore b/steps/03.02-composition/.gitignore new file mode 100644 index 0000000..fd3dbb5 --- /dev/null +++ b/steps/03.02-composition/.gitignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/steps/03.02-composition/README.md b/steps/03.02-composition/README.md new file mode 100644 index 0000000..c7109d2 --- /dev/null +++ b/steps/03.02-composition/README.md @@ -0,0 +1 @@ +# 03.02 - Server Components - Composition diff --git a/steps/03.02-composition/next.config.mjs b/steps/03.02-composition/next.config.mjs new file mode 100644 index 0000000..16343f6 --- /dev/null +++ b/steps/03.02-composition/next.config.mjs @@ -0,0 +1,15 @@ +const apiUrl = new URL(process.env.API_BASE_URL || 'http://localhost:3001'); + +/** @type {import('next').NextConfig} */ +const nextConfig = { + images: { + remotePatterns: [ + { + hostname: apiUrl.hostname, + port: apiUrl.port, + }, + ], + }, +}; + +export default nextConfig; diff --git a/steps/03.02-composition/package.json b/steps/03.02-composition/package.json new file mode 100644 index 0000000..164d3d7 --- /dev/null +++ b/steps/03.02-composition/package.json @@ -0,0 +1,38 @@ +{ + "name": "03.02", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "bright": "^0.8.5", + "clsx": "^2.1.1", + "jose": "^5.6.3", + "jsonwebtoken": "^9.0.2", + "next": "14.2.5", + "react": "^18", + "react-dom": "^18", + "react-error-boundary": "^4.0.13", + "rehype-sanitize": "^6.0.0", + "rehype-stringify": "^10.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.0", + "server-only": "^0.0.1", + "showdown": "^2.1.0", + "unified": "^11.0.5" + }, + "devDependencies": { + "@types/jsonwebtoken": "^9.0.6", + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "@types/showdown": "^2.0.6", + "eslint": "^8", + "eslint-config-next": "14.2.5", + "typescript": "^5" + } +} \ No newline at end of file diff --git a/steps/03.02-composition/postcss.config.js b/steps/03.02-composition/postcss.config.js new file mode 100644 index 0000000..33ad091 --- /dev/null +++ b/steps/03.02-composition/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/steps/03.02-composition/public/next.svg b/steps/03.02-composition/public/next.svg new file mode 100644 index 0000000..5174b28 --- /dev/null +++ b/steps/03.02-composition/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/steps/03.02-composition/public/portraits/men/30.jpg b/steps/03.02-composition/public/portraits/men/30.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d04b7a2669245212620be0cbb17922fd1cb3cbfe GIT binary patch literal 4349 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 w3$0jQtjS=6rPW;hG!!51^p@HCR)|Jncu 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 literal 0 HcmV?d00001 diff --git a/steps/03.02-composition/public/portraits/men/78.jpg b/steps/03.02-composition/public/portraits/men/78.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6438e80b9b56fcf7e6e0e9a02eb8cc54a53c91aa GIT binary patch literal 4643 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 Y-IZTg@->$i4XtycyzDV!v4pAr0qF|a9RL6T literal 0 HcmV?d00001 diff --git a/steps/03.02-composition/public/portraits/men/86.jpg b/steps/03.02-composition/public/portraits/men/86.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9358491105b401a359ef698035ab9b05b84e0457 GIT binary patch literal 5433 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 Kdoq>(yYN42Z*!dh literal 0 HcmV?d00001 diff --git a/steps/03.02-composition/public/portraits/women/65.jpg b/steps/03.02-composition/public/portraits/women/65.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3cab57987a6ff10c5639fad656fb0fc77ba569c9 GIT binary patch literal 5972 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 C^!eWa literal 0 HcmV?d00001 diff --git a/steps/03.02-composition/public/portraits/women/85.jpg b/steps/03.02-composition/public/portraits/women/85.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0a900f9e8874eddf5978f08e6de03815f23bb1ea GIT binary patch literal 3912 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 literal 0 HcmV?d00001 diff --git a/steps/03.02-composition/public/portraits/women/93.jpg b/steps/03.02-composition/public/portraits/women/93.jpg new file mode 100644 index 0000000000000000000000000000000000000000..81ea0613898f679ad77f8e4563a93be893f38a07 GIT binary patch literal 4871 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 \ No newline at end of file diff --git a/steps/03.02-composition/src/app/(auth)/layout.tsx b/steps/03.02-composition/src/app/(auth)/layout.tsx new file mode 100644 index 0000000..cac31a7 --- /dev/null +++ b/steps/03.02-composition/src/app/(auth)/layout.tsx @@ -0,0 +1,12 @@ +type AuthLayoutProps = { + children: React.ReactNode; +}; + +const AuthLayout: React.FC = ({ children }) => ( +
+
+
{children}
+
+); + +export default AuthLayout; diff --git a/steps/03.02-composition/src/app/(auth)/login/page.tsx b/steps/03.02-composition/src/app/(auth)/login/page.tsx new file mode 100644 index 0000000..3ae0596 --- /dev/null +++ b/steps/03.02-composition/src/app/(auth)/login/page.tsx @@ -0,0 +1,30 @@ +import { Metadata } from 'next'; + +import TextField from '@/components/TextField'; +import Button from '@/components/Button'; + +export const metadata: Metadata = { + title: 'SFEIR People | Login', +}; + +const LoginPage = () => { + return ( +
+

Welcome !

+ + + + + ); +}; + +export default LoginPage; diff --git a/steps/03.02-composition/src/app/(dashboard)/employees/[id]/edit/page.tsx b/steps/03.02-composition/src/app/(dashboard)/employees/[id]/edit/page.tsx new file mode 100644 index 0000000..9d29dc0 --- /dev/null +++ b/steps/03.02-composition/src/app/(dashboard)/employees/[id]/edit/page.tsx @@ -0,0 +1,24 @@ +import EmployeeForm from '@/components/EmployeeForm'; +import PageTitle from '@/components/PageTitle'; + +import employeesData from '@/data/employees.json'; + +const EmployeeDetail = async ({ params }: { params: { id: string } }) => { + const employee = employeesData.find((employee) => employee.id === params.id); + + if (!employee) return Single Employee - Not found; + + return ( + <> + + Single Employee - {employee.firstname} {employee.lastname} | Edit + + +
+ +
+ + ); +}; + +export default EmployeeDetail; diff --git a/steps/03.02-composition/src/app/(dashboard)/employees/[id]/page.tsx b/steps/03.02-composition/src/app/(dashboard)/employees/[id]/page.tsx new file mode 100644 index 0000000..a498c63 --- /dev/null +++ b/steps/03.02-composition/src/app/(dashboard)/employees/[id]/page.tsx @@ -0,0 +1,21 @@ +import PageTitle from '@/components/PageTitle'; +import PersonCard from '@/components/PersonCard'; + +import employeesData from '@/data/employees.json'; + +const EmployeeDetail = async ({ params }: { params: { id: string } }) => { + const employee = employeesData.find((employee) => employee.id === params.id); + + if (!employee) return Single Employee - Not found; + + return ( + <> + + Single Employee - {employee.firstname} {employee.lastname} + + + + ); +}; + +export default EmployeeDetail; diff --git a/steps/03.02-composition/src/app/(dashboard)/employees/new/page.tsx b/steps/03.02-composition/src/app/(dashboard)/employees/new/page.tsx new file mode 100644 index 0000000..f1e5157 --- /dev/null +++ b/steps/03.02-composition/src/app/(dashboard)/employees/new/page.tsx @@ -0,0 +1,18 @@ +import EmployeeForm from '@/components/EmployeeForm'; +import PageTitle from '@/components/PageTitle'; + +const EmployeeDetail = async () => { + return ( + <> + + Employees | Create + + +
+ +
+ + ); +}; + +export default EmployeeDetail; diff --git a/steps/03.02-composition/src/app/(dashboard)/employees/page.tsx b/steps/03.02-composition/src/app/(dashboard)/employees/page.tsx new file mode 100644 index 0000000..2a356a7 --- /dev/null +++ b/steps/03.02-composition/src/app/(dashboard)/employees/page.tsx @@ -0,0 +1,47 @@ +import Link from 'next/link'; + +import Button from '@/components/Button'; +import PageTitle from '@/components/PageTitle'; +import PersonCard from '@/components/PersonCard'; +import Search from '@/components/Search'; + +import employeesData from '@/data/employees.json'; + +const Employees = async ({ searchParams }: { searchParams: { search?: string } }) => { + const search = searchParams.search || ''; + const employees = employeesData.filter((employee) => + `${employee.firstname} ${employee.lastname}`.toLowerCase().includes(search.toLowerCase()) + ); + + return ( +
+ Employees +
+ + +
+
+ {employees?.map((employee) => ( + + + +
+ } + /> + ))} +
+ + ); +}; + +export default Employees; diff --git a/steps/03.02-composition/src/app/(dashboard)/expenses/[id]/page.tsx b/steps/03.02-composition/src/app/(dashboard)/expenses/[id]/page.tsx new file mode 100644 index 0000000..bda58e2 --- /dev/null +++ b/steps/03.02-composition/src/app/(dashboard)/expenses/[id]/page.tsx @@ -0,0 +1,18 @@ +import ExpenseDetails from '@/components/ExpensesDetails'; +import PageTitle from '@/components/PageTitle'; + +import expensesData from '@/data/expenses.json'; +import { Expense } from '@/types'; + +const SingleExpense = ({ params }: { params: { id: string } }) => { + const expense = expensesData.find((expense) => expense.id === params.id); + + return ( + <> + Single Expense - {expense?.label || 'Not found'} + {expense && } + + ); +}; + +export default SingleExpense; diff --git a/steps/03.02-composition/src/app/(dashboard)/expenses/page.tsx b/steps/03.02-composition/src/app/(dashboard)/expenses/page.tsx new file mode 100644 index 0000000..55d1d65 --- /dev/null +++ b/steps/03.02-composition/src/app/(dashboard)/expenses/page.tsx @@ -0,0 +1,17 @@ +import ExpensesTable from '@/components/ExpensesTable'; +import PageTitle from '@/components/PageTitle'; + +import { Expense } from '@/types'; + +import expensesData from '@/data/expenses.json'; + +const Expenses = async () => { + return ( + <> + Expenses + } /> + + ); +}; + +export default Expenses; diff --git a/steps/03.02-composition/src/app/(dashboard)/layout.tsx b/steps/03.02-composition/src/app/(dashboard)/layout.tsx new file mode 100644 index 0000000..f6e4708 --- /dev/null +++ b/steps/03.02-composition/src/app/(dashboard)/layout.tsx @@ -0,0 +1,37 @@ +import { Metadata } from 'next'; +import Link from 'next/link'; +import Image from 'next/image'; + +import { promises as fs } from 'fs'; +import path from 'path'; + +import NavigationMenu from '@/components/NavigationMenu'; + +import logo from '@/assets/svg/logo.svg'; + +type DashboardLayoutProps = { children: React.ReactNode }; + +export const metadata: Metadata = { + title: 'SFEIR People | Dashboard', +}; + +const DashboardLayout: React.FC = async ({ children }) => { + const packageJsonPath = path.join(process.cwd(), 'package.json'); + const packageJsonContent = await fs.readFile(packageJsonPath, 'utf-8'); + const packageJson = JSON.parse(packageJsonContent); + + return ( +
+
+ + People logo + + +
Version: {packageJson.version}
+
+
{children}
+
+ ); +}; + +export default DashboardLayout; diff --git a/steps/03.02-composition/src/app/(dashboard)/page.tsx b/steps/03.02-composition/src/app/(dashboard)/page.tsx new file mode 100644 index 0000000..f581ebb --- /dev/null +++ b/steps/03.02-composition/src/app/(dashboard)/page.tsx @@ -0,0 +1,11 @@ +import PageTitle from '@/components/PageTitle'; + +const HomePage = () => { + return ( + <> + SFEIR People + + ); +}; + +export default HomePage; diff --git a/steps/03.02-composition/src/app/layout.tsx b/steps/03.02-composition/src/app/layout.tsx new file mode 100644 index 0000000..e7d90e9 --- /dev/null +++ b/steps/03.02-composition/src/app/layout.tsx @@ -0,0 +1,21 @@ +import type { Metadata } from 'next'; +import { Inter } from 'next/font/google'; + +const inter = Inter({ subsets: ['latin'] }); + +import '@/styles/global.css'; + +export const metadata: Metadata = { + title: 'SFEIR People', + description: 'SFEIR People dashboard application', +}; + +const RootLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => { + return ( + + {children} + + ); +}; + +export default RootLayout; diff --git a/steps/03.02-composition/src/assets/images/profile-placeholder.jpg b/steps/03.02-composition/src/assets/images/profile-placeholder.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6fa00ea6c9e371e542006bb4bd69ae5e6922a324 GIT binary patch literal 11940 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 m>aIBzX~|65y0*1u%UVegt+JNI)Z4`imH;jI + + + + + + + + diff --git a/steps/03.02-composition/src/assets/svg/logoDark.svg b/steps/03.02-composition/src/assets/svg/logoDark.svg new file mode 100644 index 0000000..06eb6ed --- /dev/null +++ b/steps/03.02-composition/src/assets/svg/logoDark.svg @@ -0,0 +1,28 @@ + + + + + + + + + diff --git a/steps/03.02-composition/src/components/Alert.tsx b/steps/03.02-composition/src/components/Alert.tsx new file mode 100644 index 0000000..1c90895 --- /dev/null +++ b/steps/03.02-composition/src/components/Alert.tsx @@ -0,0 +1,17 @@ +import clsx from 'clsx'; + +type AlertProps = { + children: React.ReactNode; + className?: string; +}; + +const Alert: React.FC = ({ children, className }) => ( +
+ {children} +
+); + +export default Alert; diff --git a/steps/03.02-composition/src/components/Button.tsx b/steps/03.02-composition/src/components/Button.tsx new file mode 100644 index 0000000..2cee623 --- /dev/null +++ b/steps/03.02-composition/src/components/Button.tsx @@ -0,0 +1,34 @@ +import clsx from 'clsx'; + +export type ButtonProps = { + children: React.ReactNode; + className?: string; + variant?: 'primary' | 'secondary'; + component?: C; +} & Omit, 'className' | 'variant'>; + +const classNames = { + primary: 'inline-block text-white bg-blue-700 hover:bg-blue-800 font-medium rounded-lg text-sm px-5 py-2.5', + secondary: [ + 'inline-block py-2.5 px-5 text-sm font-medium text-slate-900 bg-white rounded-lg border border-gray-200', + 'hover:bg-gray-100 hover:text-blue-700', + 'dark:bg-slate-900 dark:text-white dark:hover:bg-slate-950 dark:hover:text-blue-200 dark:hover:border-blue-200', + ].join(' '), +}; + +const Button = ({ + children, + className, + variant = 'secondary', + component, + ...restProps +}: ButtonProps) => { + const Component = component || 'button'; + return ( + + {children} + + ); +}; + +export default Button; diff --git a/steps/03.02-composition/src/components/EmployeeForm.tsx b/steps/03.02-composition/src/components/EmployeeForm.tsx new file mode 100644 index 0000000..dc15055 --- /dev/null +++ b/steps/03.02-composition/src/components/EmployeeForm.tsx @@ -0,0 +1,113 @@ +'use client'; + +import Image from 'next/image'; +import TextField from '@/components/TextField'; +import { Person } from '@/types'; +import { useFormState } from 'react-dom'; + +import placeholderImage from '@/assets/images/profile-placeholder.jpg'; +import Button from './Button'; + +type ActionState = { + validationErrors?: { [key: string]: Array }; +}; + +type Action = (id: string, formData: FormData) => Promise; + +type EmployeeFormProps = { + employee?: Person; + action?: Action; + className?: string; +}; + +const initialState = { + validationErrors: {}, +} as ActionState; + +const EmployeeForm: React.FC = ({ employee, action, className }) => { + // @ts-ignore + const [state, formAction] = useFormState(action, initialState as unknown as void); + + return ( +
+
+ {employee +
+
+
+ + + + + +
+
+ + + +
+
+
+ +
+
+ ); +}; + +export default EmployeeForm; diff --git a/steps/03.02-composition/src/components/ExpensesDetails.tsx b/steps/03.02-composition/src/components/ExpensesDetails.tsx new file mode 100644 index 0000000..f59b51a --- /dev/null +++ b/steps/03.02-composition/src/components/ExpensesDetails.tsx @@ -0,0 +1,56 @@ +import { Expense } from '@/types'; +import Paper from './Paper'; + +type ExpenseDetailsRowProps = { + label: string; + value: string; +}; + +const ExpenseDetailsRow: React.FC = ({ label, value }) => ( +
+ {label} + {value} +
+); + +type ExpenseDetailsProps = { + expense: Expense; +}; + +const ExpenseDetails: React.FC = ({ expense }) => ( + <> +
+
+

Information

+ + + + + +
+
+

Workflow

+ + + + + +
+
+
+
+

Amount

+ + + + + +
+
+ +); + +export default ExpenseDetails; diff --git a/steps/03.02-composition/src/components/ExpensesTable.tsx b/steps/03.02-composition/src/components/ExpensesTable.tsx new file mode 100644 index 0000000..1b2e240 --- /dev/null +++ b/steps/03.02-composition/src/components/ExpensesTable.tsx @@ -0,0 +1,61 @@ +'use client'; + +import { Expense } from '@/types'; +import clsx from 'clsx'; +import { useRouter } from 'next/navigation'; + +type ExpensesTableProps = { + expenses: Array; +}; + +const ExpensesTable: React.FC = ({ expenses }) => { + const router = useRouter(); + + const handleClick = (expenseId: string) => () => { + router.push(`/expenses/${expenseId}`); + }; + + return ( + + + + + + + + + + + {expenses.map((expense, index) => ( + + + + + + + ))} + +
+ Label + + Creation date + + Category + + Price +
{expense.label}{new Date(expense.creationDate).toLocaleDateString()}{expense.category} + {expense.price.priceIncludingTax} {expense.price.currency} +
+ ); +}; + +export default ExpensesTable; diff --git a/steps/03.02-composition/src/components/Icons/ArrowLeft.tsx b/steps/03.02-composition/src/components/Icons/ArrowLeft.tsx new file mode 100644 index 0000000..1cc10c7 --- /dev/null +++ b/steps/03.02-composition/src/components/Icons/ArrowLeft.tsx @@ -0,0 +1,25 @@ +type ArrowLeftProps = { + className?: string; +}; + +const ArrowLeft: React.FC = ({ className }) => ( + +); + +export default ArrowLeft; diff --git a/steps/03.02-composition/src/components/Icons/Eye.tsx b/steps/03.02-composition/src/components/Icons/Eye.tsx new file mode 100644 index 0000000..04beb91 --- /dev/null +++ b/steps/03.02-composition/src/components/Icons/Eye.tsx @@ -0,0 +1,11 @@ +type EyeProps = { + className?: string; +}; + +const Eye: React.FC = ({ className }) => ( + + + +); + +export default Eye; diff --git a/steps/03.02-composition/src/components/Icons/Loader.tsx b/steps/03.02-composition/src/components/Icons/Loader.tsx new file mode 100644 index 0000000..9c81994 --- /dev/null +++ b/steps/03.02-composition/src/components/Icons/Loader.tsx @@ -0,0 +1,25 @@ +type LoaderProps = { + className?: string; +}; + +const Loader: React.FC = ({ className }) => ( + +); + +export default Loader; diff --git a/steps/03.02-composition/src/components/NavigationItem.tsx b/steps/03.02-composition/src/components/NavigationItem.tsx new file mode 100644 index 0000000..9f68e10 --- /dev/null +++ b/steps/03.02-composition/src/components/NavigationItem.tsx @@ -0,0 +1,30 @@ +'use client'; + +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; + +import clsx from 'clsx'; + +type NavigationItemsProps = { + href: string; + children: React.ReactNode; +}; + +const NavigationItem: React.FC = ({ href, children }) => { + const pathname = usePathname(); + + return ( + + {children} + + ); +}; + +export default NavigationItem; diff --git a/steps/03.02-composition/src/components/NavigationMenu.tsx b/steps/03.02-composition/src/components/NavigationMenu.tsx new file mode 100644 index 0000000..4b778c6 --- /dev/null +++ b/steps/03.02-composition/src/components/NavigationMenu.tsx @@ -0,0 +1,21 @@ +import NavigationItem from './NavigationItem'; + +const NavigationMenu = () => { + return ( + + ); +}; + +export default NavigationMenu; diff --git a/steps/03.02-composition/src/components/PageTitle.tsx b/steps/03.02-composition/src/components/PageTitle.tsx new file mode 100644 index 0000000..217fe69 --- /dev/null +++ b/steps/03.02-composition/src/components/PageTitle.tsx @@ -0,0 +1,25 @@ +import Link from 'next/link'; + +import ArrowLeft from './Icons/ArrowLeft'; + +type PageTitleProps = { + children: React.ReactNode; + backHref?: string; +}; + +const PageTitle: React.FC = ({ children, backHref }) => ( +
+ {backHref && ( + + + Go back + + )} +

{children}

+
+); + +export default PageTitle; diff --git a/steps/03.02-composition/src/components/Pagination.tsx b/steps/03.02-composition/src/components/Pagination.tsx new file mode 100644 index 0000000..be9a43a --- /dev/null +++ b/steps/03.02-composition/src/components/Pagination.tsx @@ -0,0 +1,95 @@ +'use client'; + +import clsx from 'clsx'; +import Link from 'next/link'; +import { usePathname, useSearchParams } from 'next/navigation'; + +type PaginationProps = { + totalPages: number; + className?: string; +}; + +type PaginationShortcutProps = { + href: string; + disabled?: boolean; + className?: string; + children: React.ReactNode; +}; + +const PaginationShortcut: React.FC = ({ href, disabled, className, children }) => { + const classNames = clsx( + 'block text-center px-3 py-2 ms-0 border bg-white dark:bg-slate-900', + className, + !disabled && + 'hover:bg-gray-100 hover:text-gray-700 text-gray-500 border-gray-300 dark:text-white dark:border-gray-700 dark:hover:bg-slate-950 dark:hover:text-white', + disabled && 'text-gray-300 border-gray-200 dark:text-gray-500 dark:border-gray-600' + ); + + if (disabled) return
{children}
; + + return ( + + {children} + + ); +}; + +const Pagination: React.FC = ({ totalPages, className }) => { + const params = useSearchParams(); + const pathname = usePathname(); + + const currentPage = Number(params.get('page')) || 1; + + const getPageUrl = (page: number): string => { + const newParams = new URLSearchParams(params); + newParams.set('page', page.toString()); + return `${pathname}?${newParams.toString()}`; + }; + + return ( + + ); +}; + +export default Pagination; diff --git a/steps/03.02-composition/src/components/Paper.tsx b/steps/03.02-composition/src/components/Paper.tsx new file mode 100644 index 0000000..8ba656e --- /dev/null +++ b/steps/03.02-composition/src/components/Paper.tsx @@ -0,0 +1,14 @@ +import clsx from 'clsx'; + +type PaperProps = React.HTMLAttributes & { + children: React.ReactNode; + rounded?: boolean; +}; + +const Paper: React.FC = ({ children, rounded = true, ...restProps }) => ( +
+ {children} +
+); + +export default Paper; diff --git a/steps/03.02-composition/src/components/PersonCard.tsx b/steps/03.02-composition/src/components/PersonCard.tsx new file mode 100644 index 0000000..b38ee53 --- /dev/null +++ b/steps/03.02-composition/src/components/PersonCard.tsx @@ -0,0 +1,43 @@ +import Image from 'next/image'; + +import { Person } from '@/types'; + +import placeholderImage from '@/assets/images/profile-placeholder.jpg'; + +type PersonCardProps = React.HTMLAttributes & { + person: Person; + actions?: React.ReactNode; + compact?: boolean; +}; + +const PersonCard: React.FC = ({ person, actions, className, compact = false }) => { + return ( +
+
+ {`Picture + + {person.firstname} {person.lastname} + + {person.position} +
+ + {!compact && ( +
+ {person.phone} + {person.email} + {person.manager && {person.manager}} +
+ )} + + {actions &&
{actions}
} +
+ ); +}; + +export default PersonCard; diff --git a/steps/03.02-composition/src/components/Search.tsx b/steps/03.02-composition/src/components/Search.tsx new file mode 100644 index 0000000..98fe032 --- /dev/null +++ b/steps/03.02-composition/src/components/Search.tsx @@ -0,0 +1,43 @@ +'use client'; + +import { debounce } from '@/functions/timing'; +import clsx from 'clsx'; +import { usePathname, useRouter, useSearchParams } from 'next/navigation'; + +const Search = ({ ...restProps }) => { + const router = useRouter(); + const params = useSearchParams(); + const pathname = usePathname(); + + const handleChange = debounce((event: React.ChangeEvent) => { + const value = event.target?.value; + const newParams = new URLSearchParams(params); + newParams.delete('page'); + if (value) newParams.set('search', value); + else newParams.delete('search'); + router.replace(`${pathname}?${newParams.toString()}`); + }, 200); + + return ( + <> + + + + ); +}; + +export default Search; diff --git a/steps/03.02-composition/src/components/TextField.tsx b/steps/03.02-composition/src/components/TextField.tsx new file mode 100644 index 0000000..d8061e9 --- /dev/null +++ b/steps/03.02-composition/src/components/TextField.tsx @@ -0,0 +1,31 @@ +import clsx from 'clsx'; + +type TextFieldProps = React.InputHTMLAttributes & { + label: string; + id: string; + type?: string; + className?: string; + errorMessages?: Array; +}; + +const TextField: React.FC = ({ label, id, type = 'text', className, errorMessages, ...restProps }) => { + return ( +
+ + + {errorMessages?.length &&

{errorMessages[0]}

} +
+ ); +}; + +export default TextField; diff --git a/steps/03.02-composition/src/data/employees.json b/steps/03.02-composition/src/data/employees.json new file mode 100644 index 0000000..5f5342e --- /dev/null +++ b/steps/03.02-composition/src/data/employees.json @@ -0,0 +1,152 @@ +[ + { + "id": "5763cd4d9d2a4f259b53c901", + "photo": "/portraits/women/85.jpg", + "firstname": "Leanne", + "lastname": "Woodard", + "position": "Developer", + "entryDate": "27/10/2015", + "birthDate": "02/01/1974", + "gender": "f", + "email": "woodard.l@acme.com", + "phone": "0784112248", + "isManager": false, + "manager": "Erika", + "managerId": "5763cd4d3b57c672861bfa1f" + }, + { + "id": "5763cd4d51fdb6588742f99e", + "photo": "/portraits/men/56.jpg", + "firstname": "Castaneda", + "lastname": "Salinas", + "position": "Developer", + "entryDate": "04/10/2015", + "birthDate": "22/01/1963", + "gender": "m", + "email": "salinas.c@acme.com", + "phone": "0145652522", + "isManager": false, + "manager": "Erika", + "managerId": "5763cd4d3b57c672861bfa1f" + }, + { + "id": "5763cd4dba6362a3f92c954e", + "photo": "/portraits/women/24.jpg", + "firstname": "Phyllis", + "lastname": "Donovan", + "position": "Sales", + "entryDate": "30/03/2015", + "birthDate": "30/11/1951", + "gender": "f", + "email": "donovan.p@acme.com", + "phone": "0685230125", + "isManager": false, + "manager": "Erika", + "managerId": "5763cd4d3b57c672861bfa1f" + }, + { + "id": "5763cd4d3b57c672861bfa1f", + "photo": "/portraits/women/65.jpg", + "firstname": "Erika", + "lastname": "Guzman", + "position": "Product Owner", + "entryDate": "13/05/2016", + "birthDate": "19/03/1962", + "gender": "f", + "email": "guzman.e@acme.com", + "phone": "0678412587", + "isManager": true, + "manager": "Mercedes", + "managerId": "5763cd4d979b62a209809160" + }, + { + "id": "5763cd4d5fc36e4f842ca5a9", + "photo": "/portraits/men/30.jpg", + "firstname": "Moody", + "lastname": "Prince", + "position": "Developer", + "entryDate": "28/09/2015", + "birthDate": "15/04/1971", + "gender": "m", + "email": "prince.m@acme.com", + "phone": "0662589632", + "isManager": false, + "manager": "Mercedes", + "managerId": "5763cd4d979b62a209809160" + }, + { + "id": "5763cd4d979b62a209809160", + "photo": "/portraits/women/8.jpg", + "firstname": "Mercedes", + "lastname": "Hebert", + "position": "Product Owner", + "entryDate": "02/01/2016", + "birthDate": "20/07/1947", + "gender": "f", + "email": "hebert.m@acme.com", + "phone": "0125878522", + "isManager": true, + "manager": "Mclaughlin", + "managerId": "5763cd4dfa6f96cd26c65787" + }, + { + "id": "5763cd4d15e6c2c28b70f2e8", + "photo": "/portraits/men/86.jpg", + "firstname": "Howell", + "lastname": "Mcknight", + "position": "Sales", + "entryDate": "26/09/2015", + "birthDate": "18/07/1979", + "gender": "m", + "email": "mcknight.h@acme.com", + "phone": "0456987425", + "isManager": false, + "manager": "Mclaughlin", + "managerId": "5763cd4dfa6f96cd26c65787" + }, + { + "id": "5763cd4d5d6ad8dfc6c34883", + "photo": "/portraits/women/93.jpg", + "firstname": "Lizzie", + "lastname": "Morris", + "position": "Human Resources", + "entryDate": "03/05/2016", + "birthDate": "15/11/1981", + "gender": "f", + "email": "morris.l@acme.com", + "phone": "0662259988", + "isManager": false, + "manager": "Mclaughlin", + "managerId": "5763cd4dfa6f96cd26c65787" + }, + { + "id": "5763cd4dc378a38ecd387737", + "photo": "/portraits/men/34.jpg", + "firstname": "Roy", + "lastname": "Nielsen", + "position": "Sales", + "entryDate": "17/05/2016", + "birthDate": "21/10/1951", + "gender": "m", + "email": "nielsen.r@acme.com", + "phone": "0755669551", + "isManager": false, + "manager": "Mclaughlin", + "managerId": "5763cd4dfa6f96cd26c65787" + }, + { + "id": "5763cd4dfa6f96cd26c65787", + "photo": "/portraits/men/78.jpg", + "firstname": "Mclaughlin", + "lastname": "Cochran", + "position": "Director", + "entryDate": "11/04/2016", + "birthDate": "19/03/1973", + "gender": "m", + "email": "cochran.m@acme.com", + "phone": "0266334856", + "isManager": true, + "manager": "", + "managerId": "" + } +] diff --git a/steps/03.02-composition/src/data/expenses.json b/steps/03.02-composition/src/data/expenses.json new file mode 100644 index 0000000..0d096dc --- /dev/null +++ b/steps/03.02-composition/src/data/expenses.json @@ -0,0 +1,342 @@ +[ + { + "id": "0475830f-a563-44e0-8c5c-6d829c11a132", + "employeeId": "5763cd4d51fdb6588742f99e", + "price": { + "priceIncludingTax": 120.50, + "taxAmount": 20.50, + "priceExcludingTax": 100, + "currency": "EUR" + }, + "label": "Business Lunch", + "description": "Lunch with a client to discuss a new project.", + "category": "Meals", + "receiptLink": "https://example.com/receipt1.pdf", + "status": "approved", + "creationDate": "2024-03-15T09:30:00Z", + "updateDate": "2024-03-18T14:45:00Z" + }, + { + "id": "a2e8b2c4-99d8-4c13-9a9c-0a8a68623260", + "employeeId": "5763cd4d51fdb6588742f99e", + "price": { + "priceIncludingTax": 55.20, + "taxAmount": 7.20, + "priceExcludingTax": 48, + "currency": "USD" + }, + "label": "Office Supplies", + "description": "Purchase of paper, pens, etc.", + "category": "Supplies", + "receiptLink": "https://example.com/receipt2.jpg", + "status": "created", + "creationDate": "2024-05-02T11:20:00Z", + "updateDate": "2024-05-02T11:20:00Z" + }, + { + "id": "3d3fb561-0d9c-4021-8285-d2f49c40c47d", + "employeeId": "5763cd4d51fdb6588742f99e", + "price": { + "priceIncludingTax": 250, + "taxAmount": 0, + "priceExcludingTax": 250, + "currency": "EUR" + }, + "label": "Plane Ticket", + "description": "Business trip to Berlin.", + "category": "Travel", + "receiptLink": "https://example.com/receipt3.png", + "status": "declined", + "creationDate": "2024-04-10T16:45:00Z", + "updateDate": "2024-04-12T09:30:00Z" + }, + { + "id": "4c41689c-e5f5-4029-a181-38c498e7a82b", + "employeeId": "5763cd4d5fc36e4f842ca5a9", + "price": { + "priceIncludingTax": 80.00, + "taxAmount": 13.33, + "priceExcludingTax": 66.67, + "currency": "USD" + }, + "label": "Hotel", + "description": "Hotel night in London.", + "category": "Accommodation", + "receiptLink": "https://example.com/receipt4.pdf", + "status": "approved", + "creationDate": "2024-06-20T08:15:00Z", + "updateDate": "2024-06-22T10:30:00Z" + }, + { + "id": "871030e0-e485-41d5-882c-30201429a23f", + "employeeId": "5763cd4d5fc36e4f842ca5a9", + "price": { + "priceIncludingTax": 35.75, + "taxAmount": 5.75, + "priceExcludingTax": 30, + "currency": "EUR" + }, + "label": "Taxi Fare", + "description": "Ride from the airport to the office.", + "category": "Transportation", + "receiptLink": "https://example.com/receipt5.jpg", + "status": "submitted", + "creationDate": "2024-07-05T19:00:00Z", + "updateDate": "2024-07-05T19:00:00Z" + }, + { + "id": "e38e7368-8686-4211-a6a2-844806839a4c", + "employeeId": "5763cd4d979b62a209809160", + "price": { + "priceIncludingTax": 180.00, + "taxAmount": 30.00, + "priceExcludingTax": 150, + "currency": "USD" + }, + "label": "Car Rental", + "description": "Car rental for a week.", + "category": "Transportation", + "receiptLink": "https://example.com/receipt6.png", + "status": "approved", + "creationDate": "2024-02-28T13:40:00Z", + "updateDate": "2024-03-02T11:15:00Z" + }, + { + "id": "90432a7b-3009-491a-832b-a4622721a490", + "employeeId": "5763cd4d15e6c2c28b70f2e8", + "price": { + "priceIncludingTax": 65.00, + "taxAmount": 10.83, + "priceExcludingTax": 54.17, + "currency": "USD" + }, + "label": "Client Meal", + "description": "Dinner with a potential client.", + "category": "Meals", + "receiptLink": "https://example.com/receipt7.pdf", + "status": "in_review", + "creationDate": "2024-07-18T20:30:00Z", + "updateDate": "2024-07-19T09:00:00Z" + }, + { + "id": "a88a026a-e928-48b6-8a1d-90d980e7a423", + "employeeId": "5763cd4d5d6ad8dfc6c34883", + "price": { + "priceIncludingTax": 95.99, + "taxAmount": 15.99, + "priceExcludingTax": 80, + "currency": "EUR" + }, + "label": "Software", + "description": "Monthly subscription to project management software.", + "category": "Software", + "receiptLink": "https://example.com/receipt8.jpg", + "status": "approved", + "creationDate": "2024-05-10T10:00:00Z", + "updateDate": "2024-05-11T14:20:00Z" + }, + { + "id": "21e5615a-9c2a-40f2-a489-0c2f4a866098", + "employeeId": "5763cd4dc378a38ecd387737", + "price": { + "priceIncludingTax": 25.50, + "taxAmount": 4.25, + "priceExcludingTax": 21.25, + "currency": "USD" + }, + "label": "Parking Fees", + "description": "Parking fees at the airport.", + "category": "Transportation", + "receiptLink": "https://example.com/receipt9.png", + "status": "created", + "creationDate": "2024-07-30T17:45:00Z", + "updateDate": "2024-07-30T17:45:00Z" + }, + { + "id": "5200c69a-e387-4292-9282-98e66878524c", + "employeeId": "5763cd4dfa6f96cd26c65787", + "price": { + "priceIncludingTax": 150.00, + "taxAmount": 25.00, + "priceExcludingTax": 125, + "currency": "USD" + }, + "label": "Training", + "description": "Participation in professional training.", + "category": "Training", + "receiptLink": "https://example.com/receipt10.pdf", + "status": "approved", + "creationDate": "2024-04-05T09:00:00Z", + "updateDate": "2024-04-07T11:30:00Z" + }, + { + "id": "9b60a0a2-d2cf-41ab-a8a6-690080e77898", + "employeeId": "5763cd4d9d2a4f259b53c901", + "price": { + "priceIncludingTax": 75.20, + "taxAmount": 12.53, + "priceExcludingTax": 62.67, + "currency": "EUR" + }, + "label": "Office Supplies", + "description": "Purchase of office supplies.", + "category": "Supplies", + "receiptLink": "https://example.com/receipt11.jpg", + "status": "approved", + "creationDate": "2024-06-12T14:20:00Z", + "updateDate": "2024-06-14T10:15:00Z" + }, + { + "id": "8478041f-7a41-46f0-9158-34759c409873", + "employeeId": "5763cd4d51fdb6588742f99e", + "price": { + "priceIncludingTax": 280.00, + "taxAmount": 46.67, + "priceExcludingTax": 233.33, + "currency": "USD" + }, + "label": "Plane Ticket", + "description": "Business trip to New York.", + "category": "Travel", + "receiptLink": "https://example.com/receipt12.png", + "status": "submitted", + "creationDate": "2024-07-25T11:30:00Z", + "updateDate": "2024-07-25T11:30:00Z" + }, + { + "id": "4902854f-2049-4498-9597-74823829501f", + "employeeId": "5763cd4dba6362a3f92c954e", + "price": { + "priceIncludingTax": 95.00, + "taxAmount": 15.83, + "priceExcludingTax": 79.17, + "currency": "EUR" + }, + "label": "Hotel", + "description": "Hotel night in Paris.", + "category": "Accommodation", + "receiptLink": "https://example.com/receipt13.pdf", + "status": "in_review", + "creationDate": "2024-08-01T08:45:00Z", + "updateDate": "2024-08-02T10:00:00Z" + }, + { + "id": "4309573f-8903-4a42-a095-839529490582", + "employeeId": "5763cd4d3b57c672861bfa1f", + "price": { + "priceIncludingTax": 42.50, + "taxAmount": 7.08, + "priceExcludingTax": 35.42, + "currency": "EUR" + }, + "label": "Taxi Fare", + "description": "Ride from the station to the conference venue.", + "category": "Transportation", + "receiptLink": "https://example.com/receipt14.jpg", + "status": "approved", + "creationDate": "2024-05-20T18:30:00Z", + "updateDate": "2024-05-22T09:15:00Z" + }, + { + "id": "3028759f-9023-4a83-a785-928375928375", + "employeeId": "5763cd4d5fc36e4f842ca5a9", + "price": { + "priceIncludingTax": 190.00, + "taxAmount": 31.67, + "priceExcludingTax": 158.33, + "currency": "USD" + }, + "label": "Car Rental", + "description": "Weekend car rental.", + "category": "Transportation", + "receiptLink": "https://example.com/receipt15.png", + "status": "created", + "creationDate": "2024-07-28T12:00:00Z", + "updateDate": "2024-07-28T12:00:00Z" + }, + { + "id": "92837592-8375-9283-7592-837592837592", + "employeeId": "5763cd4d979b62a209809160", + "price": { + "priceIncludingTax": 70.00, + "taxAmount": 11.67, + "priceExcludingTax": 58.33, + "currency": "USD" + }, + "label": "Client Meal", + "description": "Lunch with a client to discuss a project.", + "category": "Meals", + "receiptLink": "https://example.com/receipt16.pdf", + "status": "declined", + "creationDate": "2024-03-08T13:15:00Z", + "updateDate": "2024-03-10T09:30:00Z" + }, + { + "id": "85930583-0385-9305-8303-859305830385", + "employeeId": "5763cd4d15e6c2c28b70f2e8", + "price": { + "priceIncludingTax": 110.50, + "taxAmount": 18.42, + "priceExcludingTax": 92.08, + "currency": "EUR" + }, + "label": "Software", + "description": "Purchase of a license for design software.", + "category": "Software", + "receiptLink": "https://example.com/receipt17.jpg", + "status": "approved", + "creationDate": "2024-06-05T10:45:00Z", + "updateDate": "2024-06-07T14:30:00Z" + }, + { + "id": "03958305-8305-9385-0385-930583059385", + "employeeId": "5763cd4d5d6ad8dfc6c34883", + "price": { + "priceIncludingTax": 35.00, + "taxAmount": 5.83, + "priceExcludingTax": 29.17, + "currency": "USD" + }, + "label": "Parking Fees", + "description": "Parking fees at the conference center.", + "category": "Transportation", + "receiptLink": "https://example.com/receipt18.png", + "status": "submitted", + "creationDate": "2024-07-15T16:20:00Z", + "updateDate": "2024-07-15T16:20:00Z" + }, + { + "id": "93850395-8503-9585-0395-850395850395", + "employeeId": "5763cd4dc378a38ecd387737", + "price": { + "priceIncludingTax": 165.00, + "taxAmount": 27.50, + "priceExcludingTax": 137.50, + "currency": "USD" + }, + "label": "Training", + "description": "Registration for a webinar on digital marketing.", + "category": "Training", + "receiptLink": "https://example.com/receipt19.pdf", + "status": "in_review", + "creationDate": "2024-07-10T09:00:00Z", + "updateDate": "2024-07-11T14:30:00Z" + }, + { + "id": "85039585-0395-8503-9585-039585039585", + "employeeId": "5763cd4dfa6f96cd26c65787", + "price": { + "priceIncludingTax": 82.75, + "taxAmount": 13.79, + "priceExcludingTax": 68.96, + "currency": "EUR" + }, + "label": "Office Supplies", + "description": "Purchase of ink cartridges for the printer.", + "category": "Supplies", + "receiptLink": "https://example.com/receipt20.jpg", + "status": "approved", + "creationDate": "2024-06-28T11:45:00Z", + "updateDate": "2024-06-30T09:15:00Z" + } +] \ No newline at end of file diff --git a/steps/03.02-composition/src/functions/timing.ts b/steps/03.02-composition/src/functions/timing.ts new file mode 100644 index 0000000..3b8c6f3 --- /dev/null +++ b/steps/03.02-composition/src/functions/timing.ts @@ -0,0 +1,7 @@ +export const debounce = (fn: Function, ms = 300) => { + let timeoutId: ReturnType; + return function (this: any, ...args: any[]) { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => fn.apply(this, args), ms); + }; +}; diff --git a/steps/03.02-composition/src/styles/global.css b/steps/03.02-composition/src/styles/global.css new file mode 100644 index 0000000..f77ed90 --- /dev/null +++ b/steps/03.02-composition/src/styles/global.css @@ -0,0 +1,41 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --color-bg-global: #e9effc; + --color-bg-primary: #ffffff; + --color-bg-secondary: #f5f5f5; + --color-text-primary: #000000; + + --spacing-sm: 0.5rem; + --spacing-md: 0.75rem; + --spacing-lg: 1rem; + --spacing-xl: 1.5rem; +} + +/* Headings */ + +.heading1 { + font-size: 2rem; + font-weight: bold; + margin-bottom: var(--spacing-lg); +} + +.heading2 { + font-size: 1.5rem; + font-weight: bold; + margin-bottom: var(--spacing-lg); +} + +.heading3 { + font-size: 1.125rem; + font-weight: bold; + margin-bottom: var(--spacing-lg); +} + +.heading4 { + font-size: 1rem; + font-weight: bold; + margin-bottom: var(--spacing-lg); +} diff --git a/steps/03.02-composition/src/types.ts b/steps/03.02-composition/src/types.ts new file mode 100644 index 0000000..82cffb5 --- /dev/null +++ b/steps/03.02-composition/src/types.ts @@ -0,0 +1,39 @@ +export type Person = { + id: string; + photo?: string; + firstname: string; + lastname: string; + position: string; + entryDate: string; + birthDate: string; + gender: string; + email: string; + phone: string; + isManager: boolean; + manager?: string; + managerId?: string; +}; + +export type Expense = { + id: string; + employeeId: string; + price: { + priceIncludingTax: number; + taxAmount: number; + priceExcludingTax: number; + currency: string; + }; + label: string; + description: string; + category: string; + receiptLink: string; + status: 'approved' | 'created' | 'declined'; + creationDate: string; + updateDate: string; +}; + +export type PaginationAttributes = { + per_page?: number; + page: number; + total_pages: number; +}; diff --git a/steps/03.02-composition/tailwind.config.js b/steps/03.02-composition/tailwind.config.js new file mode 100644 index 0000000..eaa361c --- /dev/null +++ b/steps/03.02-composition/tailwind.config.js @@ -0,0 +1,9 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ['./src/**/*.{js,ts,jsx,tsx,mdx}'], + darkMode: 'selector', + theme: { + extend: {}, + }, + plugins: [], +}; diff --git a/steps/03.02-composition/tsconfig.json b/steps/03.02-composition/tsconfig.json new file mode 100644 index 0000000..7b28589 --- /dev/null +++ b/steps/03.02-composition/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/steps/04.01-data-fetching-solution/.env.example b/steps/04.01-data-fetching-solution/.env.example new file mode 100644 index 0000000..1ebabff --- /dev/null +++ b/steps/04.01-data-fetching-solution/.env.example @@ -0,0 +1,2 @@ +API_BASE_URL=http://localhost:3001 +API_KEY=XXXX diff --git a/steps/04.01-data-fetching-solution/.eslintrc.json b/steps/04.01-data-fetching-solution/.eslintrc.json new file mode 100644 index 0000000..bffb357 --- /dev/null +++ b/steps/04.01-data-fetching-solution/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/steps/04.01-data-fetching-solution/.gitignore b/steps/04.01-data-fetching-solution/.gitignore new file mode 100644 index 0000000..fd3dbb5 --- /dev/null +++ b/steps/04.01-data-fetching-solution/.gitignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/steps/04.01-data-fetching-solution/README.md b/steps/04.01-data-fetching-solution/README.md new file mode 100644 index 0000000..acd5880 --- /dev/null +++ b/steps/04.01-data-fetching-solution/README.md @@ -0,0 +1 @@ +# 04.01 - Data fetching diff --git a/steps/04.01-data-fetching-solution/next.config.mjs b/steps/04.01-data-fetching-solution/next.config.mjs new file mode 100644 index 0000000..16343f6 --- /dev/null +++ b/steps/04.01-data-fetching-solution/next.config.mjs @@ -0,0 +1,15 @@ +const apiUrl = new URL(process.env.API_BASE_URL || 'http://localhost:3001'); + +/** @type {import('next').NextConfig} */ +const nextConfig = { + images: { + remotePatterns: [ + { + hostname: apiUrl.hostname, + port: apiUrl.port, + }, + ], + }, +}; + +export default nextConfig; diff --git a/steps/04.01-data-fetching-solution/package.json b/steps/04.01-data-fetching-solution/package.json new file mode 100644 index 0000000..963c6fb --- /dev/null +++ b/steps/04.01-data-fetching-solution/package.json @@ -0,0 +1,38 @@ +{ + "name": "04.01-solution", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "bright": "^0.8.5", + "clsx": "^2.1.1", + "jose": "^5.6.3", + "jsonwebtoken": "^9.0.2", + "next": "14.2.5", + "react": "^18", + "react-dom": "^18", + "react-error-boundary": "^4.0.13", + "rehype-sanitize": "^6.0.0", + "rehype-stringify": "^10.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.0", + "server-only": "^0.0.1", + "showdown": "^2.1.0", + "unified": "^11.0.5" + }, + "devDependencies": { + "@types/jsonwebtoken": "^9.0.6", + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "@types/showdown": "^2.0.6", + "eslint": "^8", + "eslint-config-next": "14.2.5", + "typescript": "^5" + } +} \ No newline at end of file diff --git a/steps/04.01-data-fetching-solution/postcss.config.js b/steps/04.01-data-fetching-solution/postcss.config.js new file mode 100644 index 0000000..33ad091 --- /dev/null +++ b/steps/04.01-data-fetching-solution/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/steps/04.01-data-fetching-solution/public/next.svg b/steps/04.01-data-fetching-solution/public/next.svg new file mode 100644 index 0000000..5174b28 --- /dev/null +++ b/steps/04.01-data-fetching-solution/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/steps/04.01-data-fetching-solution/public/portraits/men/30.jpg b/steps/04.01-data-fetching-solution/public/portraits/men/30.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d04b7a2669245212620be0cbb17922fd1cb3cbfe GIT binary patch literal 4349 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 w3$0jQtjS=6rPW;hG!!51^p@HCR)|Jncu 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 literal 0 HcmV?d00001 diff --git a/steps/04.01-data-fetching-solution/public/portraits/men/78.jpg b/steps/04.01-data-fetching-solution/public/portraits/men/78.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6438e80b9b56fcf7e6e0e9a02eb8cc54a53c91aa GIT binary patch literal 4643 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 Y-IZTg@->$i4XtycyzDV!v4pAr0qF|a9RL6T literal 0 HcmV?d00001 diff --git a/steps/04.01-data-fetching-solution/public/portraits/men/86.jpg b/steps/04.01-data-fetching-solution/public/portraits/men/86.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9358491105b401a359ef698035ab9b05b84e0457 GIT binary patch literal 5433 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 Kdoq>(yYN42Z*!dh literal 0 HcmV?d00001 diff --git a/steps/04.01-data-fetching-solution/public/portraits/women/65.jpg b/steps/04.01-data-fetching-solution/public/portraits/women/65.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3cab57987a6ff10c5639fad656fb0fc77ba569c9 GIT binary patch literal 5972 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 C^!eWa literal 0 HcmV?d00001 diff --git a/steps/04.01-data-fetching-solution/public/portraits/women/85.jpg b/steps/04.01-data-fetching-solution/public/portraits/women/85.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0a900f9e8874eddf5978f08e6de03815f23bb1ea GIT binary patch literal 3912 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 literal 0 HcmV?d00001 diff --git a/steps/04.01-data-fetching-solution/public/portraits/women/93.jpg b/steps/04.01-data-fetching-solution/public/portraits/women/93.jpg new file mode 100644 index 0000000000000000000000000000000000000000..81ea0613898f679ad77f8e4563a93be893f38a07 GIT binary patch literal 4871 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 \ No newline at end of file diff --git a/steps/04.01-data-fetching-solution/src/api/common.ts b/steps/04.01-data-fetching-solution/src/api/common.ts new file mode 100644 index 0000000..494935d --- /dev/null +++ b/steps/04.01-data-fetching-solution/src/api/common.ts @@ -0,0 +1,17 @@ +import { ApiError } from './error'; + +export async function fetchJson(url: string, options?: RequestInit): Promise { + const requestOptions = { + ...options, + headers: { + ...options?.headers, + ['x-api-key']: process.env.API_KEY || 'not-set', + ['content-type']: 'application/json', + }, + }; + const response: Response = await fetch(url, requestOptions); + const data: T | unknown = await response.json(); + if (response.ok) return data as T; + + throw new ApiError(response.statusText, data as unknown); +} diff --git a/steps/04.01-data-fetching-solution/src/api/error.ts b/steps/04.01-data-fetching-solution/src/api/error.ts new file mode 100644 index 0000000..bffb65a --- /dev/null +++ b/steps/04.01-data-fetching-solution/src/api/error.ts @@ -0,0 +1,8 @@ +export class ApiError extends Error { + body; + constructor(message: string, body: unknown) { + super(message); + this.name = 'ApiError'; + this.body = body; + } +} diff --git a/steps/04.01-data-fetching-solution/src/api/expenses.ts b/steps/04.01-data-fetching-solution/src/api/expenses.ts new file mode 100644 index 0000000..57ba8f0 --- /dev/null +++ b/steps/04.01-data-fetching-solution/src/api/expenses.ts @@ -0,0 +1,19 @@ +import { Expense } from '@/types'; +import { fetchJson } from './common'; +import qs from 'query-string'; + +const baseUrl = process.env.API_BASE_URL + '/api' || 'http://localhost:3001/api'; +const apiKey = process.env.API_KEY || ''; + +export const findAll = ({ employee }: { employee?: string } = {}): Promise<{ data: Array }> => { + const url = qs.stringifyUrl({ + url: baseUrl + '/expenses', + query: { + employee, + }, + }); + return fetchJson(url, { headers: { ['x-api-key']: apiKey } }); +}; + +export const findOne = (id: string): Promise => + fetchJson(`${baseUrl}/expenses/${id}`, { headers: { ['x-api-key']: apiKey } }); diff --git a/steps/04.01-data-fetching-solution/src/api/people.ts b/steps/04.01-data-fetching-solution/src/api/people.ts new file mode 100644 index 0000000..b4932a7 --- /dev/null +++ b/steps/04.01-data-fetching-solution/src/api/people.ts @@ -0,0 +1,19 @@ +import { Person } from '@/types'; +import { fetchJson } from './common'; +import qs from 'query-string'; + +const baseUrl = process.env.API_BASE_URL + '/api' || 'http://localhost:3001/api'; +const apiKey = process.env.API_KEY || ''; + +export const findAll = ({ search }: { search?: string }): Promise<{ data: Array }> => { + const url = qs.stringifyUrl({ + url: baseUrl + '/people', + query: { + search, + }, + }); + return fetchJson(url, { headers: { ['x-api-key']: apiKey } }); +}; + +export const findOne = (id: string): Promise => + fetchJson(`${baseUrl}/people/${id}`, { headers: { ['x-api-key']: apiKey } }); diff --git a/steps/04.01-data-fetching-solution/src/app/(auth)/layout.tsx b/steps/04.01-data-fetching-solution/src/app/(auth)/layout.tsx new file mode 100644 index 0000000..cac31a7 --- /dev/null +++ b/steps/04.01-data-fetching-solution/src/app/(auth)/layout.tsx @@ -0,0 +1,12 @@ +type AuthLayoutProps = { + children: React.ReactNode; +}; + +const AuthLayout: React.FC = ({ children }) => ( +
+
+
{children}
+
+); + +export default AuthLayout; diff --git a/steps/04.01-data-fetching-solution/src/app/(auth)/login/page.tsx b/steps/04.01-data-fetching-solution/src/app/(auth)/login/page.tsx new file mode 100644 index 0000000..3ae0596 --- /dev/null +++ b/steps/04.01-data-fetching-solution/src/app/(auth)/login/page.tsx @@ -0,0 +1,30 @@ +import { Metadata } from 'next'; + +import TextField from '@/components/TextField'; +import Button from '@/components/Button'; + +export const metadata: Metadata = { + title: 'SFEIR People | Login', +}; + +const LoginPage = () => { + return ( +
+

Welcome !

+ + + + + ); +}; + +export default LoginPage; diff --git a/steps/04.01-data-fetching-solution/src/app/(dashboard)/employees/[id]/edit/page.tsx b/steps/04.01-data-fetching-solution/src/app/(dashboard)/employees/[id]/edit/page.tsx new file mode 100644 index 0000000..e2b65b1 --- /dev/null +++ b/steps/04.01-data-fetching-solution/src/app/(dashboard)/employees/[id]/edit/page.tsx @@ -0,0 +1,24 @@ +import EmployeeForm from '@/components/EmployeeForm'; +import PageTitle from '@/components/PageTitle'; + +import * as peopleApi from '@/api/people'; + +const EmployeeDetail = async ({ params }: { params: { id: string } }) => { + const employee = await peopleApi.findOne(params.id); + + if (!employee) return Single Employee - Not found; + + return ( + <> + + Single Employee - {employee.firstname} {employee.lastname} | Edit + + +
+ +
+ + ); +}; + +export default EmployeeDetail; diff --git a/steps/04.01-data-fetching-solution/src/app/(dashboard)/employees/[id]/page.tsx b/steps/04.01-data-fetching-solution/src/app/(dashboard)/employees/[id]/page.tsx new file mode 100644 index 0000000..ad91c21 --- /dev/null +++ b/steps/04.01-data-fetching-solution/src/app/(dashboard)/employees/[id]/page.tsx @@ -0,0 +1,22 @@ +import PageTitle from '@/components/PageTitle'; +import PersonCard from '@/components/PersonCard'; + +import * as peopleApi from '@/api/people'; +import EmployeeExpenses from '@/components/EmployeeExpenses'; + +const EmployeeDetail = async ({ params }: { params: { id: string } }) => { + const employee = await peopleApi.findOne(params.id); + + if (!employee) return Single Employee - Not found; + + return ( + <> + + Single Employee - {employee.firstname} {employee.lastname} + + } /> + + ); +}; + +export default EmployeeDetail; diff --git a/steps/04.01-data-fetching-solution/src/app/(dashboard)/employees/new/page.tsx b/steps/04.01-data-fetching-solution/src/app/(dashboard)/employees/new/page.tsx new file mode 100644 index 0000000..f1e5157 --- /dev/null +++ b/steps/04.01-data-fetching-solution/src/app/(dashboard)/employees/new/page.tsx @@ -0,0 +1,18 @@ +import EmployeeForm from '@/components/EmployeeForm'; +import PageTitle from '@/components/PageTitle'; + +const EmployeeDetail = async () => { + return ( + <> + + Employees | Create + + +
+ +
+ + ); +}; + +export default EmployeeDetail; diff --git a/steps/04.01-data-fetching-solution/src/app/(dashboard)/employees/page.tsx b/steps/04.01-data-fetching-solution/src/app/(dashboard)/employees/page.tsx new file mode 100644 index 0000000..93c437b --- /dev/null +++ b/steps/04.01-data-fetching-solution/src/app/(dashboard)/employees/page.tsx @@ -0,0 +1,45 @@ +import Link from 'next/link'; + +import Button from '@/components/Button'; +import PageTitle from '@/components/PageTitle'; +import PersonCard from '@/components/PersonCard'; +import Search from '@/components/Search'; + +import * as peopleApi from '@/api/people'; + +const Employees = async ({ searchParams }: { searchParams: { search?: string } }) => { + const search = searchParams.search || undefined; + const employeesData = await peopleApi.findAll({ search }); + + return ( +
+ Employees +
+ + +
+
+ {employeesData?.data?.map((employee) => ( + + + +
+ } + /> + ))} +
+ + ); +}; + +export default Employees; diff --git a/steps/04.01-data-fetching-solution/src/app/(dashboard)/expenses/[id]/page.tsx b/steps/04.01-data-fetching-solution/src/app/(dashboard)/expenses/[id]/page.tsx new file mode 100644 index 0000000..d02e0c7 --- /dev/null +++ b/steps/04.01-data-fetching-solution/src/app/(dashboard)/expenses/[id]/page.tsx @@ -0,0 +1,17 @@ +import ExpenseDetails from '@/components/ExpensesDetails'; +import PageTitle from '@/components/PageTitle'; + +import * as expensesApi from '@/api/expenses'; + +const SingleExpense = async ({ params }: { params: { id: string } }) => { + const expense = await expensesApi.findOne(params.id); + + return ( + <> + Single Expense - {expense?.label || 'Not found'} + {expense && } + + ); +}; + +export default SingleExpense; diff --git a/steps/04.01-data-fetching-solution/src/app/(dashboard)/expenses/page.tsx b/steps/04.01-data-fetching-solution/src/app/(dashboard)/expenses/page.tsx new file mode 100644 index 0000000..8929e32 --- /dev/null +++ b/steps/04.01-data-fetching-solution/src/app/(dashboard)/expenses/page.tsx @@ -0,0 +1,16 @@ +import ExpensesTable from '@/components/ExpensesTable'; +import PageTitle from '@/components/PageTitle'; + +import * as expensesApi from '@/api/expenses'; + +const Expenses = async () => { + const expensesData = await expensesApi.findAll(); + return ( + <> + Expenses + + + ); +}; + +export default Expenses; diff --git a/steps/04.01-data-fetching-solution/src/app/(dashboard)/layout.tsx b/steps/04.01-data-fetching-solution/src/app/(dashboard)/layout.tsx new file mode 100644 index 0000000..f6e4708 --- /dev/null +++ b/steps/04.01-data-fetching-solution/src/app/(dashboard)/layout.tsx @@ -0,0 +1,37 @@ +import { Metadata } from 'next'; +import Link from 'next/link'; +import Image from 'next/image'; + +import { promises as fs } from 'fs'; +import path from 'path'; + +import NavigationMenu from '@/components/NavigationMenu'; + +import logo from '@/assets/svg/logo.svg'; + +type DashboardLayoutProps = { children: React.ReactNode }; + +export const metadata: Metadata = { + title: 'SFEIR People | Dashboard', +}; + +const DashboardLayout: React.FC = async ({ children }) => { + const packageJsonPath = path.join(process.cwd(), 'package.json'); + const packageJsonContent = await fs.readFile(packageJsonPath, 'utf-8'); + const packageJson = JSON.parse(packageJsonContent); + + return ( +
+
+ + People logo + + +
Version: {packageJson.version}
+
+
{children}
+
+ ); +}; + +export default DashboardLayout; diff --git a/steps/04.01-data-fetching-solution/src/app/(dashboard)/page.tsx b/steps/04.01-data-fetching-solution/src/app/(dashboard)/page.tsx new file mode 100644 index 0000000..f581ebb --- /dev/null +++ b/steps/04.01-data-fetching-solution/src/app/(dashboard)/page.tsx @@ -0,0 +1,11 @@ +import PageTitle from '@/components/PageTitle'; + +const HomePage = () => { + return ( + <> + SFEIR People + + ); +}; + +export default HomePage; diff --git a/steps/04.01-data-fetching-solution/src/app/layout.tsx b/steps/04.01-data-fetching-solution/src/app/layout.tsx new file mode 100644 index 0000000..e7d90e9 --- /dev/null +++ b/steps/04.01-data-fetching-solution/src/app/layout.tsx @@ -0,0 +1,21 @@ +import type { Metadata } from 'next'; +import { Inter } from 'next/font/google'; + +const inter = Inter({ subsets: ['latin'] }); + +import '@/styles/global.css'; + +export const metadata: Metadata = { + title: 'SFEIR People', + description: 'SFEIR People dashboard application', +}; + +const RootLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => { + return ( + + {children} + + ); +}; + +export default RootLayout; diff --git a/steps/04.01-data-fetching-solution/src/app/rest/expenses/route.ts b/steps/04.01-data-fetching-solution/src/app/rest/expenses/route.ts new file mode 100644 index 0000000..b26cada --- /dev/null +++ b/steps/04.01-data-fetching-solution/src/app/rest/expenses/route.ts @@ -0,0 +1,9 @@ +import { NextRequest } from 'next/server'; + +import * as expensesApi from '@/api/expenses'; + +export const GET = async (request: NextRequest) => { + const employeeId = request.nextUrl.searchParams.get('employeeId'); + const data = await expensesApi.findAll({ employee: employeeId || undefined }); + return Response.json(data); +}; diff --git a/steps/04.01-data-fetching-solution/src/assets/images/profile-placeholder.jpg b/steps/04.01-data-fetching-solution/src/assets/images/profile-placeholder.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6fa00ea6c9e371e542006bb4bd69ae5e6922a324 GIT binary patch literal 11940 zcmd^kbyQT{_xB7lLyB}aNJw``icZ zuh0AX{PSDuUGKd!>+btGpMCZ|XPUd#f}?@LHa0DwRsKnivOE+znC0C+G2 z9s-7khrlBsz#}4~BO@arA!FY}yMc~}jgOCqjf+c2LQO_UL`95?OU_76MMHa={x$&_ z6Dt!PD>dD1y6=?$5fBiN5s|Twk+J9qaS7@E^>NV%z(9oSh3f?YDFJX8KoAD-q8UI4 z00KbYy}deM&Vqn&urhoY47y$d0KkF3z>9If4HyiE4nhY2fPJXto>#j6OA><9GiKuv zfzHG`SUvVN63RhE;yrgcFiMdm2?s?IZYLh91FZka72w)pJW5=imq6O~iU^DZgmbO# zkQh_a73Y{?%K8T_(xlbbA6Jp{eib9~P5Ba;b=5#6{=tln+S>bay6WGx!MzPLjIH&5 z@ZkSmGvb*gMz zE*1F|BASS-(1q%J1zbs}x4x_s$2GEFAz*@`{?KRUtyjpEq%+>Hy)=ma<_e+DGo?lL z%AvbLE+wE|TDuwaDm<_Pcqj3w(yWC)#s^r~vSwCl(vxyo0YJ#+SZg?V&QjzGx|HCS z;|vVn5^#B5A`mXlR+n9H+9hyJ5ZpGeR2kPFCJz4%f|Bpoehz9f68R1M$CY|*r!vke zg0B2G3Vc)Bp(~~RXEqAqmh)5~XM#&Z?=L<^!n^!~u2|6JSo>Yi&nuFPo8=UbW+2Ni z7~aU8dJi(_`Jb%ccW7>erm?8Z?wBuBe=;b}jPi0;n*P|0-<6~tIcA96Pf>|aEi>_E zgoM_w(vcFqJ75 zG1&I`TXvT@-!xXW65m7=*-fY<5l_2ySXq0LA`IY%lHuVKaYPf&t5kR zrbhqn&h>+2YHobyutzFrRi5_6~a*l-Xd{5uLnE1v^?%I9%QA>; zv9u&B*Wx7rK&ZX%B)1my_zJm_pnajc%G%`(^_LK?Lri#LVMo>_a7}>o{;(USDxYu# znR2*{=Y8Q=y+W=@KJmkg#l|RRmk?-OFM6==nbLno=vOg_tgu5{M(^6vDA(7gv)qp} zdZ~Y1=r&o+&CfFJyu=_wpA3_gxUH zI!Jo7_BQ6Gk)U4NCFi<8`QYW~ay4ukUxAh7EvqKA&)&{nL01w)AmQ8KY0$M!LsGSu z>d_t`Vb9^re*bN3R6^v6{Zq2>q8o?yh<&JPZ_P1B`p@E9ve~v81hLe1W9+tbg2-vg z)Lc6U1fZ1Pb%171?gr6me(a+q5fIq5E_p#>04n-jcy$GigeOvF66iX0Q8DSdnX!+t zYdcSg@f>uoDhq3E%!g}doj+K1U8wG7KdtJrx{+SpznyxYyKgp=v^wD4RW<)Rk}zzC zLyrysf`M>g1lUIBr&Txr5CoRT5P@J~VUts`vT<-erb4F(hwXU~VY?w91nvUhAKWdS z)4rB{zluuxb$;83f$5j~N!7WD2_7WGl9b3&44;K!M}lAVE8ep?r=feOf+LR_Tp}`9 zn4s(Dowh%A^8U+n)Y!K55tGt=hT`{QpP=Voib-fD)qS$3$ByM!CvpnEjE(m>7)+}N zWzyIdyYrSs>wgiC&mx9e<-NQ$mQ9hNx!#bgo@|fW&cE6&x@>amkD1*qKCu*dR(*qF3*CTR0DRb*X4*XG z(H1+c51@~EeU3h2CrfYR>8eA~g&9YYX5uVb>sYq}jwHEcN(ySrHPpJScV~9sx(1om z4~9G9+$IgXiP})YIU>;>9mMoVL8Ig%ESJ((;`ucmW@zTH+t2qXzK+%&8sp3C8F`&U zI^Uc~cL4xu=tRZ`v41l-MZH~OSu$39O72Ao@h_S)PO~m-mg%k0SsH2KM0-b6U^vq-0Jlnj3 zv><$MB^Wz~1cOi5FYmeB4%1^UYIP7RdaKE8x_QJ?ZiZt2*iWZo6oKZpAIvkVhNj(Ykoie@-?M+!L~f8CyeKLSCMOLpx{=2T!R^LJR>2m-azQ{OymMcw zTfjmgG7LwaOhd!_zEr5N4sWP9jwtLd<^H2~p621aL77gb!iZv{(AW=jo$eUHZ4eFz z5|gGUUz>-`?RRjvBW@mbX-3hK zVL89F!$a3uX?=y+-F*qvJtMeU71UrL$0PU({BZGP zbtQs}q`@AZHi`pXQ43d5?$#TTkK`%k%--?>&h3EE+4PSTPrOZmsEr2P3-;{5xpon- z>Vkn9QePLvqJ$VW_o4xhI%-xTQh=3Ivw}vzD{TYSMSDd8BR?_h;Y@=OX|6t5Gg?_j zt0HH8gZ(sv198EAIWskJikP`KU9aUBn7^n@wN^E0uQ7icaapgSo+jK})E<1l5;YK8 zcPa*36{s=3uL>YA?7isMQUrV30f}H>v9eLHiz=XF%9C6FSP)IO`N~V#P(`s2W}#f0~LeSli#B3rSLIDkuh-U(0h6M~JS&^V^!58RPz*!NxDFfkz-A5WYuPl7eabE*XwrTIf%KC%3{ z)qu91S&1#82>zBiwxdCJ-TlW@`&hF2pW=SkDJMFdra3{95`Ux zPj}A;FlHzA3jZCEHIvk2pbP;IDXU4dz+|LzT7#EO6QyUhi`BO|C1HO9YJ~nu4T}I;cYxFwx5n0LGPNyXa6~MbC_}aRy)C@v0Zv&5 zuQd3I>2}*pUrBnMcDq9V6_<0>8%r)5T|ThoXX$bG-TjBE7`rtwu83tdZw&Pi+rQ1H zHF$d;HTuPM;c(VPIuH9h*HNbV2buwuArL`u9`rqy+wpAzna`0H`NqA&$t*#6@ZvLF z<}NyL?0W&U_HDOc9Wf!;>#gbW=~d|QA-amDSneJ%go_LqP;Vgk*si&VFP>}c4vxGu z;hTHk!+iQOJf6idV}Nr7&m;ix?$cogRoJNzpJ6~Np3fvQ!p}#dB$Fd-GLdSg7VQI-7kI9)&SF%IdqGm=sMIizJT-8=gGKQJk9i-#^QL0lCHE1jKIYQndYp?{7}#jrWZP)l z$NS|>C@AIG*hLidyf8&=UXimt24PX%ReX|K zgw7Ej@$0DyFARICVo-zk%wIYlNo+VXxjC9IeAId1_Rg`~(bS8?orK=WbZ9K)Rb32r zNSwHn>Fc^QL-ek6wh<80!w^=#=YAd~7k~tYvzXIk)JE#0C)NDTxZSKmo<0IIyfHy@ z>ADAjWzj6tG}8{NQ@9Iy_G8+h*nrQky8E;U5Er^7xN2E)$Uf~2*Ao{lk~(BKr4-Z{ zN>YeQcdDv7q@=8?Py5sua^k2eiK=|RIHhc<%MW|M$}@c8?WO*OP7q>@bDZ_g#w}M?c{#06tP86xOpej9$7h__d`>FhUGnxg z`nA;Cy|>SR)8zjMSG&AQ!Wy2O?$u9V`*PFn^2v|}hhZ+-!gG~wBhB;N5l+&#QRJMeX6(7e zS{eoOMD=V^y}r-RqK3Zh9rVlXt}q2`yZe$KIG8~K#1Trn(O@sUj&Fo>t*S&T&I1bkucJIw5ri`Qqtp&=f znYJf#&vQJVBTi*amN7r!&8C@PXLtBjfdqxa2Z~~-KRotMx~70846lD^)&-r zqgpH17swFQ-G-;z4bIp$^w^(Ar62)l9-|x1T0AqHH4vq|(Tr*srQhh?-kPKmW543D z1W6>AI8mRzG4E4M@X3J4rdM4NSsG8RD0^t@e&I;+nWu}*L zL^&r44xcu}M|yEbP)Fm}{kBA!QNv1USQ*`GAo%1-^P`TT;({+~;E~9s{r%}I8g4^h z5V75NOOwWYC?mZ`@!_%j9td8y*+-wx1)s5A>aAm*fn$ADi8)0j1M6!cAHF*Xb_f&A zgBO5jSddlcT)eELmGe-31isqY zk9OeYUyJ3()h$?hUKjO>KSt4~G%+*zGp-&xvZl4gb}N&5?(*Kr`^^!5<+q-?)QP<( z?;r8R?rNkGd+SMtj;tV%63zGyAucNk$$TYh&E0%CB@3uk9=2y}7v)Z>)eiTq9;x$T z2x$!yPHY8_Z0mQi#kmU#RMFTodP_t~@I`QlhI>lgxbeu0LJ{$+nzPZ%*VF1L85omU zMT3%Fe2BLi?%Bnr-?HMIin(s1NXreMC`M zD1sb2s+Dq=4KX0<6HcL@>2Y=~dKa0pWP1T>)J@`U!%y15Mq2Xzw|&5Th=lokhy>^Z z@SJ$VV(LO42$6Eq8$T9@(W5+I=Tp^C_iFUS2e*X-fE38Ba3CtMHg;Rl#yTlZ=~)k> zYuyKPi}Ti&tzpOW;r(^~3jo_@h*r+i5aOcG4`k|9zjbnm)dg+ zvU>@3+BCy@-JBNABcg>XA;_BdAdnmC?EI>Y(ToA8eGi`bF3h2A!g&*KQlcAApel20 zy%4?W#EOhQT`%82Ue5Rwr}o2v9tKL0{^#V;qIvI48ldL-ZDy?)9?k^M(Prj5k83Uf zkY<~7D6v}E$$v0 zaGGUw;4#d)c%aa+bZ^%TbCC${js5IfpFI3-;T@FUA2NQt`=eg~{(m^PmUstZF92Kr zzO9=v0?-}-Xa~;BztcV6`k~h&u=B5so?HD=gZ2>q8-p)TzkB)PS10$kil`UiincTEfWFM@E{k1#>_Z> zKWm*OZeuhEl|InFTzkB>LVglR$NdNlZWM~iZKl#G*J0{n)c>c^j+q$xU zN#Fg4WiadyTxic9N80h}Wo_35-9LG8be(Y}|B-v}r?x?RJpNSgpPB~k5&9GL?2j!I zSojln^2)_)&g}O5H+S9R8sSQ-edNEX7l7nHxIL9*#(D`GW|D^H%G}a8D#DHb);NXCeYq?bnHW3O|QFM6) zJ)3KZRoI4%05TiYgD&()?o05%oKFiI*2^)nNF=coal{p&PbV$L(}LdT?n67%_rO3& zYvd=loGf%vkr6uVRrVk=_PxEhWj5y~#-~#)smTtHdX>x6s6^#?oc&&+Po>&3YJZ`v z>~N2%2y<(uge3aoem4AyQ}(qktVm_oL1ot!F$qnVIAq~iqjTqYs9~xBJvap!=>REA zrrnKB;Dpj{PNrjI6CHAk<#r4=P=iX~v%tB>T>}?Zjy>UoBm89yLI;vQu+6~SyT;GL z%2c}_o9qKRogOCNS{h#rj#FMN%I ztA=NWlP4tq)OGA+``pTK3%Pb%#h%TA49U-ty^{f+4FF)BPBe(!pDrZ>z_sxe+xqT@vvlzUSrBWm$|vb1L~!@DUA`9hMEx{V#RS(da7 zxu<$S`2hgGA-0ifd;D}tHxD`IpmM|H4pH5KiN{pcYZ2YODb8ZlkOA>t?x_h->Rl%b zS!67ii3-`;&#oW0+9Ji}p3qaEAEMmfSJa)&JG_&3Cgp--%lbW!T>5&?)RrOt-$c}h?Tb{_4xY#Ew zsG@qIdYB(4Q38A^KQt^=_;L5zVYgxXZ@1Kceje~8$zNudmGlu91O~Jm*1-b7gbw?V z2!tU1h{TUj05Jd*Y@+fu-_xY8QorY5SRa$m!SXOaKQqAS-$T;O-Kd#Om%p@~lfzR$ zrPlGt4Lh_q8CyRzY&n5z=(Lx=-fd`i7>!z6y5<>SAVY;)_U?l^S>NY_xaTU?`P3vc zIAH&I*Iobs4`B}ADY5gOS`ur#0H^-l$N3Dh5^z=W8V_6oxBg0$L^&&?saQ1v^#-@3qMv?S$$QGp?hKn8nH)}KQeGsp8r3h?KAi>(5FsLZ4To{-J|Z^H`=X&Q z;c))dc(e*bKnWZM8l@U6@E{B?@~vEe3chq^sT;A3G?- zj?V*4bz5P*0LTvR8?avhEPMA>gdGpv4TK$!+*i0804NRTW2l8vUb7xD%i0a5@5Stp z`e?2AM|Hfom{T0=va&l1UK^WC%%`I#S=PMcuOSv6+qT>%pJ!qesF-}AHv=lS+{Jj~ z7P<~wy6{yOD^p2t@@G_8`aW{E)|QG^SN&Ee5HV7l-nsEX<6zJ^c;8M^?y1Y?3AT6d zVrq;9r71I-V<`V>FrJ8cN68$eMcWSh;;d+wO;_R zThk~Xg_1Q5VWPaT9G+Rx0CxpeY0pGqaMPdh^pR@^eSEMsa2Sz-TnY@4>Uy2lxP`4D z?dXUM$eyKkRz^S1IugH?WwUU1;anZDR+U&+wV%=p!&geuX$R~cgq45p0B@JxnY)IA zEpq{Qb54-Pa@v^D?cfYaG>Q^_(rH>1vjY*KF9gzW=1}mA4+MSbIgEJUl=vVnf^0|_ zDdV0mUI0=Dw|q^Y-~ICe4|+%Fu`qqBn$fv zT8P%_O(`{iggJ_2$-2}ZXUM84u)RW-n5L>aCa|-_*(g$qNhIUEmje4Tw+kT0C?c#G zvy68+rObUUqrsW{P#Kr;MKW;NpI3yO8;pS|5vlkl3D5EU0X*7~)BD2(?BgIhiA8&l zxEHHtgKl=-g0fMh^Ys@1=Dn2g=C8~fA^hSQVXOwVMmOWiuxK{m`iGS8apk3|!sfmGwj}sP zDBr9C6CH{V2X&@`ty1H3lB59ftHS8qdw#A>H^nNKOVb{Ztc9^n(iOj^^5f_Y3bkv)v6)9lVD0r|_)-yaa78 z$_#dis&7xZk=q9&I6KZha<5pOavmJSKi%)qacN_Y2LeOyeescudf zbiA?U?9YevwaV6alO`7#uf&JH@b<%DLPe6rjl!-oab%9t%=lyEcq?#~;kTq6xl%G$ ztI;?#=dDpfIZca+kFaHKv}`5)#Imv=4R6{t?Ks8VvT$Cl)a5}4>4_=%^hrzZzG%%s zn5#*szzKAW*KT71WwnK4sz(MY*lsm(lX{MiN{}EVh0n(_c9ul1dU2vhFg7ibyzov( zho#O_FCXc|MkyT@we`eOCnV};HM*HpJW`(~;vblZ(w@=K(xlXej3&|}Oj^EHx-pC} z$rvL>7;((=WG@xFZvRd7I3sx#CCyv~1>YdLSAEQTtGHmy#fHj0aa34I>p0&Su!C0+85{WLy+Jo1B+OZ{e4av-ReR@3YJ(;M zz0oD*q-h0aQ%LCsemlwC7U9!HY9&v7d!xB3V0c!qbN;EYuW~0zJsO87MJRG;j4~GM z!wW!fuN8s@X8gnZxG0luQLfB#lm!J9+Bi;JeRH{w`bpQ(?Txe9Dw6}PVgGs(%`b)e m>aIBzX~|65y0*1u%UVegt+JNI)Z4`imH;jI + + + + + + + + diff --git a/steps/04.01-data-fetching-solution/src/assets/svg/logoDark.svg b/steps/04.01-data-fetching-solution/src/assets/svg/logoDark.svg new file mode 100644 index 0000000..06eb6ed --- /dev/null +++ b/steps/04.01-data-fetching-solution/src/assets/svg/logoDark.svg @@ -0,0 +1,28 @@ + + + + + + + + + diff --git a/steps/04.01-data-fetching-solution/src/components/Alert.tsx b/steps/04.01-data-fetching-solution/src/components/Alert.tsx new file mode 100644 index 0000000..1c90895 --- /dev/null +++ b/steps/04.01-data-fetching-solution/src/components/Alert.tsx @@ -0,0 +1,17 @@ +import clsx from 'clsx'; + +type AlertProps = { + children: React.ReactNode; + className?: string; +}; + +const Alert: React.FC = ({ children, className }) => ( +
+ {children} +
+); + +export default Alert; diff --git a/steps/04.01-data-fetching-solution/src/components/Button.tsx b/steps/04.01-data-fetching-solution/src/components/Button.tsx new file mode 100644 index 0000000..2cee623 --- /dev/null +++ b/steps/04.01-data-fetching-solution/src/components/Button.tsx @@ -0,0 +1,34 @@ +import clsx from 'clsx'; + +export type ButtonProps = { + children: React.ReactNode; + className?: string; + variant?: 'primary' | 'secondary'; + component?: C; +} & Omit, 'className' | 'variant'>; + +const classNames = { + primary: 'inline-block text-white bg-blue-700 hover:bg-blue-800 font-medium rounded-lg text-sm px-5 py-2.5', + secondary: [ + 'inline-block py-2.5 px-5 text-sm font-medium text-slate-900 bg-white rounded-lg border border-gray-200', + 'hover:bg-gray-100 hover:text-blue-700', + 'dark:bg-slate-900 dark:text-white dark:hover:bg-slate-950 dark:hover:text-blue-200 dark:hover:border-blue-200', + ].join(' '), +}; + +const Button = ({ + children, + className, + variant = 'secondary', + component, + ...restProps +}: ButtonProps) => { + const Component = component || 'button'; + return ( + + {children} + + ); +}; + +export default Button; diff --git a/steps/04.01-data-fetching-solution/src/components/EmployeeExpenses.tsx b/steps/04.01-data-fetching-solution/src/components/EmployeeExpenses.tsx new file mode 100644 index 0000000..ab5073a --- /dev/null +++ b/steps/04.01-data-fetching-solution/src/components/EmployeeExpenses.tsx @@ -0,0 +1,46 @@ +'use client'; + +import { useState } from 'react'; +import Button from './Button'; +import { Expense } from '@/types'; +import ExpensesTable from './ExpensesTable'; +import Alert from './Alert'; + +type EmployeeExpensesProps = { + employeeId: string; +}; + +const EmployeeExpenses: React.FC = ({ employeeId }) => { + const [loadingStatus, setLoadingStatus] = useState('IDLE'); + const [expenses, setExpenses] = useState>(null); + + const handleOpen = async () => { + setLoadingStatus('LOADING'); + try { + const expensesData = (await fetch(`/rest/expenses?employeeId=${employeeId}`).then((res) => res.json())) as { + data: Array; + }; + setExpenses(expensesData.data); + setLoadingStatus('LOADED'); + } catch (err) { + setLoadingStatus('ERROR'); + } + }; + + if (loadingStatus === 'IDLE') { + return ; + } + + if (loadingStatus === 'LOADING') { + return 'Loading...'; + } + + if (loadingStatus === 'LOADED') { + if (!expenses?.length) return No expenses for this employee; + return ; + } + + return Oops, something went wrong :/; +}; + +export default EmployeeExpenses; diff --git a/steps/04.01-data-fetching-solution/src/components/EmployeeForm.tsx b/steps/04.01-data-fetching-solution/src/components/EmployeeForm.tsx new file mode 100644 index 0000000..dc15055 --- /dev/null +++ b/steps/04.01-data-fetching-solution/src/components/EmployeeForm.tsx @@ -0,0 +1,113 @@ +'use client'; + +import Image from 'next/image'; +import TextField from '@/components/TextField'; +import { Person } from '@/types'; +import { useFormState } from 'react-dom'; + +import placeholderImage from '@/assets/images/profile-placeholder.jpg'; +import Button from './Button'; + +type ActionState = { + validationErrors?: { [key: string]: Array }; +}; + +type Action = (id: string, formData: FormData) => Promise; + +type EmployeeFormProps = { + employee?: Person; + action?: Action; + className?: string; +}; + +const initialState = { + validationErrors: {}, +} as ActionState; + +const EmployeeForm: React.FC = ({ employee, action, className }) => { + // @ts-ignore + const [state, formAction] = useFormState(action, initialState as unknown as void); + + return ( +
+
+ {employee +
+
+
+ + + + + +
+
+ + + +
+
+
+ +
+
+ ); +}; + +export default EmployeeForm; diff --git a/steps/04.01-data-fetching-solution/src/components/ExpensesDetails.tsx b/steps/04.01-data-fetching-solution/src/components/ExpensesDetails.tsx new file mode 100644 index 0000000..f59b51a --- /dev/null +++ b/steps/04.01-data-fetching-solution/src/components/ExpensesDetails.tsx @@ -0,0 +1,56 @@ +import { Expense } from '@/types'; +import Paper from './Paper'; + +type ExpenseDetailsRowProps = { + label: string; + value: string; +}; + +const ExpenseDetailsRow: React.FC = ({ label, value }) => ( +
+ {label} + {value} +
+); + +type ExpenseDetailsProps = { + expense: Expense; +}; + +const ExpenseDetails: React.FC = ({ expense }) => ( + <> +
+
+

Information

+ + + + + +
+
+

Workflow

+ + + + + +
+
+
+
+

Amount

+ + + + + +
+
+ +); + +export default ExpenseDetails; diff --git a/steps/04.01-data-fetching-solution/src/components/ExpensesTable.tsx b/steps/04.01-data-fetching-solution/src/components/ExpensesTable.tsx new file mode 100644 index 0000000..1b2e240 --- /dev/null +++ b/steps/04.01-data-fetching-solution/src/components/ExpensesTable.tsx @@ -0,0 +1,61 @@ +'use client'; + +import { Expense } from '@/types'; +import clsx from 'clsx'; +import { useRouter } from 'next/navigation'; + +type ExpensesTableProps = { + expenses: Array; +}; + +const ExpensesTable: React.FC = ({ expenses }) => { + const router = useRouter(); + + const handleClick = (expenseId: string) => () => { + router.push(`/expenses/${expenseId}`); + }; + + return ( + + + + + + + + + + + {expenses.map((expense, index) => ( + + + + + + + ))} + +
+ Label + + Creation date + + Category + + Price +
{expense.label}{new Date(expense.creationDate).toLocaleDateString()}{expense.category} + {expense.price.priceIncludingTax} {expense.price.currency} +
+ ); +}; + +export default ExpensesTable; diff --git a/steps/04.01-data-fetching-solution/src/components/Icons/ArrowLeft.tsx b/steps/04.01-data-fetching-solution/src/components/Icons/ArrowLeft.tsx new file mode 100644 index 0000000..1cc10c7 --- /dev/null +++ b/steps/04.01-data-fetching-solution/src/components/Icons/ArrowLeft.tsx @@ -0,0 +1,25 @@ +type ArrowLeftProps = { + className?: string; +}; + +const ArrowLeft: React.FC = ({ className }) => ( + +); + +export default ArrowLeft; diff --git a/steps/04.01-data-fetching-solution/src/components/Icons/Eye.tsx b/steps/04.01-data-fetching-solution/src/components/Icons/Eye.tsx new file mode 100644 index 0000000..04beb91 --- /dev/null +++ b/steps/04.01-data-fetching-solution/src/components/Icons/Eye.tsx @@ -0,0 +1,11 @@ +type EyeProps = { + className?: string; +}; + +const Eye: React.FC = ({ className }) => ( + + + +); + +export default Eye; diff --git a/steps/04.01-data-fetching-solution/src/components/Icons/Loader.tsx b/steps/04.01-data-fetching-solution/src/components/Icons/Loader.tsx new file mode 100644 index 0000000..9c81994 --- /dev/null +++ b/steps/04.01-data-fetching-solution/src/components/Icons/Loader.tsx @@ -0,0 +1,25 @@ +type LoaderProps = { + className?: string; +}; + +const Loader: React.FC = ({ className }) => ( + +); + +export default Loader; diff --git a/steps/04.01-data-fetching-solution/src/components/NavigationItem.tsx b/steps/04.01-data-fetching-solution/src/components/NavigationItem.tsx new file mode 100644 index 0000000..9f68e10 --- /dev/null +++ b/steps/04.01-data-fetching-solution/src/components/NavigationItem.tsx @@ -0,0 +1,30 @@ +'use client'; + +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; + +import clsx from 'clsx'; + +type NavigationItemsProps = { + href: string; + children: React.ReactNode; +}; + +const NavigationItem: React.FC = ({ href, children }) => { + const pathname = usePathname(); + + return ( + + {children} + + ); +}; + +export default NavigationItem; diff --git a/steps/04.01-data-fetching-solution/src/components/NavigationMenu.tsx b/steps/04.01-data-fetching-solution/src/components/NavigationMenu.tsx new file mode 100644 index 0000000..4b778c6 --- /dev/null +++ b/steps/04.01-data-fetching-solution/src/components/NavigationMenu.tsx @@ -0,0 +1,21 @@ +import NavigationItem from './NavigationItem'; + +const NavigationMenu = () => { + return ( + + ); +}; + +export default NavigationMenu; diff --git a/steps/04.01-data-fetching-solution/src/components/PageTitle.tsx b/steps/04.01-data-fetching-solution/src/components/PageTitle.tsx new file mode 100644 index 0000000..217fe69 --- /dev/null +++ b/steps/04.01-data-fetching-solution/src/components/PageTitle.tsx @@ -0,0 +1,25 @@ +import Link from 'next/link'; + +import ArrowLeft from './Icons/ArrowLeft'; + +type PageTitleProps = { + children: React.ReactNode; + backHref?: string; +}; + +const PageTitle: React.FC = ({ children, backHref }) => ( +
+ {backHref && ( + + + Go back + + )} +

{children}

+
+); + +export default PageTitle; diff --git a/steps/04.01-data-fetching-solution/src/components/Pagination.tsx b/steps/04.01-data-fetching-solution/src/components/Pagination.tsx new file mode 100644 index 0000000..be9a43a --- /dev/null +++ b/steps/04.01-data-fetching-solution/src/components/Pagination.tsx @@ -0,0 +1,95 @@ +'use client'; + +import clsx from 'clsx'; +import Link from 'next/link'; +import { usePathname, useSearchParams } from 'next/navigation'; + +type PaginationProps = { + totalPages: number; + className?: string; +}; + +type PaginationShortcutProps = { + href: string; + disabled?: boolean; + className?: string; + children: React.ReactNode; +}; + +const PaginationShortcut: React.FC = ({ href, disabled, className, children }) => { + const classNames = clsx( + 'block text-center px-3 py-2 ms-0 border bg-white dark:bg-slate-900', + className, + !disabled && + 'hover:bg-gray-100 hover:text-gray-700 text-gray-500 border-gray-300 dark:text-white dark:border-gray-700 dark:hover:bg-slate-950 dark:hover:text-white', + disabled && 'text-gray-300 border-gray-200 dark:text-gray-500 dark:border-gray-600' + ); + + if (disabled) return
{children}
; + + return ( + + {children} + + ); +}; + +const Pagination: React.FC = ({ totalPages, className }) => { + const params = useSearchParams(); + const pathname = usePathname(); + + const currentPage = Number(params.get('page')) || 1; + + const getPageUrl = (page: number): string => { + const newParams = new URLSearchParams(params); + newParams.set('page', page.toString()); + return `${pathname}?${newParams.toString()}`; + }; + + return ( + + ); +}; + +export default Pagination; diff --git a/steps/04.01-data-fetching-solution/src/components/Paper.tsx b/steps/04.01-data-fetching-solution/src/components/Paper.tsx new file mode 100644 index 0000000..8ba656e --- /dev/null +++ b/steps/04.01-data-fetching-solution/src/components/Paper.tsx @@ -0,0 +1,14 @@ +import clsx from 'clsx'; + +type PaperProps = React.HTMLAttributes & { + children: React.ReactNode; + rounded?: boolean; +}; + +const Paper: React.FC = ({ children, rounded = true, ...restProps }) => ( +
+ {children} +
+); + +export default Paper; diff --git a/steps/04.01-data-fetching-solution/src/components/PersonCard.tsx b/steps/04.01-data-fetching-solution/src/components/PersonCard.tsx new file mode 100644 index 0000000..b38ee53 --- /dev/null +++ b/steps/04.01-data-fetching-solution/src/components/PersonCard.tsx @@ -0,0 +1,43 @@ +import Image from 'next/image'; + +import { Person } from '@/types'; + +import placeholderImage from '@/assets/images/profile-placeholder.jpg'; + +type PersonCardProps = React.HTMLAttributes & { + person: Person; + actions?: React.ReactNode; + compact?: boolean; +}; + +const PersonCard: React.FC = ({ person, actions, className, compact = false }) => { + return ( +
+
+ {`Picture + + {person.firstname} {person.lastname} + + {person.position} +
+ + {!compact && ( +
+ {person.phone} + {person.email} + {person.manager && {person.manager}} +
+ )} + + {actions &&
{actions}
} +
+ ); +}; + +export default PersonCard; diff --git a/steps/04.01-data-fetching-solution/src/components/Search.tsx b/steps/04.01-data-fetching-solution/src/components/Search.tsx new file mode 100644 index 0000000..98fe032 --- /dev/null +++ b/steps/04.01-data-fetching-solution/src/components/Search.tsx @@ -0,0 +1,43 @@ +'use client'; + +import { debounce } from '@/functions/timing'; +import clsx from 'clsx'; +import { usePathname, useRouter, useSearchParams } from 'next/navigation'; + +const Search = ({ ...restProps }) => { + const router = useRouter(); + const params = useSearchParams(); + const pathname = usePathname(); + + const handleChange = debounce((event: React.ChangeEvent) => { + const value = event.target?.value; + const newParams = new URLSearchParams(params); + newParams.delete('page'); + if (value) newParams.set('search', value); + else newParams.delete('search'); + router.replace(`${pathname}?${newParams.toString()}`); + }, 200); + + return ( + <> + + + + ); +}; + +export default Search; diff --git a/steps/04.01-data-fetching-solution/src/components/TextField.tsx b/steps/04.01-data-fetching-solution/src/components/TextField.tsx new file mode 100644 index 0000000..d8061e9 --- /dev/null +++ b/steps/04.01-data-fetching-solution/src/components/TextField.tsx @@ -0,0 +1,31 @@ +import clsx from 'clsx'; + +type TextFieldProps = React.InputHTMLAttributes & { + label: string; + id: string; + type?: string; + className?: string; + errorMessages?: Array; +}; + +const TextField: React.FC = ({ label, id, type = 'text', className, errorMessages, ...restProps }) => { + return ( +
+ + + {errorMessages?.length &&

{errorMessages[0]}

} +
+ ); +}; + +export default TextField; diff --git a/steps/04.01-data-fetching-solution/src/functions/timing.ts b/steps/04.01-data-fetching-solution/src/functions/timing.ts new file mode 100644 index 0000000..3b8c6f3 --- /dev/null +++ b/steps/04.01-data-fetching-solution/src/functions/timing.ts @@ -0,0 +1,7 @@ +export const debounce = (fn: Function, ms = 300) => { + let timeoutId: ReturnType; + return function (this: any, ...args: any[]) { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => fn.apply(this, args), ms); + }; +}; diff --git a/steps/04.01-data-fetching-solution/src/styles/global.css b/steps/04.01-data-fetching-solution/src/styles/global.css new file mode 100644 index 0000000..f77ed90 --- /dev/null +++ b/steps/04.01-data-fetching-solution/src/styles/global.css @@ -0,0 +1,41 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --color-bg-global: #e9effc; + --color-bg-primary: #ffffff; + --color-bg-secondary: #f5f5f5; + --color-text-primary: #000000; + + --spacing-sm: 0.5rem; + --spacing-md: 0.75rem; + --spacing-lg: 1rem; + --spacing-xl: 1.5rem; +} + +/* Headings */ + +.heading1 { + font-size: 2rem; + font-weight: bold; + margin-bottom: var(--spacing-lg); +} + +.heading2 { + font-size: 1.5rem; + font-weight: bold; + margin-bottom: var(--spacing-lg); +} + +.heading3 { + font-size: 1.125rem; + font-weight: bold; + margin-bottom: var(--spacing-lg); +} + +.heading4 { + font-size: 1rem; + font-weight: bold; + margin-bottom: var(--spacing-lg); +} diff --git a/steps/04.01-data-fetching-solution/src/types.ts b/steps/04.01-data-fetching-solution/src/types.ts new file mode 100644 index 0000000..82cffb5 --- /dev/null +++ b/steps/04.01-data-fetching-solution/src/types.ts @@ -0,0 +1,39 @@ +export type Person = { + id: string; + photo?: string; + firstname: string; + lastname: string; + position: string; + entryDate: string; + birthDate: string; + gender: string; + email: string; + phone: string; + isManager: boolean; + manager?: string; + managerId?: string; +}; + +export type Expense = { + id: string; + employeeId: string; + price: { + priceIncludingTax: number; + taxAmount: number; + priceExcludingTax: number; + currency: string; + }; + label: string; + description: string; + category: string; + receiptLink: string; + status: 'approved' | 'created' | 'declined'; + creationDate: string; + updateDate: string; +}; + +export type PaginationAttributes = { + per_page?: number; + page: number; + total_pages: number; +}; diff --git a/steps/04.01-data-fetching-solution/tailwind.config.js b/steps/04.01-data-fetching-solution/tailwind.config.js new file mode 100644 index 0000000..eaa361c --- /dev/null +++ b/steps/04.01-data-fetching-solution/tailwind.config.js @@ -0,0 +1,9 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ['./src/**/*.{js,ts,jsx,tsx,mdx}'], + darkMode: 'selector', + theme: { + extend: {}, + }, + plugins: [], +}; diff --git a/steps/04.01-data-fetching-solution/tsconfig.json b/steps/04.01-data-fetching-solution/tsconfig.json new file mode 100644 index 0000000..7b28589 --- /dev/null +++ b/steps/04.01-data-fetching-solution/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/steps/04.01-data-fetching/.env.example b/steps/04.01-data-fetching/.env.example new file mode 100644 index 0000000..1ebabff --- /dev/null +++ b/steps/04.01-data-fetching/.env.example @@ -0,0 +1,2 @@ +API_BASE_URL=http://localhost:3001 +API_KEY=XXXX diff --git a/steps/04.01-data-fetching/.eslintrc.json b/steps/04.01-data-fetching/.eslintrc.json new file mode 100644 index 0000000..bffb357 --- /dev/null +++ b/steps/04.01-data-fetching/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/steps/04.01-data-fetching/.gitignore b/steps/04.01-data-fetching/.gitignore new file mode 100644 index 0000000..fd3dbb5 --- /dev/null +++ b/steps/04.01-data-fetching/.gitignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/steps/04.01-data-fetching/README.md b/steps/04.01-data-fetching/README.md new file mode 100644 index 0000000..acd5880 --- /dev/null +++ b/steps/04.01-data-fetching/README.md @@ -0,0 +1 @@ +# 04.01 - Data fetching diff --git a/steps/04.01-data-fetching/next.config.mjs b/steps/04.01-data-fetching/next.config.mjs new file mode 100644 index 0000000..16343f6 --- /dev/null +++ b/steps/04.01-data-fetching/next.config.mjs @@ -0,0 +1,15 @@ +const apiUrl = new URL(process.env.API_BASE_URL || 'http://localhost:3001'); + +/** @type {import('next').NextConfig} */ +const nextConfig = { + images: { + remotePatterns: [ + { + hostname: apiUrl.hostname, + port: apiUrl.port, + }, + ], + }, +}; + +export default nextConfig; diff --git a/steps/04.01-data-fetching/package.json b/steps/04.01-data-fetching/package.json new file mode 100644 index 0000000..27cd234 --- /dev/null +++ b/steps/04.01-data-fetching/package.json @@ -0,0 +1,38 @@ +{ + "name": "04.01", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "bright": "^0.8.5", + "clsx": "^2.1.1", + "jose": "^5.6.3", + "jsonwebtoken": "^9.0.2", + "next": "14.2.5", + "react": "^18", + "react-dom": "^18", + "react-error-boundary": "^4.0.13", + "rehype-sanitize": "^6.0.0", + "rehype-stringify": "^10.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.0", + "server-only": "^0.0.1", + "showdown": "^2.1.0", + "unified": "^11.0.5" + }, + "devDependencies": { + "@types/jsonwebtoken": "^9.0.6", + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "@types/showdown": "^2.0.6", + "eslint": "^8", + "eslint-config-next": "14.2.5", + "typescript": "^5" + } +} \ No newline at end of file diff --git a/steps/04.01-data-fetching/postcss.config.js b/steps/04.01-data-fetching/postcss.config.js new file mode 100644 index 0000000..33ad091 --- /dev/null +++ b/steps/04.01-data-fetching/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/steps/04.01-data-fetching/public/next.svg b/steps/04.01-data-fetching/public/next.svg new file mode 100644 index 0000000..5174b28 --- /dev/null +++ b/steps/04.01-data-fetching/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/steps/04.01-data-fetching/public/portraits/men/30.jpg b/steps/04.01-data-fetching/public/portraits/men/30.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d04b7a2669245212620be0cbb17922fd1cb3cbfe GIT binary patch literal 4349 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 w3$0jQtjS=6rPW;hG!!51^p@HCR)|Jncu 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 literal 0 HcmV?d00001 diff --git a/steps/04.01-data-fetching/public/portraits/men/78.jpg b/steps/04.01-data-fetching/public/portraits/men/78.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6438e80b9b56fcf7e6e0e9a02eb8cc54a53c91aa GIT binary patch literal 4643 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 Y-IZTg@->$i4XtycyzDV!v4pAr0qF|a9RL6T literal 0 HcmV?d00001 diff --git a/steps/04.01-data-fetching/public/portraits/men/86.jpg b/steps/04.01-data-fetching/public/portraits/men/86.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9358491105b401a359ef698035ab9b05b84e0457 GIT binary patch literal 5433 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 Kdoq>(yYN42Z*!dh literal 0 HcmV?d00001 diff --git a/steps/04.01-data-fetching/public/portraits/women/65.jpg b/steps/04.01-data-fetching/public/portraits/women/65.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3cab57987a6ff10c5639fad656fb0fc77ba569c9 GIT binary patch literal 5972 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 C^!eWa literal 0 HcmV?d00001 diff --git a/steps/04.01-data-fetching/public/portraits/women/85.jpg b/steps/04.01-data-fetching/public/portraits/women/85.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0a900f9e8874eddf5978f08e6de03815f23bb1ea GIT binary patch literal 3912 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 literal 0 HcmV?d00001 diff --git a/steps/04.01-data-fetching/public/portraits/women/93.jpg b/steps/04.01-data-fetching/public/portraits/women/93.jpg new file mode 100644 index 0000000000000000000000000000000000000000..81ea0613898f679ad77f8e4563a93be893f38a07 GIT binary patch literal 4871 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 \ No newline at end of file diff --git a/steps/04.01-data-fetching/src/api/common.ts b/steps/04.01-data-fetching/src/api/common.ts new file mode 100644 index 0000000..494935d --- /dev/null +++ b/steps/04.01-data-fetching/src/api/common.ts @@ -0,0 +1,17 @@ +import { ApiError } from './error'; + +export async function fetchJson(url: string, options?: RequestInit): Promise { + const requestOptions = { + ...options, + headers: { + ...options?.headers, + ['x-api-key']: process.env.API_KEY || 'not-set', + ['content-type']: 'application/json', + }, + }; + const response: Response = await fetch(url, requestOptions); + const data: T | unknown = await response.json(); + if (response.ok) return data as T; + + throw new ApiError(response.statusText, data as unknown); +} diff --git a/steps/04.01-data-fetching/src/api/error.ts b/steps/04.01-data-fetching/src/api/error.ts new file mode 100644 index 0000000..bffb65a --- /dev/null +++ b/steps/04.01-data-fetching/src/api/error.ts @@ -0,0 +1,8 @@ +export class ApiError extends Error { + body; + constructor(message: string, body: unknown) { + super(message); + this.name = 'ApiError'; + this.body = body; + } +} diff --git a/steps/04.01-data-fetching/src/app/(auth)/layout.tsx b/steps/04.01-data-fetching/src/app/(auth)/layout.tsx new file mode 100644 index 0000000..cac31a7 --- /dev/null +++ b/steps/04.01-data-fetching/src/app/(auth)/layout.tsx @@ -0,0 +1,12 @@ +type AuthLayoutProps = { + children: React.ReactNode; +}; + +const AuthLayout: React.FC = ({ children }) => ( +
+
+
{children}
+
+); + +export default AuthLayout; diff --git a/steps/04.01-data-fetching/src/app/(auth)/login/page.tsx b/steps/04.01-data-fetching/src/app/(auth)/login/page.tsx new file mode 100644 index 0000000..3ae0596 --- /dev/null +++ b/steps/04.01-data-fetching/src/app/(auth)/login/page.tsx @@ -0,0 +1,30 @@ +import { Metadata } from 'next'; + +import TextField from '@/components/TextField'; +import Button from '@/components/Button'; + +export const metadata: Metadata = { + title: 'SFEIR People | Login', +}; + +const LoginPage = () => { + return ( +
+

Welcome !

+ + + + + ); +}; + +export default LoginPage; diff --git a/steps/04.01-data-fetching/src/app/(dashboard)/employees/[id]/edit/page.tsx b/steps/04.01-data-fetching/src/app/(dashboard)/employees/[id]/edit/page.tsx new file mode 100644 index 0000000..a8c3a22 --- /dev/null +++ b/steps/04.01-data-fetching/src/app/(dashboard)/employees/[id]/edit/page.tsx @@ -0,0 +1,24 @@ +import EmployeeForm from '@/components/EmployeeForm'; +import PageTitle from '@/components/PageTitle'; + +import employeesData from '@/data/employees.json'; + +const EmployeeDetail = ({ params }: { params: { id: string } }) => { + const employee = employeesData.find((employee) => employee.id === params.id); + + if (!employee) return Single Employee - Not found; + + return ( + <> + + Single Employee - {employee.firstname} {employee.lastname} | Edit + + +
+ +
+ + ); +}; + +export default EmployeeDetail; diff --git a/steps/04.01-data-fetching/src/app/(dashboard)/employees/[id]/page.tsx b/steps/04.01-data-fetching/src/app/(dashboard)/employees/[id]/page.tsx new file mode 100644 index 0000000..44e1160 --- /dev/null +++ b/steps/04.01-data-fetching/src/app/(dashboard)/employees/[id]/page.tsx @@ -0,0 +1,22 @@ +import EmployeeExpenses from '@/components/EmployeeExpenses'; +import PageTitle from '@/components/PageTitle'; +import PersonCard from '@/components/PersonCard'; + +import employeesData from '@/data/employees.json'; + +const EmployeeDetail = ({ params }: { params: { id: string } }) => { + const employee = employeesData.find((employee) => employee.id === params.id); + + if (!employee) return Single Employee - Not found; + + return ( + <> + + Single Employee - {employee.firstname} {employee.lastname} + + } /> + + ); +}; + +export default EmployeeDetail; diff --git a/steps/04.01-data-fetching/src/app/(dashboard)/employees/new/page.tsx b/steps/04.01-data-fetching/src/app/(dashboard)/employees/new/page.tsx new file mode 100644 index 0000000..775f4f7 --- /dev/null +++ b/steps/04.01-data-fetching/src/app/(dashboard)/employees/new/page.tsx @@ -0,0 +1,18 @@ +import EmployeeForm from '@/components/EmployeeForm'; +import PageTitle from '@/components/PageTitle'; + +const EmployeeDetail = () => { + return ( + <> + + Employees | Create + + +
+ +
+ + ); +}; + +export default EmployeeDetail; diff --git a/steps/04.01-data-fetching/src/app/(dashboard)/employees/page.tsx b/steps/04.01-data-fetching/src/app/(dashboard)/employees/page.tsx new file mode 100644 index 0000000..2a356a7 --- /dev/null +++ b/steps/04.01-data-fetching/src/app/(dashboard)/employees/page.tsx @@ -0,0 +1,47 @@ +import Link from 'next/link'; + +import Button from '@/components/Button'; +import PageTitle from '@/components/PageTitle'; +import PersonCard from '@/components/PersonCard'; +import Search from '@/components/Search'; + +import employeesData from '@/data/employees.json'; + +const Employees = async ({ searchParams }: { searchParams: { search?: string } }) => { + const search = searchParams.search || ''; + const employees = employeesData.filter((employee) => + `${employee.firstname} ${employee.lastname}`.toLowerCase().includes(search.toLowerCase()) + ); + + return ( +
+ Employees +
+ + +
+
+ {employees?.map((employee) => ( + + + +
+ } + /> + ))} +
+ + ); +}; + +export default Employees; diff --git a/steps/04.01-data-fetching/src/app/(dashboard)/expenses/[id]/page.tsx b/steps/04.01-data-fetching/src/app/(dashboard)/expenses/[id]/page.tsx new file mode 100644 index 0000000..bda58e2 --- /dev/null +++ b/steps/04.01-data-fetching/src/app/(dashboard)/expenses/[id]/page.tsx @@ -0,0 +1,18 @@ +import ExpenseDetails from '@/components/ExpensesDetails'; +import PageTitle from '@/components/PageTitle'; + +import expensesData from '@/data/expenses.json'; +import { Expense } from '@/types'; + +const SingleExpense = ({ params }: { params: { id: string } }) => { + const expense = expensesData.find((expense) => expense.id === params.id); + + return ( + <> + Single Expense - {expense?.label || 'Not found'} + {expense && } + + ); +}; + +export default SingleExpense; diff --git a/steps/04.01-data-fetching/src/app/(dashboard)/expenses/page.tsx b/steps/04.01-data-fetching/src/app/(dashboard)/expenses/page.tsx new file mode 100644 index 0000000..01d2506 --- /dev/null +++ b/steps/04.01-data-fetching/src/app/(dashboard)/expenses/page.tsx @@ -0,0 +1,17 @@ +import ExpensesTable from '@/components/ExpensesTable'; +import PageTitle from '@/components/PageTitle'; + +import { Expense } from '@/types'; + +import expensesData from '@/data/expenses.json'; + +const Expenses = () => { + return ( + <> + Expenses + } /> + + ); +}; + +export default Expenses; diff --git a/steps/04.01-data-fetching/src/app/(dashboard)/layout.tsx b/steps/04.01-data-fetching/src/app/(dashboard)/layout.tsx new file mode 100644 index 0000000..f6e4708 --- /dev/null +++ b/steps/04.01-data-fetching/src/app/(dashboard)/layout.tsx @@ -0,0 +1,37 @@ +import { Metadata } from 'next'; +import Link from 'next/link'; +import Image from 'next/image'; + +import { promises as fs } from 'fs'; +import path from 'path'; + +import NavigationMenu from '@/components/NavigationMenu'; + +import logo from '@/assets/svg/logo.svg'; + +type DashboardLayoutProps = { children: React.ReactNode }; + +export const metadata: Metadata = { + title: 'SFEIR People | Dashboard', +}; + +const DashboardLayout: React.FC = async ({ children }) => { + const packageJsonPath = path.join(process.cwd(), 'package.json'); + const packageJsonContent = await fs.readFile(packageJsonPath, 'utf-8'); + const packageJson = JSON.parse(packageJsonContent); + + return ( +
+
+ + People logo + + +
Version: {packageJson.version}
+
+
{children}
+
+ ); +}; + +export default DashboardLayout; diff --git a/steps/04.01-data-fetching/src/app/(dashboard)/page.tsx b/steps/04.01-data-fetching/src/app/(dashboard)/page.tsx new file mode 100644 index 0000000..f581ebb --- /dev/null +++ b/steps/04.01-data-fetching/src/app/(dashboard)/page.tsx @@ -0,0 +1,11 @@ +import PageTitle from '@/components/PageTitle'; + +const HomePage = () => { + return ( + <> + SFEIR People + + ); +}; + +export default HomePage; diff --git a/steps/04.01-data-fetching/src/app/layout.tsx b/steps/04.01-data-fetching/src/app/layout.tsx new file mode 100644 index 0000000..e7d90e9 --- /dev/null +++ b/steps/04.01-data-fetching/src/app/layout.tsx @@ -0,0 +1,21 @@ +import type { Metadata } from 'next'; +import { Inter } from 'next/font/google'; + +const inter = Inter({ subsets: ['latin'] }); + +import '@/styles/global.css'; + +export const metadata: Metadata = { + title: 'SFEIR People', + description: 'SFEIR People dashboard application', +}; + +const RootLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => { + return ( + + {children} + + ); +}; + +export default RootLayout; diff --git a/steps/04.01-data-fetching/src/assets/images/profile-placeholder.jpg b/steps/04.01-data-fetching/src/assets/images/profile-placeholder.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6fa00ea6c9e371e542006bb4bd69ae5e6922a324 GIT binary patch literal 11940 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 m>aIBzX~|65y0*1u%UVegt+JNI)Z4`imH;jI + + + + + + + + diff --git a/steps/04.01-data-fetching/src/assets/svg/logoDark.svg b/steps/04.01-data-fetching/src/assets/svg/logoDark.svg new file mode 100644 index 0000000..06eb6ed --- /dev/null +++ b/steps/04.01-data-fetching/src/assets/svg/logoDark.svg @@ -0,0 +1,28 @@ + + + + + + + + + diff --git a/steps/04.01-data-fetching/src/components/Alert.tsx b/steps/04.01-data-fetching/src/components/Alert.tsx new file mode 100644 index 0000000..1c90895 --- /dev/null +++ b/steps/04.01-data-fetching/src/components/Alert.tsx @@ -0,0 +1,17 @@ +import clsx from 'clsx'; + +type AlertProps = { + children: React.ReactNode; + className?: string; +}; + +const Alert: React.FC = ({ children, className }) => ( +
+ {children} +
+); + +export default Alert; diff --git a/steps/04.01-data-fetching/src/components/Button.tsx b/steps/04.01-data-fetching/src/components/Button.tsx new file mode 100644 index 0000000..2cee623 --- /dev/null +++ b/steps/04.01-data-fetching/src/components/Button.tsx @@ -0,0 +1,34 @@ +import clsx from 'clsx'; + +export type ButtonProps = { + children: React.ReactNode; + className?: string; + variant?: 'primary' | 'secondary'; + component?: C; +} & Omit, 'className' | 'variant'>; + +const classNames = { + primary: 'inline-block text-white bg-blue-700 hover:bg-blue-800 font-medium rounded-lg text-sm px-5 py-2.5', + secondary: [ + 'inline-block py-2.5 px-5 text-sm font-medium text-slate-900 bg-white rounded-lg border border-gray-200', + 'hover:bg-gray-100 hover:text-blue-700', + 'dark:bg-slate-900 dark:text-white dark:hover:bg-slate-950 dark:hover:text-blue-200 dark:hover:border-blue-200', + ].join(' '), +}; + +const Button = ({ + children, + className, + variant = 'secondary', + component, + ...restProps +}: ButtonProps) => { + const Component = component || 'button'; + return ( + + {children} + + ); +}; + +export default Button; diff --git a/steps/04.01-data-fetching/src/components/EmployeeExpenses.tsx b/steps/04.01-data-fetching/src/components/EmployeeExpenses.tsx new file mode 100644 index 0000000..ab5073a --- /dev/null +++ b/steps/04.01-data-fetching/src/components/EmployeeExpenses.tsx @@ -0,0 +1,46 @@ +'use client'; + +import { useState } from 'react'; +import Button from './Button'; +import { Expense } from '@/types'; +import ExpensesTable from './ExpensesTable'; +import Alert from './Alert'; + +type EmployeeExpensesProps = { + employeeId: string; +}; + +const EmployeeExpenses: React.FC = ({ employeeId }) => { + const [loadingStatus, setLoadingStatus] = useState('IDLE'); + const [expenses, setExpenses] = useState>(null); + + const handleOpen = async () => { + setLoadingStatus('LOADING'); + try { + const expensesData = (await fetch(`/rest/expenses?employeeId=${employeeId}`).then((res) => res.json())) as { + data: Array; + }; + setExpenses(expensesData.data); + setLoadingStatus('LOADED'); + } catch (err) { + setLoadingStatus('ERROR'); + } + }; + + if (loadingStatus === 'IDLE') { + return ; + } + + if (loadingStatus === 'LOADING') { + return 'Loading...'; + } + + if (loadingStatus === 'LOADED') { + if (!expenses?.length) return No expenses for this employee; + return ; + } + + return Oops, something went wrong :/; +}; + +export default EmployeeExpenses; diff --git a/steps/04.01-data-fetching/src/components/EmployeeForm.tsx b/steps/04.01-data-fetching/src/components/EmployeeForm.tsx new file mode 100644 index 0000000..dc15055 --- /dev/null +++ b/steps/04.01-data-fetching/src/components/EmployeeForm.tsx @@ -0,0 +1,113 @@ +'use client'; + +import Image from 'next/image'; +import TextField from '@/components/TextField'; +import { Person } from '@/types'; +import { useFormState } from 'react-dom'; + +import placeholderImage from '@/assets/images/profile-placeholder.jpg'; +import Button from './Button'; + +type ActionState = { + validationErrors?: { [key: string]: Array }; +}; + +type Action = (id: string, formData: FormData) => Promise; + +type EmployeeFormProps = { + employee?: Person; + action?: Action; + className?: string; +}; + +const initialState = { + validationErrors: {}, +} as ActionState; + +const EmployeeForm: React.FC = ({ employee, action, className }) => { + // @ts-ignore + const [state, formAction] = useFormState(action, initialState as unknown as void); + + return ( +
+
+ {employee +
+
+
+ + + + + +
+
+ + + +
+
+
+ +
+
+ ); +}; + +export default EmployeeForm; diff --git a/steps/04.01-data-fetching/src/components/ExpensesDetails.tsx b/steps/04.01-data-fetching/src/components/ExpensesDetails.tsx new file mode 100644 index 0000000..f59b51a --- /dev/null +++ b/steps/04.01-data-fetching/src/components/ExpensesDetails.tsx @@ -0,0 +1,56 @@ +import { Expense } from '@/types'; +import Paper from './Paper'; + +type ExpenseDetailsRowProps = { + label: string; + value: string; +}; + +const ExpenseDetailsRow: React.FC = ({ label, value }) => ( +
+ {label} + {value} +
+); + +type ExpenseDetailsProps = { + expense: Expense; +}; + +const ExpenseDetails: React.FC = ({ expense }) => ( + <> +
+
+

Information

+ + + + + +
+
+

Workflow

+ + + + + +
+
+
+
+

Amount

+ + + + + +
+
+ +); + +export default ExpenseDetails; diff --git a/steps/04.01-data-fetching/src/components/ExpensesTable.tsx b/steps/04.01-data-fetching/src/components/ExpensesTable.tsx new file mode 100644 index 0000000..1b2e240 --- /dev/null +++ b/steps/04.01-data-fetching/src/components/ExpensesTable.tsx @@ -0,0 +1,61 @@ +'use client'; + +import { Expense } from '@/types'; +import clsx from 'clsx'; +import { useRouter } from 'next/navigation'; + +type ExpensesTableProps = { + expenses: Array; +}; + +const ExpensesTable: React.FC = ({ expenses }) => { + const router = useRouter(); + + const handleClick = (expenseId: string) => () => { + router.push(`/expenses/${expenseId}`); + }; + + return ( + + + + + + + + + + + {expenses.map((expense, index) => ( + + + + + + + ))} + +
+ Label + + Creation date + + Category + + Price +
{expense.label}{new Date(expense.creationDate).toLocaleDateString()}{expense.category} + {expense.price.priceIncludingTax} {expense.price.currency} +
+ ); +}; + +export default ExpensesTable; diff --git a/steps/04.01-data-fetching/src/components/Icons/ArrowLeft.tsx b/steps/04.01-data-fetching/src/components/Icons/ArrowLeft.tsx new file mode 100644 index 0000000..1cc10c7 --- /dev/null +++ b/steps/04.01-data-fetching/src/components/Icons/ArrowLeft.tsx @@ -0,0 +1,25 @@ +type ArrowLeftProps = { + className?: string; +}; + +const ArrowLeft: React.FC = ({ className }) => ( + +); + +export default ArrowLeft; diff --git a/steps/04.01-data-fetching/src/components/Icons/Eye.tsx b/steps/04.01-data-fetching/src/components/Icons/Eye.tsx new file mode 100644 index 0000000..04beb91 --- /dev/null +++ b/steps/04.01-data-fetching/src/components/Icons/Eye.tsx @@ -0,0 +1,11 @@ +type EyeProps = { + className?: string; +}; + +const Eye: React.FC = ({ className }) => ( + + + +); + +export default Eye; diff --git a/steps/04.01-data-fetching/src/components/Icons/Loader.tsx b/steps/04.01-data-fetching/src/components/Icons/Loader.tsx new file mode 100644 index 0000000..9c81994 --- /dev/null +++ b/steps/04.01-data-fetching/src/components/Icons/Loader.tsx @@ -0,0 +1,25 @@ +type LoaderProps = { + className?: string; +}; + +const Loader: React.FC = ({ className }) => ( + +); + +export default Loader; diff --git a/steps/04.01-data-fetching/src/components/NavigationItem.tsx b/steps/04.01-data-fetching/src/components/NavigationItem.tsx new file mode 100644 index 0000000..9f68e10 --- /dev/null +++ b/steps/04.01-data-fetching/src/components/NavigationItem.tsx @@ -0,0 +1,30 @@ +'use client'; + +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; + +import clsx from 'clsx'; + +type NavigationItemsProps = { + href: string; + children: React.ReactNode; +}; + +const NavigationItem: React.FC = ({ href, children }) => { + const pathname = usePathname(); + + return ( + + {children} + + ); +}; + +export default NavigationItem; diff --git a/steps/04.01-data-fetching/src/components/NavigationMenu.tsx b/steps/04.01-data-fetching/src/components/NavigationMenu.tsx new file mode 100644 index 0000000..4b778c6 --- /dev/null +++ b/steps/04.01-data-fetching/src/components/NavigationMenu.tsx @@ -0,0 +1,21 @@ +import NavigationItem from './NavigationItem'; + +const NavigationMenu = () => { + return ( + + ); +}; + +export default NavigationMenu; diff --git a/steps/04.01-data-fetching/src/components/PageTitle.tsx b/steps/04.01-data-fetching/src/components/PageTitle.tsx new file mode 100644 index 0000000..217fe69 --- /dev/null +++ b/steps/04.01-data-fetching/src/components/PageTitle.tsx @@ -0,0 +1,25 @@ +import Link from 'next/link'; + +import ArrowLeft from './Icons/ArrowLeft'; + +type PageTitleProps = { + children: React.ReactNode; + backHref?: string; +}; + +const PageTitle: React.FC = ({ children, backHref }) => ( +
+ {backHref && ( + + + Go back + + )} +

{children}

+
+); + +export default PageTitle; diff --git a/steps/04.01-data-fetching/src/components/Pagination.tsx b/steps/04.01-data-fetching/src/components/Pagination.tsx new file mode 100644 index 0000000..be9a43a --- /dev/null +++ b/steps/04.01-data-fetching/src/components/Pagination.tsx @@ -0,0 +1,95 @@ +'use client'; + +import clsx from 'clsx'; +import Link from 'next/link'; +import { usePathname, useSearchParams } from 'next/navigation'; + +type PaginationProps = { + totalPages: number; + className?: string; +}; + +type PaginationShortcutProps = { + href: string; + disabled?: boolean; + className?: string; + children: React.ReactNode; +}; + +const PaginationShortcut: React.FC = ({ href, disabled, className, children }) => { + const classNames = clsx( + 'block text-center px-3 py-2 ms-0 border bg-white dark:bg-slate-900', + className, + !disabled && + 'hover:bg-gray-100 hover:text-gray-700 text-gray-500 border-gray-300 dark:text-white dark:border-gray-700 dark:hover:bg-slate-950 dark:hover:text-white', + disabled && 'text-gray-300 border-gray-200 dark:text-gray-500 dark:border-gray-600' + ); + + if (disabled) return
{children}
; + + return ( + + {children} + + ); +}; + +const Pagination: React.FC = ({ totalPages, className }) => { + const params = useSearchParams(); + const pathname = usePathname(); + + const currentPage = Number(params.get('page')) || 1; + + const getPageUrl = (page: number): string => { + const newParams = new URLSearchParams(params); + newParams.set('page', page.toString()); + return `${pathname}?${newParams.toString()}`; + }; + + return ( + + ); +}; + +export default Pagination; diff --git a/steps/04.01-data-fetching/src/components/Paper.tsx b/steps/04.01-data-fetching/src/components/Paper.tsx new file mode 100644 index 0000000..8ba656e --- /dev/null +++ b/steps/04.01-data-fetching/src/components/Paper.tsx @@ -0,0 +1,14 @@ +import clsx from 'clsx'; + +type PaperProps = React.HTMLAttributes & { + children: React.ReactNode; + rounded?: boolean; +}; + +const Paper: React.FC = ({ children, rounded = true, ...restProps }) => ( +
+ {children} +
+); + +export default Paper; diff --git a/steps/04.01-data-fetching/src/components/PersonCard.tsx b/steps/04.01-data-fetching/src/components/PersonCard.tsx new file mode 100644 index 0000000..b38ee53 --- /dev/null +++ b/steps/04.01-data-fetching/src/components/PersonCard.tsx @@ -0,0 +1,43 @@ +import Image from 'next/image'; + +import { Person } from '@/types'; + +import placeholderImage from '@/assets/images/profile-placeholder.jpg'; + +type PersonCardProps = React.HTMLAttributes & { + person: Person; + actions?: React.ReactNode; + compact?: boolean; +}; + +const PersonCard: React.FC = ({ person, actions, className, compact = false }) => { + return ( +
+
+ {`Picture + + {person.firstname} {person.lastname} + + {person.position} +
+ + {!compact && ( +
+ {person.phone} + {person.email} + {person.manager && {person.manager}} +
+ )} + + {actions &&
{actions}
} +
+ ); +}; + +export default PersonCard; diff --git a/steps/04.01-data-fetching/src/components/Search.tsx b/steps/04.01-data-fetching/src/components/Search.tsx new file mode 100644 index 0000000..98fe032 --- /dev/null +++ b/steps/04.01-data-fetching/src/components/Search.tsx @@ -0,0 +1,43 @@ +'use client'; + +import { debounce } from '@/functions/timing'; +import clsx from 'clsx'; +import { usePathname, useRouter, useSearchParams } from 'next/navigation'; + +const Search = ({ ...restProps }) => { + const router = useRouter(); + const params = useSearchParams(); + const pathname = usePathname(); + + const handleChange = debounce((event: React.ChangeEvent) => { + const value = event.target?.value; + const newParams = new URLSearchParams(params); + newParams.delete('page'); + if (value) newParams.set('search', value); + else newParams.delete('search'); + router.replace(`${pathname}?${newParams.toString()}`); + }, 200); + + return ( + <> + + + + ); +}; + +export default Search; diff --git a/steps/04.01-data-fetching/src/components/TextField.tsx b/steps/04.01-data-fetching/src/components/TextField.tsx new file mode 100644 index 0000000..d8061e9 --- /dev/null +++ b/steps/04.01-data-fetching/src/components/TextField.tsx @@ -0,0 +1,31 @@ +import clsx from 'clsx'; + +type TextFieldProps = React.InputHTMLAttributes & { + label: string; + id: string; + type?: string; + className?: string; + errorMessages?: Array; +}; + +const TextField: React.FC = ({ label, id, type = 'text', className, errorMessages, ...restProps }) => { + return ( +
+ + + {errorMessages?.length &&

{errorMessages[0]}

} +
+ ); +}; + +export default TextField; diff --git a/steps/04.01-data-fetching/src/data/employees.json b/steps/04.01-data-fetching/src/data/employees.json new file mode 100644 index 0000000..5f5342e --- /dev/null +++ b/steps/04.01-data-fetching/src/data/employees.json @@ -0,0 +1,152 @@ +[ + { + "id": "5763cd4d9d2a4f259b53c901", + "photo": "/portraits/women/85.jpg", + "firstname": "Leanne", + "lastname": "Woodard", + "position": "Developer", + "entryDate": "27/10/2015", + "birthDate": "02/01/1974", + "gender": "f", + "email": "woodard.l@acme.com", + "phone": "0784112248", + "isManager": false, + "manager": "Erika", + "managerId": "5763cd4d3b57c672861bfa1f" + }, + { + "id": "5763cd4d51fdb6588742f99e", + "photo": "/portraits/men/56.jpg", + "firstname": "Castaneda", + "lastname": "Salinas", + "position": "Developer", + "entryDate": "04/10/2015", + "birthDate": "22/01/1963", + "gender": "m", + "email": "salinas.c@acme.com", + "phone": "0145652522", + "isManager": false, + "manager": "Erika", + "managerId": "5763cd4d3b57c672861bfa1f" + }, + { + "id": "5763cd4dba6362a3f92c954e", + "photo": "/portraits/women/24.jpg", + "firstname": "Phyllis", + "lastname": "Donovan", + "position": "Sales", + "entryDate": "30/03/2015", + "birthDate": "30/11/1951", + "gender": "f", + "email": "donovan.p@acme.com", + "phone": "0685230125", + "isManager": false, + "manager": "Erika", + "managerId": "5763cd4d3b57c672861bfa1f" + }, + { + "id": "5763cd4d3b57c672861bfa1f", + "photo": "/portraits/women/65.jpg", + "firstname": "Erika", + "lastname": "Guzman", + "position": "Product Owner", + "entryDate": "13/05/2016", + "birthDate": "19/03/1962", + "gender": "f", + "email": "guzman.e@acme.com", + "phone": "0678412587", + "isManager": true, + "manager": "Mercedes", + "managerId": "5763cd4d979b62a209809160" + }, + { + "id": "5763cd4d5fc36e4f842ca5a9", + "photo": "/portraits/men/30.jpg", + "firstname": "Moody", + "lastname": "Prince", + "position": "Developer", + "entryDate": "28/09/2015", + "birthDate": "15/04/1971", + "gender": "m", + "email": "prince.m@acme.com", + "phone": "0662589632", + "isManager": false, + "manager": "Mercedes", + "managerId": "5763cd4d979b62a209809160" + }, + { + "id": "5763cd4d979b62a209809160", + "photo": "/portraits/women/8.jpg", + "firstname": "Mercedes", + "lastname": "Hebert", + "position": "Product Owner", + "entryDate": "02/01/2016", + "birthDate": "20/07/1947", + "gender": "f", + "email": "hebert.m@acme.com", + "phone": "0125878522", + "isManager": true, + "manager": "Mclaughlin", + "managerId": "5763cd4dfa6f96cd26c65787" + }, + { + "id": "5763cd4d15e6c2c28b70f2e8", + "photo": "/portraits/men/86.jpg", + "firstname": "Howell", + "lastname": "Mcknight", + "position": "Sales", + "entryDate": "26/09/2015", + "birthDate": "18/07/1979", + "gender": "m", + "email": "mcknight.h@acme.com", + "phone": "0456987425", + "isManager": false, + "manager": "Mclaughlin", + "managerId": "5763cd4dfa6f96cd26c65787" + }, + { + "id": "5763cd4d5d6ad8dfc6c34883", + "photo": "/portraits/women/93.jpg", + "firstname": "Lizzie", + "lastname": "Morris", + "position": "Human Resources", + "entryDate": "03/05/2016", + "birthDate": "15/11/1981", + "gender": "f", + "email": "morris.l@acme.com", + "phone": "0662259988", + "isManager": false, + "manager": "Mclaughlin", + "managerId": "5763cd4dfa6f96cd26c65787" + }, + { + "id": "5763cd4dc378a38ecd387737", + "photo": "/portraits/men/34.jpg", + "firstname": "Roy", + "lastname": "Nielsen", + "position": "Sales", + "entryDate": "17/05/2016", + "birthDate": "21/10/1951", + "gender": "m", + "email": "nielsen.r@acme.com", + "phone": "0755669551", + "isManager": false, + "manager": "Mclaughlin", + "managerId": "5763cd4dfa6f96cd26c65787" + }, + { + "id": "5763cd4dfa6f96cd26c65787", + "photo": "/portraits/men/78.jpg", + "firstname": "Mclaughlin", + "lastname": "Cochran", + "position": "Director", + "entryDate": "11/04/2016", + "birthDate": "19/03/1973", + "gender": "m", + "email": "cochran.m@acme.com", + "phone": "0266334856", + "isManager": true, + "manager": "", + "managerId": "" + } +] diff --git a/steps/04.01-data-fetching/src/data/expenses.json b/steps/04.01-data-fetching/src/data/expenses.json new file mode 100644 index 0000000..0d096dc --- /dev/null +++ b/steps/04.01-data-fetching/src/data/expenses.json @@ -0,0 +1,342 @@ +[ + { + "id": "0475830f-a563-44e0-8c5c-6d829c11a132", + "employeeId": "5763cd4d51fdb6588742f99e", + "price": { + "priceIncludingTax": 120.50, + "taxAmount": 20.50, + "priceExcludingTax": 100, + "currency": "EUR" + }, + "label": "Business Lunch", + "description": "Lunch with a client to discuss a new project.", + "category": "Meals", + "receiptLink": "https://example.com/receipt1.pdf", + "status": "approved", + "creationDate": "2024-03-15T09:30:00Z", + "updateDate": "2024-03-18T14:45:00Z" + }, + { + "id": "a2e8b2c4-99d8-4c13-9a9c-0a8a68623260", + "employeeId": "5763cd4d51fdb6588742f99e", + "price": { + "priceIncludingTax": 55.20, + "taxAmount": 7.20, + "priceExcludingTax": 48, + "currency": "USD" + }, + "label": "Office Supplies", + "description": "Purchase of paper, pens, etc.", + "category": "Supplies", + "receiptLink": "https://example.com/receipt2.jpg", + "status": "created", + "creationDate": "2024-05-02T11:20:00Z", + "updateDate": "2024-05-02T11:20:00Z" + }, + { + "id": "3d3fb561-0d9c-4021-8285-d2f49c40c47d", + "employeeId": "5763cd4d51fdb6588742f99e", + "price": { + "priceIncludingTax": 250, + "taxAmount": 0, + "priceExcludingTax": 250, + "currency": "EUR" + }, + "label": "Plane Ticket", + "description": "Business trip to Berlin.", + "category": "Travel", + "receiptLink": "https://example.com/receipt3.png", + "status": "declined", + "creationDate": "2024-04-10T16:45:00Z", + "updateDate": "2024-04-12T09:30:00Z" + }, + { + "id": "4c41689c-e5f5-4029-a181-38c498e7a82b", + "employeeId": "5763cd4d5fc36e4f842ca5a9", + "price": { + "priceIncludingTax": 80.00, + "taxAmount": 13.33, + "priceExcludingTax": 66.67, + "currency": "USD" + }, + "label": "Hotel", + "description": "Hotel night in London.", + "category": "Accommodation", + "receiptLink": "https://example.com/receipt4.pdf", + "status": "approved", + "creationDate": "2024-06-20T08:15:00Z", + "updateDate": "2024-06-22T10:30:00Z" + }, + { + "id": "871030e0-e485-41d5-882c-30201429a23f", + "employeeId": "5763cd4d5fc36e4f842ca5a9", + "price": { + "priceIncludingTax": 35.75, + "taxAmount": 5.75, + "priceExcludingTax": 30, + "currency": "EUR" + }, + "label": "Taxi Fare", + "description": "Ride from the airport to the office.", + "category": "Transportation", + "receiptLink": "https://example.com/receipt5.jpg", + "status": "submitted", + "creationDate": "2024-07-05T19:00:00Z", + "updateDate": "2024-07-05T19:00:00Z" + }, + { + "id": "e38e7368-8686-4211-a6a2-844806839a4c", + "employeeId": "5763cd4d979b62a209809160", + "price": { + "priceIncludingTax": 180.00, + "taxAmount": 30.00, + "priceExcludingTax": 150, + "currency": "USD" + }, + "label": "Car Rental", + "description": "Car rental for a week.", + "category": "Transportation", + "receiptLink": "https://example.com/receipt6.png", + "status": "approved", + "creationDate": "2024-02-28T13:40:00Z", + "updateDate": "2024-03-02T11:15:00Z" + }, + { + "id": "90432a7b-3009-491a-832b-a4622721a490", + "employeeId": "5763cd4d15e6c2c28b70f2e8", + "price": { + "priceIncludingTax": 65.00, + "taxAmount": 10.83, + "priceExcludingTax": 54.17, + "currency": "USD" + }, + "label": "Client Meal", + "description": "Dinner with a potential client.", + "category": "Meals", + "receiptLink": "https://example.com/receipt7.pdf", + "status": "in_review", + "creationDate": "2024-07-18T20:30:00Z", + "updateDate": "2024-07-19T09:00:00Z" + }, + { + "id": "a88a026a-e928-48b6-8a1d-90d980e7a423", + "employeeId": "5763cd4d5d6ad8dfc6c34883", + "price": { + "priceIncludingTax": 95.99, + "taxAmount": 15.99, + "priceExcludingTax": 80, + "currency": "EUR" + }, + "label": "Software", + "description": "Monthly subscription to project management software.", + "category": "Software", + "receiptLink": "https://example.com/receipt8.jpg", + "status": "approved", + "creationDate": "2024-05-10T10:00:00Z", + "updateDate": "2024-05-11T14:20:00Z" + }, + { + "id": "21e5615a-9c2a-40f2-a489-0c2f4a866098", + "employeeId": "5763cd4dc378a38ecd387737", + "price": { + "priceIncludingTax": 25.50, + "taxAmount": 4.25, + "priceExcludingTax": 21.25, + "currency": "USD" + }, + "label": "Parking Fees", + "description": "Parking fees at the airport.", + "category": "Transportation", + "receiptLink": "https://example.com/receipt9.png", + "status": "created", + "creationDate": "2024-07-30T17:45:00Z", + "updateDate": "2024-07-30T17:45:00Z" + }, + { + "id": "5200c69a-e387-4292-9282-98e66878524c", + "employeeId": "5763cd4dfa6f96cd26c65787", + "price": { + "priceIncludingTax": 150.00, + "taxAmount": 25.00, + "priceExcludingTax": 125, + "currency": "USD" + }, + "label": "Training", + "description": "Participation in professional training.", + "category": "Training", + "receiptLink": "https://example.com/receipt10.pdf", + "status": "approved", + "creationDate": "2024-04-05T09:00:00Z", + "updateDate": "2024-04-07T11:30:00Z" + }, + { + "id": "9b60a0a2-d2cf-41ab-a8a6-690080e77898", + "employeeId": "5763cd4d9d2a4f259b53c901", + "price": { + "priceIncludingTax": 75.20, + "taxAmount": 12.53, + "priceExcludingTax": 62.67, + "currency": "EUR" + }, + "label": "Office Supplies", + "description": "Purchase of office supplies.", + "category": "Supplies", + "receiptLink": "https://example.com/receipt11.jpg", + "status": "approved", + "creationDate": "2024-06-12T14:20:00Z", + "updateDate": "2024-06-14T10:15:00Z" + }, + { + "id": "8478041f-7a41-46f0-9158-34759c409873", + "employeeId": "5763cd4d51fdb6588742f99e", + "price": { + "priceIncludingTax": 280.00, + "taxAmount": 46.67, + "priceExcludingTax": 233.33, + "currency": "USD" + }, + "label": "Plane Ticket", + "description": "Business trip to New York.", + "category": "Travel", + "receiptLink": "https://example.com/receipt12.png", + "status": "submitted", + "creationDate": "2024-07-25T11:30:00Z", + "updateDate": "2024-07-25T11:30:00Z" + }, + { + "id": "4902854f-2049-4498-9597-74823829501f", + "employeeId": "5763cd4dba6362a3f92c954e", + "price": { + "priceIncludingTax": 95.00, + "taxAmount": 15.83, + "priceExcludingTax": 79.17, + "currency": "EUR" + }, + "label": "Hotel", + "description": "Hotel night in Paris.", + "category": "Accommodation", + "receiptLink": "https://example.com/receipt13.pdf", + "status": "in_review", + "creationDate": "2024-08-01T08:45:00Z", + "updateDate": "2024-08-02T10:00:00Z" + }, + { + "id": "4309573f-8903-4a42-a095-839529490582", + "employeeId": "5763cd4d3b57c672861bfa1f", + "price": { + "priceIncludingTax": 42.50, + "taxAmount": 7.08, + "priceExcludingTax": 35.42, + "currency": "EUR" + }, + "label": "Taxi Fare", + "description": "Ride from the station to the conference venue.", + "category": "Transportation", + "receiptLink": "https://example.com/receipt14.jpg", + "status": "approved", + "creationDate": "2024-05-20T18:30:00Z", + "updateDate": "2024-05-22T09:15:00Z" + }, + { + "id": "3028759f-9023-4a83-a785-928375928375", + "employeeId": "5763cd4d5fc36e4f842ca5a9", + "price": { + "priceIncludingTax": 190.00, + "taxAmount": 31.67, + "priceExcludingTax": 158.33, + "currency": "USD" + }, + "label": "Car Rental", + "description": "Weekend car rental.", + "category": "Transportation", + "receiptLink": "https://example.com/receipt15.png", + "status": "created", + "creationDate": "2024-07-28T12:00:00Z", + "updateDate": "2024-07-28T12:00:00Z" + }, + { + "id": "92837592-8375-9283-7592-837592837592", + "employeeId": "5763cd4d979b62a209809160", + "price": { + "priceIncludingTax": 70.00, + "taxAmount": 11.67, + "priceExcludingTax": 58.33, + "currency": "USD" + }, + "label": "Client Meal", + "description": "Lunch with a client to discuss a project.", + "category": "Meals", + "receiptLink": "https://example.com/receipt16.pdf", + "status": "declined", + "creationDate": "2024-03-08T13:15:00Z", + "updateDate": "2024-03-10T09:30:00Z" + }, + { + "id": "85930583-0385-9305-8303-859305830385", + "employeeId": "5763cd4d15e6c2c28b70f2e8", + "price": { + "priceIncludingTax": 110.50, + "taxAmount": 18.42, + "priceExcludingTax": 92.08, + "currency": "EUR" + }, + "label": "Software", + "description": "Purchase of a license for design software.", + "category": "Software", + "receiptLink": "https://example.com/receipt17.jpg", + "status": "approved", + "creationDate": "2024-06-05T10:45:00Z", + "updateDate": "2024-06-07T14:30:00Z" + }, + { + "id": "03958305-8305-9385-0385-930583059385", + "employeeId": "5763cd4d5d6ad8dfc6c34883", + "price": { + "priceIncludingTax": 35.00, + "taxAmount": 5.83, + "priceExcludingTax": 29.17, + "currency": "USD" + }, + "label": "Parking Fees", + "description": "Parking fees at the conference center.", + "category": "Transportation", + "receiptLink": "https://example.com/receipt18.png", + "status": "submitted", + "creationDate": "2024-07-15T16:20:00Z", + "updateDate": "2024-07-15T16:20:00Z" + }, + { + "id": "93850395-8503-9585-0395-850395850395", + "employeeId": "5763cd4dc378a38ecd387737", + "price": { + "priceIncludingTax": 165.00, + "taxAmount": 27.50, + "priceExcludingTax": 137.50, + "currency": "USD" + }, + "label": "Training", + "description": "Registration for a webinar on digital marketing.", + "category": "Training", + "receiptLink": "https://example.com/receipt19.pdf", + "status": "in_review", + "creationDate": "2024-07-10T09:00:00Z", + "updateDate": "2024-07-11T14:30:00Z" + }, + { + "id": "85039585-0395-8503-9585-039585039585", + "employeeId": "5763cd4dfa6f96cd26c65787", + "price": { + "priceIncludingTax": 82.75, + "taxAmount": 13.79, + "priceExcludingTax": 68.96, + "currency": "EUR" + }, + "label": "Office Supplies", + "description": "Purchase of ink cartridges for the printer.", + "category": "Supplies", + "receiptLink": "https://example.com/receipt20.jpg", + "status": "approved", + "creationDate": "2024-06-28T11:45:00Z", + "updateDate": "2024-06-30T09:15:00Z" + } +] \ No newline at end of file diff --git a/steps/04.01-data-fetching/src/functions/timing.ts b/steps/04.01-data-fetching/src/functions/timing.ts new file mode 100644 index 0000000..3b8c6f3 --- /dev/null +++ b/steps/04.01-data-fetching/src/functions/timing.ts @@ -0,0 +1,7 @@ +export const debounce = (fn: Function, ms = 300) => { + let timeoutId: ReturnType; + return function (this: any, ...args: any[]) { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => fn.apply(this, args), ms); + }; +}; diff --git a/steps/04.01-data-fetching/src/styles/global.css b/steps/04.01-data-fetching/src/styles/global.css new file mode 100644 index 0000000..f77ed90 --- /dev/null +++ b/steps/04.01-data-fetching/src/styles/global.css @@ -0,0 +1,41 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --color-bg-global: #e9effc; + --color-bg-primary: #ffffff; + --color-bg-secondary: #f5f5f5; + --color-text-primary: #000000; + + --spacing-sm: 0.5rem; + --spacing-md: 0.75rem; + --spacing-lg: 1rem; + --spacing-xl: 1.5rem; +} + +/* Headings */ + +.heading1 { + font-size: 2rem; + font-weight: bold; + margin-bottom: var(--spacing-lg); +} + +.heading2 { + font-size: 1.5rem; + font-weight: bold; + margin-bottom: var(--spacing-lg); +} + +.heading3 { + font-size: 1.125rem; + font-weight: bold; + margin-bottom: var(--spacing-lg); +} + +.heading4 { + font-size: 1rem; + font-weight: bold; + margin-bottom: var(--spacing-lg); +} diff --git a/steps/04.01-data-fetching/src/types.ts b/steps/04.01-data-fetching/src/types.ts new file mode 100644 index 0000000..82cffb5 --- /dev/null +++ b/steps/04.01-data-fetching/src/types.ts @@ -0,0 +1,39 @@ +export type Person = { + id: string; + photo?: string; + firstname: string; + lastname: string; + position: string; + entryDate: string; + birthDate: string; + gender: string; + email: string; + phone: string; + isManager: boolean; + manager?: string; + managerId?: string; +}; + +export type Expense = { + id: string; + employeeId: string; + price: { + priceIncludingTax: number; + taxAmount: number; + priceExcludingTax: number; + currency: string; + }; + label: string; + description: string; + category: string; + receiptLink: string; + status: 'approved' | 'created' | 'declined'; + creationDate: string; + updateDate: string; +}; + +export type PaginationAttributes = { + per_page?: number; + page: number; + total_pages: number; +}; diff --git a/steps/04.01-data-fetching/tailwind.config.js b/steps/04.01-data-fetching/tailwind.config.js new file mode 100644 index 0000000..eaa361c --- /dev/null +++ b/steps/04.01-data-fetching/tailwind.config.js @@ -0,0 +1,9 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ['./src/**/*.{js,ts,jsx,tsx,mdx}'], + darkMode: 'selector', + theme: { + extend: {}, + }, + plugins: [], +}; diff --git a/steps/04.01-data-fetching/tsconfig.json b/steps/04.01-data-fetching/tsconfig.json new file mode 100644 index 0000000..7b28589 --- /dev/null +++ b/steps/04.01-data-fetching/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/steps/04.02-caching-solution/.env.example b/steps/04.02-caching-solution/.env.example new file mode 100644 index 0000000..1ebabff --- /dev/null +++ b/steps/04.02-caching-solution/.env.example @@ -0,0 +1,2 @@ +API_BASE_URL=http://localhost:3001 +API_KEY=XXXX diff --git a/steps/04.02-caching-solution/.eslintrc.json b/steps/04.02-caching-solution/.eslintrc.json new file mode 100644 index 0000000..bffb357 --- /dev/null +++ b/steps/04.02-caching-solution/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/steps/04.02-caching-solution/.gitignore b/steps/04.02-caching-solution/.gitignore new file mode 100644 index 0000000..fd3dbb5 --- /dev/null +++ b/steps/04.02-caching-solution/.gitignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/steps/04.02-caching-solution/README.md b/steps/04.02-caching-solution/README.md new file mode 100644 index 0000000..3df355a --- /dev/null +++ b/steps/04.02-caching-solution/README.md @@ -0,0 +1 @@ +# 04.02 - Caching diff --git a/steps/04.02-caching-solution/next.config.mjs b/steps/04.02-caching-solution/next.config.mjs new file mode 100644 index 0000000..16343f6 --- /dev/null +++ b/steps/04.02-caching-solution/next.config.mjs @@ -0,0 +1,15 @@ +const apiUrl = new URL(process.env.API_BASE_URL || 'http://localhost:3001'); + +/** @type {import('next').NextConfig} */ +const nextConfig = { + images: { + remotePatterns: [ + { + hostname: apiUrl.hostname, + port: apiUrl.port, + }, + ], + }, +}; + +export default nextConfig; diff --git a/steps/04.02-caching-solution/package.json b/steps/04.02-caching-solution/package.json new file mode 100644 index 0000000..c009ce8 --- /dev/null +++ b/steps/04.02-caching-solution/package.json @@ -0,0 +1,38 @@ +{ + "name": "04.02-solution", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "bright": "^0.8.5", + "clsx": "^2.1.1", + "jose": "^5.6.3", + "jsonwebtoken": "^9.0.2", + "next": "14.2.5", + "react": "^18", + "react-dom": "^18", + "react-error-boundary": "^4.0.13", + "rehype-sanitize": "^6.0.0", + "rehype-stringify": "^10.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.0", + "server-only": "^0.0.1", + "showdown": "^2.1.0", + "unified": "^11.0.5" + }, + "devDependencies": { + "@types/jsonwebtoken": "^9.0.6", + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "@types/showdown": "^2.0.6", + "eslint": "^8", + "eslint-config-next": "14.2.5", + "typescript": "^5" + } +} diff --git a/steps/04.02-caching-solution/postcss.config.js b/steps/04.02-caching-solution/postcss.config.js new file mode 100644 index 0000000..33ad091 --- /dev/null +++ b/steps/04.02-caching-solution/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/steps/04.02-caching-solution/public/next.svg b/steps/04.02-caching-solution/public/next.svg new file mode 100644 index 0000000..5174b28 --- /dev/null +++ b/steps/04.02-caching-solution/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/steps/04.02-caching-solution/public/portraits/men/30.jpg b/steps/04.02-caching-solution/public/portraits/men/30.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d04b7a2669245212620be0cbb17922fd1cb3cbfe GIT binary patch literal 4349 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 w3$0jQtjS=6rPW;hG!!51^p@HCR)|Jncu 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 literal 0 HcmV?d00001 diff --git a/steps/04.02-caching-solution/public/portraits/men/78.jpg b/steps/04.02-caching-solution/public/portraits/men/78.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6438e80b9b56fcf7e6e0e9a02eb8cc54a53c91aa GIT binary patch literal 4643 zcmbu8XH=70v&VM`RRTy;=_QDYfDn3>-a!aOns6|5sR>m?K|mCd5;_LyVCW!Sy3$2D z(hmYhI!F-|!Q7y0-Sg#rKiqX^uV>G1&417AXJ$PQVUn-_&g*DsYXArY09zJNKrV6*Yg(Ww|-+&U30|p=fIP6duFJ(hR zJ@8-cZ~_o30Wd0bR_nhW`_BTky#odX0ECh#OQXEdK15a`vVp&k*BQqVnF-}=XHVoj zA`7C4FG%E}v-sUVynMz^fB5?uqfL;i#NJ>;=63qSf@gg951;kIjdDi26VJF2na|zL zm-r69?W_}+gNLax(X;=4FaQZOfePRTcY!Z(0dBwt2ob#pac2KH5Ai$C0C*B}P{iE} z1OhZM!wEPOa|MY}Uw{D)MDIw9I}n!}@dVM%W`E`Z_;;olN3pYd#Fk+?0FW&a2>Sv6 zP`m`-G?GC0nL{9)<^lkn1fVVP-+a$R;yAa7@wk6ud>H`Hg#l38@^9>JJ^*#Z8DEd;KuJzcK~6?VK|w)9MR^X!L<6IyhOsa((lK$ca&dC7va@sZ318vn6@;_1UzNHh zC?YB@F3xpDMnM`OFDxdGI4c67qN0LP!!FU#Tte`$^C14uMrZ@{lpq9zKq0(WDGo_Tvse0Do?0H$k7qyP>EZXWmdzuBY4MmBuAv*A~9yI+yi^ z-}}U*FfCA^)TsyC7)J}R#qBM)clrd0zS)pwaA;>A-pDVemCh3CeQbJ=6VrG%vmIh@ z+^gvLJo@EuGIlb!)v3^~MQ};t@~rJU)Bb__Tz77|n|eQnE}Ygk$4K6OVf(HE=KHJu zq_}lS|NiM=z8eyfP;)Wi%Ok~W8?1-#8(Ni7<|1k%jauBw{am&@`(NJHZ0U!`F!x4h zO0gqpP-gC?^bD6nrBdiu&b6rSaWxB=t2y^=yoJjsQQb8DX(n$Z^-_W?oi}PJ(JrnJ zO+(!Xzt1LbN^LMv=F>If(U~Lt0f{a=RZMcL`c$%$$X7cETj@a;F1sMQ-%IyaedN5t z0=vGI6+@Z5R6zzosw-pIlTUWU6BAZZ?qrw9(du-U^W1T-%ZwL$W@*hDf}TI^o#RyZ z6)R<`$lj5{7r5s3jNN=k`HvNOljQAdM^@q!4ovoL5WH zU0M=2gXM_hl2ChdUfYYAr2#%`E6LQ9DruP>lly}T+CPlqCv|-v4t9N|=9F)y!0#PT zgr27P>9&mNpRNkd(Fapxn8w`DGRdsv$~k0sr}mgBd5XW`DoegMZe?`Hm^y>A!)UIi z=~0s9w_4LN+KV9~S%DDK28&ktgjj`vX6BAT$<8iU- z{NVgMg(#67MtJ-+zEc|AB7IJh7=6P7Jp zNh7aVV^XFj0)tsM^`f!PGrC56_FJFNSGHwyPJFJccVb#nlHf(uV2pHmE>=UQX-+pVUW`vX;0Y7TXhC}GW^V!u)324TEkj9YHt1A zuawrhjrB`{?m2lPjyjxk75MDs?&V&y8A2MBikZ+mjy8CTT0A-o%NrD&NU(~Q-O=by zT_6Bk+154It(fxj7mm(tk_SkbZ@n#Y8J1&L+^=4V@02v?9nSQ7IKs3=dh~?n68&=M zkj%n(-I5&z+nSWqBHivt0oc?W~m%W%&eUX3outNwRbZ$zx3&5s1!2-)9qdOs5~b$wn-F{nk|732QiK5_xvf z0}6i>SqWNKKIjWNuL2oBz1H?I`1CbB#+Rx6656?0W&XiC3`y@Uqe^^bbU?BwF8<`r zmh$x{!?&VxWh5@Ijs{WZPIYH%dw;F;z*I=bk~Q^yJ1AOlmb+S)l3>f*^}#$T%El0f zQvADry7~^QTtJYJeeHL8ae+PzZhM9Ac(FLWX0c{7p6sXiCFn$1zp(VEeE_^)FsD&$ zDplQ0uw49L7N;>PlE7u{#01%#!~3Lu#P;5JES)V+XV44^ElAh72fc}C|| z{`Yc$tm;mh*oo__%wg@_%OB^LSY{XNq9XB3ySU7RTvQ`b8xXt)fWJ>R^>V>C$}l26Pc`{6z$ym8|3n4>~A|sadsA z+i1o*Gs>r%q)^fSG_DSo{Z+l!x5g884^N&yu)zR4Jc*CBXLLk%>g)7-yT}z}c+X-ohrsqV$)kdN zE8XwEHXJKEn#1!?IYSXFBmPWa7$6aamYjp&|E5LZaQ*k6n&UNTSX}nB`fA%?} z&Lmlk*kGoutpI6xY0h&O>uSVi&sYt{J&5C|LlcTUJe?039Q=q~ml9hGE^9!D{B(ik zMw9pUGP}0QW;$#Mmm3?_w!W!V+2&}{k0wzJ9Q?<`{ZX}9b6!oINLpl?a;j>~oo_=W+?#tJ_F4A(*N}SjD%kyY-YbEmysdb&Zfj5o-YAj3VX+PV*1nA?$Mv`ae)S++T zm`p~9$?~jA)vew=w=nZDhl=UsvSRtc=GdIjkelT?x9Mcp>WQVFCVM9Y`o8cy4P&O%-D*+i_q1!P%6gRp^Zt2Zrg z=vQkd1{Qs?tnyEf_O9S(q|#tnDm8|=+FsC2fk#ypOQzz5fq?6kQQ8Lx+?Zuq!{v-u zFJXN0BfYWJ))`s*W^B&`vTLL+(#@TR2q>&NEDk&@YqwDgE)tMBKila&M@6fz_SLov zP3ux}Xly;#iSg*=a}#^_*DsCeG(A&;kr-=5xY6%~?mxq<(Q1mAN1}e^Z<5iR0pPzD;iusUp5a)rv^9~tMFgn{C zaHLf)-@V!X5Ah6znL06EwF(6ZT%X^NA4?DJ+$!H>`diZnSF&4NVMGAca)2+l9q z&KyyW4t8B}no(fw81!ijXX#dxi(R`z*{hIMW&hmLG1Q6AY~8@rR7025r0(%vU>QkV zI1c&=8oKwbsBL^(!k1Cp)BUczE@SWt0W@^S9oXX6H2=Os0IlRS-^tq0DOvUKoL_|Wmt;RA)FE%q6%o7;e(gve zd38D1-#TjSQUCkdV=hbawMSVJ9xcypULaiV>owNq%*!B#b05-gX&m}t@K$r{$Hkk~ zDIM_%Ar$2w@v@yi{laX+w5oe*UOfIQFm!D6XV9eY9W|vGM$J(Z>?3OT_3pz`g9DUl zmDL>iA?2$teP*x5?3bfOI1e(Lny7JK%8wYn*cK5Mz{E>uBYn2t-l--G3 Y-IZTg@->$i4XtycyzDV!v4pAr0qF|a9RL6T literal 0 HcmV?d00001 diff --git a/steps/04.02-caching-solution/public/portraits/men/86.jpg b/steps/04.02-caching-solution/public/portraits/men/86.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9358491105b401a359ef698035ab9b05b84e0457 GIT binary patch literal 5433 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 Kdoq>(yYN42Z*!dh literal 0 HcmV?d00001 diff --git a/steps/04.02-caching-solution/public/portraits/women/65.jpg b/steps/04.02-caching-solution/public/portraits/women/65.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3cab57987a6ff10c5639fad656fb0fc77ba569c9 GIT binary patch literal 5972 zcmbt&Wmr^Q)b<$~Bpga=$WdBpNokPIp*sg82L=g6LQ=X*x>1l0m5`Q}7*eDKL_q<` znRj@eAJ3on{qbGj+Sl3ZzSi3Jz1LdTIe!jj9`g;jt*)Y`0)Rju;4yXqn01^&HAO{h zU40!DHBDt~0swH5-0arj#e6r|?q7V<3#&aG;f_7yhQ&~K zHzc-(f9$3cQb!M%0oF79^Y{SzfGVH>umW}f5^w?B0AGL~>pieD``>v&|M0W{Pb|kC zyL$lv00PT!2H;pOA2x~vd;mwRcf!UUvC9p60&6$3zwrR@-%Nd+gm3h)Et9GP0R9FB z^M?lj2y+48ItqiiEXH82O8@|O9ss)2{^NV5VaNFs8&CQ#27L_x6yX5S()nM^t_%QL zu`{Oo>Sc?t{pTKB?2hB)1OUG)0D#OC0I0CDCNcm2&Hp=ZtoDsQP=W#g!yo|A90P#t z900h7y^q2Ivjivt__%m@c)0l34Idw$fRL1k5Ni~-ZV{7$DJUty6ksqFEz=z;Y6coG zn2wE(0RmxRVWGOi4rOPCGBL9---v*)R6+tmav~yfW@<1s^Z&D9x&bf|5CVkYg4h8Z zFbEe6!t~v|5IDHl?+J9%!aqhph>M3$gah1UAKeCUK)5(J#p4s=-Lwh9!Dhh#8v&)D zJe8iUHz9j6HHVNwC=rc*Q9Z34BCKcXP*`N`NYTI^%Vz`u|A_ymj%DM32mnHCk`Ig( z$HVG_i2fPijW~dd2WF!bWXG2ewI!h9(DSbEnG#Aa!Yl%$xFBrRxL`mQV5&wmhiRhu z|5Ukdy4XSnW6(Jl)m--k5i_d4Yj-5CZE*&`t{k!gGRSm@N_JGn_E>e{Eo<%{~HD9Wb@HHat+$*gkJHQ)YDaKYJNviFlo z2M0_Y?h3UgDEiEy^41Gm0upHITo-Kfsd#hgc)4B*IEg1A!{d5!4 z`)W;MV+hgE_n!OaR-LEp2a1U)K+_~@QWu& zCx?&aQ7xJ01@vk;RUhKkm!-!gjpf^X4!mU1z9_M)YcNp3+nP`8%}38qp5^(E&)Are zt@_Z6n+;vJwD>q9FXJ=QuU;-DMeBqC6zhBhuNOewxLIxli&J0k z-Fr$?Ap>Ihih2(fFBzA5f&z!YF~C^W!TneAalhgn8pj+q_KT{Xr?)1SeZZr6=ox|jd@^phSvu3_q3@F}Sq zZ1e0SIdoiBvHcX%+)5b3SxGJKO)joIrr0aCrhyk=^Wj8@cH3!Cd3e8*uxcWssTQ>c zFS+jktMbm^18q*o39nu}VURqbUU%{ik5Tiu%$L75x!iSs@wR5Ymc62W$}nC3liDgL zPX*K&mC3oh_XNf_58f)5f`48!U-|mi=p;-zEK+Ps=e_lZo>fNQiQpZTSAA(sz7zb8 zR<)e&gxf}Bv#v3RWkBv)T1|oa*5lYf`fHm z%Hcg{Cin?tv^<>k8d&>j% zvOIt=KX7MSE6A}tEcwx1zQ`#ik3sz5gqWlyck|z7gt6GZv;+GWL*#w%F)<)u#fx%YBln#K@lX=}auBG9~ zs^z_$kDFC8gO*OpPm^GS9z9#*Ik$e;y&Soc6IObDVm0XiSG8~*!vN^X%e{kgYO%w4 z`hk}R(OdXK0zyO~0hyOOD$)XZX%2yKSY}F#HwO6bYuOvZU_0tXw7>OMpIODPWlm8M zw~OgZ$B(YO3zv#M7{WkN)h!Av`_duVr%NOk^b^EDwMHWTM0{=u&$fPiOggHL3zxaq z=PmDBuYwrf{VnNw6>NSYmMSQKqBt79?XYa>%Z`eM zb~nxPQfcFgjvugbLiB|KDfo_0>E|3R6_)qxO@(MZyV6qQ*aisqBUp;2?}Wh*GWJy0 z8Bf$g9dz1ZsY$B0=b0`rfR?(&YxS9Falwf0E0LD755{rydE|bO4{3p9kZGktM0h+j zGycq}qlhZ%7wYlm&K5bPDceM{o#54R|60(B(ucI!6uxx2h3dV_>IpKZ1 zT$8>-sQn^B@$q;5VFxgI?Fnr*(uem9LI~>p?O%<3X_}t##;+SN2-nfx8<+Wut%LCw z_1=AWmycrNf!7KjQcx(7nr+i#L#-?9UGQPXA9w-(=Xlar`Fnn{3aV|70{y(>ej6OF zxZ8m_qgWovUCAM@krS(pW#f{GuUatxX)<-H;3ntj$+h=u;t%F&y$Kr868*mMt{sND z)hbhHEC0ynjVO;C^6e1IHU7tW%d0Vvw1REb-_yApWd%R3G>oOi*_DlWbQHv(!C4b+ zU(BA@dKdg6Go4U+?4SEVjzdIgz*^$_Jh~%gSbrcYDN#!qDN!nMJX_FsZuYte+vK|aA9tZO>-8N24Z z!UX__6w}s14?32h{?R=ZE^#N~`ai^A!_diecXZT#m2q9>2ANa7M0lPo#V^K>GH-tT zRvXjrM$IA(~EF-^8c@%c;8dAHHaNV3iXhQ(YKBsK>hYC0s>`d;UAF~3?T8*?H& zz=5yZ7xe#)dKTntNj8X5E^X9d5BuR&NPaG&aD4Hrjr;^#`dH@Wj5etacCe$NPfGPBAuXJniIWYObky z_JxqH{6@5(Zc_nSNsjDA^yF~DI^+$8;Lv zUI99k7&B+sSm2sI`x^r|RlkJ;k8Ww=NYh|k0FUy@g>8f5u9Rr^-C%+b$mIUO#L zzvz=gEz4_>jr3HSB)e8E<#5ZLXU?AtwDRYv8Kx;Dv_KJpiL~tUJwv-mPp7K5c$b4n zRqxWD4<6k&EpON$_9_kN-o(!%7|ggRwldS`jm}fUTgz5ICY6lW32rtxiV*635qh_f z9O~+o&oCtad8ZvX-wSYY*|kp~f!l}Cp>8p7fmAa@b3;RbP@{G27pNCAM(9Zkn6Z6B zua=TS;6h=BS2T#JC`Gy@h=NMWGfBkYkSa=t%nR9>Q)*Y_a;4VrLO9=VqErkd|g^ zQ_tCs_Lsvg$ixBrYf2BJ=cjzC+)G3Ix#B4qj&ICE;@Gtxr?H(I$*BsQN`ZeREbe!T zHhxdTVN&dl9jt6|#@{)`JZo&w2iMsyV~|Zi{V}4`MfHEn+liy2YC&W7tgYv*fR0 zZ8}ncIG^yAAh=Tx20(Nl4^KU%Aa|WrqPOcNCDUUTU61vVQz^^O!vJ>hq=E?!(DF~D z-=|KQgqHb!(gu1GwjXs#rI!r_C~^RMS2)S z-pH}bz1S^%w%q=g{#v_mIRQ!cnf9U_&(K@+gASr5tyZF=E}5+DnmhqbF*5<_sE&Yg z{y_3om!egedYLUxjMtHP)F{q3<%{{>tbCUa8$FDVZ7YMxr3<<|?)RM`_*`||;xIsG z%UF)rOeC}K0}@NN+LwxtXp3b+>`@~sO;#k;yw!o8ues4fTS|wN4fns*8N7WDb9}ZJ zJB>*46JIq|@Al)^M~e&Q|27yTP9M@Ge17i*{3~lmZ+COkX=X;gdRVvvn)H^*nJ}t>}Ttwo`FLMdierm?<|H3Nhq$kv(c$iwWZyDQh4J zzb{ejuVVN>04l&3=h>P4fx~#1GYkXVH_r?n72`OW%Dz3@=i-_srn2x;M$~TX3_g?@ zt>K_eoHyRU2aAPT#jb^NObVc*&RyXjOyt3z)vrY-S7|#bK5EwaH};6a8Jnd&@JeRY z7Ct7RJJc=x13M!eolC3YRJ=xbY!c5eE_YEt{HXaO@^Z_}wZGsVsb<&3-5%NeeJyli ziD#7bU6}{eJu+IKrl7j1yrrU3np-LuAVTd|JS=01G;d7;Aw|=}p@ab_{H?Y$(_!1$ zYw>G54dUIyGVfnE?w0=yD;7>TqHGO}>!C3C75!J(K-EBlDVr+sqZ`^{GjS^sr52cz z3Q?Mc(@6pE5NOtr9ggMrE=ytmhS>5Jd3?!?3cPKFRgQ;MEU)I>_v>i2smsf}`J$UC z+eU^qx{@G~C8j1bb(?{aC)O54yS<@H!MYx`DDSRCt~oPA@=ig8iumr<>h4?XsAp1_ z9p%;W-Q*R#$zi>a=aGxD&2+5MiUtf3ka1F;_}#%gEgEmh7YBko=$fgUfz% z7=Sz+EHEl2bJUXR-L7ShD-`6KmBs;aJZdhx-$bFkpI3!@eXK0QtwyUnAYA2KW(tA^ z1wOIMMGCl*ZfbjcJ4KH_Q%G3x)lZ&@tk~utS?+be8m7B&N$+pscYeRvR?U_kN8(y6 z8(#5nat8yLoeX{;)CsOqOM7-m0iMTAXX>Tn_~USp_jC$h#QI4?bH=)&7F6@4z;c6| z-=D#&RZHsFcW&~q$_lw*X={SFu7-)^MoitF;pZmL1%Jx;x;wU&9+k;hbGtb*F?Sa3 z!g>+obGz_8kn4LtJESMtBoqToP2|V%(L@G%O^Roc*Q?cYt~Lf9OS)SV3OzR*ilfu4 z6tfk2PBiy?KZ?(|e5Y-RyCRd0bhitQG+IF_1PJZ*zCrxxot8PNZhpdUBSul@E}lL^ z^y-U^-QRG3%3;sV%G&L=*%0EC!Y$(~?#ZCM*ATV4JSv9l0vvQ4l8{0dN+HNAK?(l( z9i#RH;k^BO-!BK|rsQUZ*K;lTKjrAND%+G+;kNCTtr&VKy<}x16U`$xn(*=|<5*iI z3nNEre8K)%xX=NgZf7Yf6ka4Q1{Bwr1rAx6^s*<9677jTylwqso#zrphU60!oc@s1 zw7D_BW2%3bwMN~rMX(L(T-a~AcDKX5+W0+ROa&d<8R$2{$)`H0)Q9WsR#P*f!Bp`< z!6o``Xrc_A9`#cx-}x}X@X{T6)Xa*%YX)!pPpB5-k;qi$gDv1Lc-Wv`*D>^p94a*_%)uOVS+QkwBrW zGjwJ=E4A*7hH4WrT-eb(6lXZnKF4)8To+BzP&(@27?gUz#}`F5aqhkU`aU%%yRih{ zKe9L@P!a<)Oz6R2$)~Zl>RVtAHnblhzRw71qE%1j@Gt%DcTe8~p}7aE!+3837EF!1 z4)bs-3|0;jJzQ#=kRtAW1Y+u;dTf4bsAfrn{OV{dlf5 zY;f&IZiBCnG6u^9BwfNP;ov@JiSeh`c6Zh5c;B?BbY{uskr z5zGr<*%Pf$<>^Ic5HUe_BQ^7g?WMv*Gd0}dE{kvqj?!tlL3+{;J5G=R3awyRg-|!S zZBFhX)6V9E<^{{9FK;eMbSTQ^r$*z+9*qa{*NRu%949?-Bs^{V99yp?92tE=+h&ud zYob>YcDOdW&?3UlwMQTiANDop1ioi&S5=tnI zv~);HDJ|dN^W$0HTJQVgTi-tGKId9{@9R2y?|a?%>B#9cIET{G)dCO*1n3YJIGrWR z(Y<`x-WYA9rK^915CDKE(bEy*Pb>}q#w)-Nt*ya*)ykTi>^qh z0pkc*#E(!Q0r#HyZ~TMj&#>!1c>fIhnV~NedZQ*_Zr6XX$Qi!z4?gRK+tJMrL&)(Y zU?j%#7NLf}cGd~CvzNIk;e`Kv`~ezh0}a3pZh%|B9e9EOAWAsB2s8W7JjuU2L*Px| zI1+Xr5DfeX3|HVr;EE8uw}3xzCY&w=zcXQZ5;6#OHv6*^fPZJ|?;>@kM`)QE1pvw1 z>FJIT0Av{eoWz`-9_5{$p5y}n9Rc8D%D;T?6v8++2>!%>eaK7z=_jdu0BjcmKw|{}17X&bHvixFKl3JNpXr1AVF1iR0nqOPApJQ2 z7YO&!*`AJr%YXz*OiTotM z!p6bD#l^*No(IXpiG*`gYXte2;URrtcAZu1|=pTB`1PV076*h9DqQfL{LK6gxb%NAVg4NKmwyjkV>eN zF_<{`GV(}9B#|@m=Dyj~xY5*yyo`>VIWOgh&D$dI*&qP=ztjOiorn~2rY!^mL_`pR z81%33Uu^(`64N6jNEmn|)lD378F_spk~UAr05udss2K_aszAK6TI;TwH76SWdHUyb zgJ(f3Ng?K>o-au_}Kq{Cr}Me!AJi?O%8JB6EbpRQ7gF00sYX}4tfPM?C; z^4~PJtB*D9YRD|co`>ZWetsdu_}OpPen{e7!Be;g$M~~f<;Xlo114$I2c-c-?TUcm z%6VRkF=TENkARht63ue*=2c;J+IK_2eC7h*n)QQJnI$aB>kRmB-4!WB+mTfBr{1Z( znKdITcOA^FBRFDwoKgK%bxCzv9w_ZxEUd(E~^VNUTL%U z`-sev>aapQn6jt>O8i$-Lt3@xO-6|q_OdEY7ERuj_==JT)n7|tnxQfh3-vY@{e-WA zd%#b~SbHnDC~LRe-fPy>B*W<273Ydd*h^~+Zs|RxX)j0alBhPC&)6p`HOd|9K9DI8 zAhAv%oxlEb#?VTwdzv!_d0hI;voboPH_v8ZBgqaibds+|{A=mJw51qPFQu-nbqab- zKaZh4r}80h!*nWXTd6tgO^1?S09jmX!1G5XHO?(p{G zWwUK?a`Ss^@UtEERi2@5pD0HS-jONAF=+f5yj?nK+wdXq!-WpFN9RY*tq?sfdG@CG z3X5V261PSxJ7MCosvuMTU1(0UwWig@Y$ulgdP=0xyzo*u>sHF7Oyhf>`r>PyJs~%w zlQ!^W9_Tc!yE3kRVSpW?zijF*!d>Y?+3?8GZ~aokh4C?iMpI|Cy>8q2s%@}+jr7Df z^KkHeMpc4BHOf$~Zd^rJjpWFz4%M6( zUGl=k0{-yBi|MPg9@kip)T|b_sD-I&_Qo|cMa6tcXo5P&zw368_n6*au+R*wZeFfe z#?&gxj$TkK!L;gKpF#TZ=_0T1kR>fyF)Om5v9pO5xi!ft6dL)3}i?3rU0qehyC&Dr+{hl9GOY%#^l%l2l+L1dAXC_!gUfkumyd(YNoBB<@NQ_ z>&ysc!Bt0P!`<(R>MXpRjO&ToygebtT30{WG#RRwH^JZ7;yCW{GEuZmXfr&xt+LSS zkJ2-CEbfi;Va`z=Jnkmzt|kNFo`3o+>3KpG-YWmOVB;{J*MFBZVfp6CNXH*r+UoRK zoD@c46LRfGa z?Q6$$RkDjpu@Gqm1DdN9eN*Od-zC-QjNds3C{}&t!dH;mn^$C?9#I`paChr80`iDP5fs_Dq&I7c|X z#M(gZ>xW=|tCw=@>mqR0yXh@Tx|CZtu_i)_58{aEtG3Z}4|%zt7*P%%vj!c_1gRkK zuhS*UX=PJH$>cZ2ba?0Ua#w_5u4wweLE0e2#cHlmvE!4rXcwyo(JeJgG6mH%yYj<5 zlcbM|$S@BLsN#>T?SKBLDW|QKU>>V>RnR#RdUBzS(|T}MFTZSliNuOMq)+nXLb#uG zk~bZ9@}@Vwz|{3iO_iO4kF8Ug4ToHD4dfI&c%zx{;S$rm<_w3LlxbgG!^~^ab7Fj( zlZlX3+g1t;O$rHPk*6;j5J8`7mzi+r#BYeVaVe-P)taWkQEz+cgs!|;BNIBI5i3Uz zM)9Mxo89Dm4_??_upHwZEJh%);_^d&M_ya>Bhz2*MA0HtZyO9&xlV43vAuml7KYdMhWQxHoPY_*q zt}(wA{%z^rzR>*1oAJ~R9rxbESNuub)Vl?KCdP~t-n73$9pDou5sNQe^nU1A(|vyB zy<}BdRceB_w^-40E=>4D*-q@ga7Ccj|D z<0a+#O6#z>sdVSuyGSMD>-cA_F2{e+wNr1aQ&$Y><6qkgGtaLYq27D{UG<(;{a4yAk4upwTq zHCR6LduT&4)BXcxv2I|fd(cm+5N0wSTJ=wQRxdN+BSrhtqY(Sc)|lFo1~(SH!Y8+43q-t>1hs-O?-&>EX`s^X^mV^|iIgaA zxW~D0ca?N@LOa{fuu3{~Fh5IeiYqnj*E*tuYW8NvlkCumEAy90l4E(cF2TtAZlIpE zcqq^GYMdj}Fq{$#P|u3CnK=+E-0V-=C8ma8gNbV^F8YsUPha^Sd%%rv_$(^mn$qK+>zS;R#s_oay&Rwm8k z{M@%;?cZFhCdu#@ns*mgo*g?efhrvJtr5B`cbkS`(VrKVNQetSOh4U8>kcwF!Ewog zAvnV4b=13Q-&%udS8N2+3aJIdFy-{4cvso0DK)jzFM<=Y?TSCu_NNM3<57OH6Z@u( z@|R8aYn)`irHx#RsF51pm2M`59$1w1hogQZj@_5nX19G*vrj8pV-IOlBN4FHW!afJ z9Iz~_8M!hqA)~IHYvF4bcqKG?&nayg%et3{?QbRx(qedG_jadHOtoc?2>(5{s#qDN z^GVN{dk6hTmfaI-`Y?`B8f(U^i?N}2& z)pyNDDyb_+!xpR{x_ds>px}C(<#oj}ZDR`Fq}=ItSUGGU=xb5n`yh|LkJ#+wVqC$U z5#vqnC9lSfih{8HI+We#U$XA*>0G)gzEkJ>W>Y-HHop#c-BF3J>(>|A>)QqE zj|W7vHZzI0yTlD4!1XKT?a`lnNY9jMr?~>_k{5en>E(*&q{h~w;c2|mvwV%So3bBu z$vZgtwT#NFm#Pl)iFo-Gl2eUW*9yYTTOz!aRBVs+0wPzh|J;zTZOe-|>_df0ePb-3 zg$|?|46;rXNrq(NCttb|gw-&S= z64Kkbgu=;j^mhaCk~|v^9torA)hv#(Y`iU3;jq9NVwGkYRd(lHKDp%|;nw9_qVKDl zqj}vCk)l`OXIN-+UKEQ5LzKPacb! z?8EKPG2Lg08ymTZXpWLqXW`Da?|*C|HuiH1q4mT+hVsYcy|MbH+^(95`?VhzuRvvK z7F$F0N?4xWWW}+d;21u=)A?i6KUSu>u?_2H2u#z;;B4L5w*q}nYX zlBCH!rO=Dcp_plhW2q7OH)wycf41B|mT58d>M6AM=^q*oZt_(}KKNtBpF>(|R}`4+ zFfDAIm5HG1LHm7W9;7M{&fYCNc=AOiDO78n?2%l5C|<9sni#?N>~#iGy4~utbb>28 z*8c^ixrCHIRkCCum!#0p#3bVV#Ls^5`4}g`6IHI;)dEG-w^-Xcq#J53)G}KmKi>Js zTlQ(M7>zP7L%wn?m)-7CSo<@oyj@_Uiy94(Er=nKD3Dh?NQIfwJBeD3h6jg=bW|eV zjg5s{uoxo*i4&!IRM=fZf+AYgPMn37X)jy&i^xZ_1tZ{leDZQzJHLuKdp1a0S`#Zc z`iD8R^_F$_GmJaho-LW+N8|aP<`Cm>sOEs$3JtX;IDWNJVIYCUy}3oC^rBWuAqzyl z)7Frjw>+SfJ7h>QYQ+5I>&Y!sQ`F3=D{PcKojA|*qAc8KhySS8GCoxG?x@HYcS>7v zY=NNm^hc$Y=;tZBQ43C|o zJJYJ}C+r>zwojEC6&Ow3mVYdh+u>)|Qb#d}eYI+_thIk|@I0VZxPsD$YC~zwFI{xS zhLVlBdC&(-w*`$z)ZwW}ATbQv_AZx(Y_~wYA zsqJEZV%Oe3<_`6ZMM@lOW6?JK#`Jg@8g(aFxT@qcg;?K%A8DN9Jpn!;0K$mSwlQTxI0w=n%u&r7POqyGbm C^!eWa literal 0 HcmV?d00001 diff --git a/steps/04.02-caching-solution/public/portraits/women/85.jpg b/steps/04.02-caching-solution/public/portraits/women/85.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0a900f9e8874eddf5978f08e6de03815f23bb1ea GIT binary patch literal 3912 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 literal 0 HcmV?d00001 diff --git a/steps/04.02-caching-solution/public/portraits/women/93.jpg b/steps/04.02-caching-solution/public/portraits/women/93.jpg new file mode 100644 index 0000000000000000000000000000000000000000..81ea0613898f679ad77f8e4563a93be893f38a07 GIT binary patch literal 4871 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 \ No newline at end of file diff --git a/steps/04.02-caching-solution/src/api/common.ts b/steps/04.02-caching-solution/src/api/common.ts new file mode 100644 index 0000000..494935d --- /dev/null +++ b/steps/04.02-caching-solution/src/api/common.ts @@ -0,0 +1,17 @@ +import { ApiError } from './error'; + +export async function fetchJson(url: string, options?: RequestInit): Promise { + const requestOptions = { + ...options, + headers: { + ...options?.headers, + ['x-api-key']: process.env.API_KEY || 'not-set', + ['content-type']: 'application/json', + }, + }; + const response: Response = await fetch(url, requestOptions); + const data: T | unknown = await response.json(); + if (response.ok) return data as T; + + throw new ApiError(response.statusText, data as unknown); +} diff --git a/steps/04.02-caching-solution/src/api/error.ts b/steps/04.02-caching-solution/src/api/error.ts new file mode 100644 index 0000000..bffb65a --- /dev/null +++ b/steps/04.02-caching-solution/src/api/error.ts @@ -0,0 +1,8 @@ +export class ApiError extends Error { + body; + constructor(message: string, body: unknown) { + super(message); + this.name = 'ApiError'; + this.body = body; + } +} diff --git a/steps/04.02-caching-solution/src/api/expenses.ts b/steps/04.02-caching-solution/src/api/expenses.ts new file mode 100644 index 0000000..e2cb21e --- /dev/null +++ b/steps/04.02-caching-solution/src/api/expenses.ts @@ -0,0 +1,19 @@ +import { Expense } from '@/types'; +import { fetchJson } from './common'; +import qs from 'query-string'; + +const baseUrl = process.env.API_BASE_URL + '/api' || 'http://localhost:3001/api'; +const apiKey = process.env.API_KEY || ''; + +export const findAll = ({ employee }: { employee?: string } = {}): Promise<{ data: Array }> => { + const url = qs.stringifyUrl({ + url: baseUrl + '/expenses', + query: { + employee, + }, + }); + return fetchJson(url, { headers: { ['x-api-key']: apiKey }, next: { tags: ['list-expenses'] } }); +}; + +export const findOne = (id: string): Promise => + fetchJson(`${baseUrl}/expenses/${id}`, { headers: { ['x-api-key']: apiKey, next: { tags: ['single-expense'] } } }); diff --git a/steps/04.02-caching-solution/src/api/people.ts b/steps/04.02-caching-solution/src/api/people.ts new file mode 100644 index 0000000..a1e9e87 --- /dev/null +++ b/steps/04.02-caching-solution/src/api/people.ts @@ -0,0 +1,19 @@ +import { Person } from '@/types'; +import { fetchJson } from './common'; +import qs from 'query-string'; + +const baseUrl = process.env.API_BASE_URL + '/api' || 'http://localhost:3001/api'; +const apiKey = process.env.API_KEY || ''; + +export const findAll = ({ search }: { search?: string }): Promise<{ data: Array }> => { + const url = qs.stringifyUrl({ + url: baseUrl + '/people', + query: { + search, + }, + }); + return fetchJson(url, { headers: { ['x-api-key']: apiKey }, next: { tags: ['all-employees'] } }); +}; + +export const findOne = (id: string): Promise => + fetchJson(`${baseUrl}/people/${id}`, { headers: { ['x-api-key']: apiKey }, next: { tags: ['single-employee'] } }); diff --git a/steps/04.02-caching-solution/src/app/(auth)/layout.tsx b/steps/04.02-caching-solution/src/app/(auth)/layout.tsx new file mode 100644 index 0000000..cac31a7 --- /dev/null +++ b/steps/04.02-caching-solution/src/app/(auth)/layout.tsx @@ -0,0 +1,12 @@ +type AuthLayoutProps = { + children: React.ReactNode; +}; + +const AuthLayout: React.FC = ({ children }) => ( +
+
+
{children}
+
+); + +export default AuthLayout; diff --git a/steps/04.02-caching-solution/src/app/(auth)/login/page.tsx b/steps/04.02-caching-solution/src/app/(auth)/login/page.tsx new file mode 100644 index 0000000..3ae0596 --- /dev/null +++ b/steps/04.02-caching-solution/src/app/(auth)/login/page.tsx @@ -0,0 +1,30 @@ +import { Metadata } from 'next'; + +import TextField from '@/components/TextField'; +import Button from '@/components/Button'; + +export const metadata: Metadata = { + title: 'SFEIR People | Login', +}; + +const LoginPage = () => { + return ( +
+

Welcome !

+ + + + + ); +}; + +export default LoginPage; diff --git a/steps/04.02-caching-solution/src/app/(dashboard)/employees/[id]/edit/page.tsx b/steps/04.02-caching-solution/src/app/(dashboard)/employees/[id]/edit/page.tsx new file mode 100644 index 0000000..e2b65b1 --- /dev/null +++ b/steps/04.02-caching-solution/src/app/(dashboard)/employees/[id]/edit/page.tsx @@ -0,0 +1,24 @@ +import EmployeeForm from '@/components/EmployeeForm'; +import PageTitle from '@/components/PageTitle'; + +import * as peopleApi from '@/api/people'; + +const EmployeeDetail = async ({ params }: { params: { id: string } }) => { + const employee = await peopleApi.findOne(params.id); + + if (!employee) return Single Employee - Not found; + + return ( + <> + + Single Employee - {employee.firstname} {employee.lastname} | Edit + + +
+ +
+ + ); +}; + +export default EmployeeDetail; diff --git a/steps/04.02-caching-solution/src/app/(dashboard)/employees/[id]/page.tsx b/steps/04.02-caching-solution/src/app/(dashboard)/employees/[id]/page.tsx new file mode 100644 index 0000000..ad91c21 --- /dev/null +++ b/steps/04.02-caching-solution/src/app/(dashboard)/employees/[id]/page.tsx @@ -0,0 +1,22 @@ +import PageTitle from '@/components/PageTitle'; +import PersonCard from '@/components/PersonCard'; + +import * as peopleApi from '@/api/people'; +import EmployeeExpenses from '@/components/EmployeeExpenses'; + +const EmployeeDetail = async ({ params }: { params: { id: string } }) => { + const employee = await peopleApi.findOne(params.id); + + if (!employee) return Single Employee - Not found; + + return ( + <> + + Single Employee - {employee.firstname} {employee.lastname} + + } /> + + ); +}; + +export default EmployeeDetail; diff --git a/steps/04.02-caching-solution/src/app/(dashboard)/employees/new/page.tsx b/steps/04.02-caching-solution/src/app/(dashboard)/employees/new/page.tsx new file mode 100644 index 0000000..f1e5157 --- /dev/null +++ b/steps/04.02-caching-solution/src/app/(dashboard)/employees/new/page.tsx @@ -0,0 +1,18 @@ +import EmployeeForm from '@/components/EmployeeForm'; +import PageTitle from '@/components/PageTitle'; + +const EmployeeDetail = async () => { + return ( + <> + + Employees | Create + + +
+ +
+ + ); +}; + +export default EmployeeDetail; diff --git a/steps/04.02-caching-solution/src/app/(dashboard)/employees/page.tsx b/steps/04.02-caching-solution/src/app/(dashboard)/employees/page.tsx new file mode 100644 index 0000000..93c437b --- /dev/null +++ b/steps/04.02-caching-solution/src/app/(dashboard)/employees/page.tsx @@ -0,0 +1,45 @@ +import Link from 'next/link'; + +import Button from '@/components/Button'; +import PageTitle from '@/components/PageTitle'; +import PersonCard from '@/components/PersonCard'; +import Search from '@/components/Search'; + +import * as peopleApi from '@/api/people'; + +const Employees = async ({ searchParams }: { searchParams: { search?: string } }) => { + const search = searchParams.search || undefined; + const employeesData = await peopleApi.findAll({ search }); + + return ( +
+ Employees +
+ + +
+
+ {employeesData?.data?.map((employee) => ( + + + +
+ } + /> + ))} +
+ + ); +}; + +export default Employees; diff --git a/steps/04.02-caching-solution/src/app/(dashboard)/expenses/[id]/page.tsx b/steps/04.02-caching-solution/src/app/(dashboard)/expenses/[id]/page.tsx new file mode 100644 index 0000000..d02e0c7 --- /dev/null +++ b/steps/04.02-caching-solution/src/app/(dashboard)/expenses/[id]/page.tsx @@ -0,0 +1,17 @@ +import ExpenseDetails from '@/components/ExpensesDetails'; +import PageTitle from '@/components/PageTitle'; + +import * as expensesApi from '@/api/expenses'; + +const SingleExpense = async ({ params }: { params: { id: string } }) => { + const expense = await expensesApi.findOne(params.id); + + return ( + <> + Single Expense - {expense?.label || 'Not found'} + {expense && } + + ); +}; + +export default SingleExpense; diff --git a/steps/04.02-caching-solution/src/app/(dashboard)/expenses/page.tsx b/steps/04.02-caching-solution/src/app/(dashboard)/expenses/page.tsx new file mode 100644 index 0000000..8929e32 --- /dev/null +++ b/steps/04.02-caching-solution/src/app/(dashboard)/expenses/page.tsx @@ -0,0 +1,16 @@ +import ExpensesTable from '@/components/ExpensesTable'; +import PageTitle from '@/components/PageTitle'; + +import * as expensesApi from '@/api/expenses'; + +const Expenses = async () => { + const expensesData = await expensesApi.findAll(); + return ( + <> + Expenses + + + ); +}; + +export default Expenses; diff --git a/steps/04.02-caching-solution/src/app/(dashboard)/layout.tsx b/steps/04.02-caching-solution/src/app/(dashboard)/layout.tsx new file mode 100644 index 0000000..f6e4708 --- /dev/null +++ b/steps/04.02-caching-solution/src/app/(dashboard)/layout.tsx @@ -0,0 +1,37 @@ +import { Metadata } from 'next'; +import Link from 'next/link'; +import Image from 'next/image'; + +import { promises as fs } from 'fs'; +import path from 'path'; + +import NavigationMenu from '@/components/NavigationMenu'; + +import logo from '@/assets/svg/logo.svg'; + +type DashboardLayoutProps = { children: React.ReactNode }; + +export const metadata: Metadata = { + title: 'SFEIR People | Dashboard', +}; + +const DashboardLayout: React.FC = async ({ children }) => { + const packageJsonPath = path.join(process.cwd(), 'package.json'); + const packageJsonContent = await fs.readFile(packageJsonPath, 'utf-8'); + const packageJson = JSON.parse(packageJsonContent); + + return ( +
+
+ + People logo + + +
Version: {packageJson.version}
+
+
{children}
+
+ ); +}; + +export default DashboardLayout; diff --git a/steps/04.02-caching-solution/src/app/(dashboard)/page.tsx b/steps/04.02-caching-solution/src/app/(dashboard)/page.tsx new file mode 100644 index 0000000..f581ebb --- /dev/null +++ b/steps/04.02-caching-solution/src/app/(dashboard)/page.tsx @@ -0,0 +1,11 @@ +import PageTitle from '@/components/PageTitle'; + +const HomePage = () => { + return ( + <> + SFEIR People + + ); +}; + +export default HomePage; diff --git a/steps/04.02-caching-solution/src/app/api/revalidate/route.ts b/steps/04.02-caching-solution/src/app/api/revalidate/route.ts new file mode 100644 index 0000000..bf15faf --- /dev/null +++ b/steps/04.02-caching-solution/src/app/api/revalidate/route.ts @@ -0,0 +1,17 @@ +import { revalidatePath, revalidateTag } from 'next/cache'; + +export const GET = (request: NextRequest) => { + const tag = request.nextUrl.searchParams.get('tag'); + + if (!tag) { + return Response.json({ error: 'No tags to revalidate' }, { status: 400 }); + } + + if (tag === 'all') { + revalidatePath('/', 'layout'); + return Response.json({ message: 'Revalidated all data', now: new Date() }); + } + + revalidateTag(tag); + return Response.json({ message: `Revalidated tag "${tag}"`, now: new Date() }); +}; diff --git a/steps/04.02-caching-solution/src/app/layout.tsx b/steps/04.02-caching-solution/src/app/layout.tsx new file mode 100644 index 0000000..e7d90e9 --- /dev/null +++ b/steps/04.02-caching-solution/src/app/layout.tsx @@ -0,0 +1,21 @@ +import type { Metadata } from 'next'; +import { Inter } from 'next/font/google'; + +const inter = Inter({ subsets: ['latin'] }); + +import '@/styles/global.css'; + +export const metadata: Metadata = { + title: 'SFEIR People', + description: 'SFEIR People dashboard application', +}; + +const RootLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => { + return ( + + {children} + + ); +}; + +export default RootLayout; diff --git a/steps/04.02-caching-solution/src/app/rest/expenses/route.ts b/steps/04.02-caching-solution/src/app/rest/expenses/route.ts new file mode 100644 index 0000000..b26cada --- /dev/null +++ b/steps/04.02-caching-solution/src/app/rest/expenses/route.ts @@ -0,0 +1,9 @@ +import { NextRequest } from 'next/server'; + +import * as expensesApi from '@/api/expenses'; + +export const GET = async (request: NextRequest) => { + const employeeId = request.nextUrl.searchParams.get('employeeId'); + const data = await expensesApi.findAll({ employee: employeeId || undefined }); + return Response.json(data); +}; diff --git a/steps/04.02-caching-solution/src/assets/images/profile-placeholder.jpg b/steps/04.02-caching-solution/src/assets/images/profile-placeholder.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6fa00ea6c9e371e542006bb4bd69ae5e6922a324 GIT binary patch literal 11940 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 m>aIBzX~|65y0*1u%UVegt+JNI)Z4`imH;jI + + + + + + + + diff --git a/steps/04.02-caching-solution/src/assets/svg/logoDark.svg b/steps/04.02-caching-solution/src/assets/svg/logoDark.svg new file mode 100644 index 0000000..06eb6ed --- /dev/null +++ b/steps/04.02-caching-solution/src/assets/svg/logoDark.svg @@ -0,0 +1,28 @@ + + + + + + + + + diff --git a/steps/04.02-caching-solution/src/components/Alert.tsx b/steps/04.02-caching-solution/src/components/Alert.tsx new file mode 100644 index 0000000..1c90895 --- /dev/null +++ b/steps/04.02-caching-solution/src/components/Alert.tsx @@ -0,0 +1,17 @@ +import clsx from 'clsx'; + +type AlertProps = { + children: React.ReactNode; + className?: string; +}; + +const Alert: React.FC = ({ children, className }) => ( +
+ {children} +
+); + +export default Alert; diff --git a/steps/04.02-caching-solution/src/components/Button.tsx b/steps/04.02-caching-solution/src/components/Button.tsx new file mode 100644 index 0000000..2cee623 --- /dev/null +++ b/steps/04.02-caching-solution/src/components/Button.tsx @@ -0,0 +1,34 @@ +import clsx from 'clsx'; + +export type ButtonProps = { + children: React.ReactNode; + className?: string; + variant?: 'primary' | 'secondary'; + component?: C; +} & Omit, 'className' | 'variant'>; + +const classNames = { + primary: 'inline-block text-white bg-blue-700 hover:bg-blue-800 font-medium rounded-lg text-sm px-5 py-2.5', + secondary: [ + 'inline-block py-2.5 px-5 text-sm font-medium text-slate-900 bg-white rounded-lg border border-gray-200', + 'hover:bg-gray-100 hover:text-blue-700', + 'dark:bg-slate-900 dark:text-white dark:hover:bg-slate-950 dark:hover:text-blue-200 dark:hover:border-blue-200', + ].join(' '), +}; + +const Button = ({ + children, + className, + variant = 'secondary', + component, + ...restProps +}: ButtonProps) => { + const Component = component || 'button'; + return ( + + {children} + + ); +}; + +export default Button; diff --git a/steps/04.02-caching-solution/src/components/EmployeeExpenses.tsx b/steps/04.02-caching-solution/src/components/EmployeeExpenses.tsx new file mode 100644 index 0000000..ab5073a --- /dev/null +++ b/steps/04.02-caching-solution/src/components/EmployeeExpenses.tsx @@ -0,0 +1,46 @@ +'use client'; + +import { useState } from 'react'; +import Button from './Button'; +import { Expense } from '@/types'; +import ExpensesTable from './ExpensesTable'; +import Alert from './Alert'; + +type EmployeeExpensesProps = { + employeeId: string; +}; + +const EmployeeExpenses: React.FC = ({ employeeId }) => { + const [loadingStatus, setLoadingStatus] = useState('IDLE'); + const [expenses, setExpenses] = useState>(null); + + const handleOpen = async () => { + setLoadingStatus('LOADING'); + try { + const expensesData = (await fetch(`/rest/expenses?employeeId=${employeeId}`).then((res) => res.json())) as { + data: Array; + }; + setExpenses(expensesData.data); + setLoadingStatus('LOADED'); + } catch (err) { + setLoadingStatus('ERROR'); + } + }; + + if (loadingStatus === 'IDLE') { + return ; + } + + if (loadingStatus === 'LOADING') { + return 'Loading...'; + } + + if (loadingStatus === 'LOADED') { + if (!expenses?.length) return No expenses for this employee; + return ; + } + + return Oops, something went wrong :/; +}; + +export default EmployeeExpenses; diff --git a/steps/04.02-caching-solution/src/components/EmployeeForm.tsx b/steps/04.02-caching-solution/src/components/EmployeeForm.tsx new file mode 100644 index 0000000..dc15055 --- /dev/null +++ b/steps/04.02-caching-solution/src/components/EmployeeForm.tsx @@ -0,0 +1,113 @@ +'use client'; + +import Image from 'next/image'; +import TextField from '@/components/TextField'; +import { Person } from '@/types'; +import { useFormState } from 'react-dom'; + +import placeholderImage from '@/assets/images/profile-placeholder.jpg'; +import Button from './Button'; + +type ActionState = { + validationErrors?: { [key: string]: Array }; +}; + +type Action = (id: string, formData: FormData) => Promise; + +type EmployeeFormProps = { + employee?: Person; + action?: Action; + className?: string; +}; + +const initialState = { + validationErrors: {}, +} as ActionState; + +const EmployeeForm: React.FC = ({ employee, action, className }) => { + // @ts-ignore + const [state, formAction] = useFormState(action, initialState as unknown as void); + + return ( +
+
+ {employee +
+
+
+ + + + + +
+
+ + + +
+
+
+ +
+
+ ); +}; + +export default EmployeeForm; diff --git a/steps/04.02-caching-solution/src/components/ExpensesDetails.tsx b/steps/04.02-caching-solution/src/components/ExpensesDetails.tsx new file mode 100644 index 0000000..f59b51a --- /dev/null +++ b/steps/04.02-caching-solution/src/components/ExpensesDetails.tsx @@ -0,0 +1,56 @@ +import { Expense } from '@/types'; +import Paper from './Paper'; + +type ExpenseDetailsRowProps = { + label: string; + value: string; +}; + +const ExpenseDetailsRow: React.FC = ({ label, value }) => ( +
+ {label} + {value} +
+); + +type ExpenseDetailsProps = { + expense: Expense; +}; + +const ExpenseDetails: React.FC = ({ expense }) => ( + <> +
+
+

Information

+ + + + + +
+
+

Workflow

+ + + + + +
+
+
+
+

Amount

+ + + + + +
+
+ +); + +export default ExpenseDetails; diff --git a/steps/04.02-caching-solution/src/components/ExpensesTable.tsx b/steps/04.02-caching-solution/src/components/ExpensesTable.tsx new file mode 100644 index 0000000..1b2e240 --- /dev/null +++ b/steps/04.02-caching-solution/src/components/ExpensesTable.tsx @@ -0,0 +1,61 @@ +'use client'; + +import { Expense } from '@/types'; +import clsx from 'clsx'; +import { useRouter } from 'next/navigation'; + +type ExpensesTableProps = { + expenses: Array; +}; + +const ExpensesTable: React.FC = ({ expenses }) => { + const router = useRouter(); + + const handleClick = (expenseId: string) => () => { + router.push(`/expenses/${expenseId}`); + }; + + return ( + + + + + + + + + + + {expenses.map((expense, index) => ( + + + + + + + ))} + +
+ Label + + Creation date + + Category + + Price +
{expense.label}{new Date(expense.creationDate).toLocaleDateString()}{expense.category} + {expense.price.priceIncludingTax} {expense.price.currency} +
+ ); +}; + +export default ExpensesTable; diff --git a/steps/04.02-caching-solution/src/components/Icons/ArrowLeft.tsx b/steps/04.02-caching-solution/src/components/Icons/ArrowLeft.tsx new file mode 100644 index 0000000..1cc10c7 --- /dev/null +++ b/steps/04.02-caching-solution/src/components/Icons/ArrowLeft.tsx @@ -0,0 +1,25 @@ +type ArrowLeftProps = { + className?: string; +}; + +const ArrowLeft: React.FC = ({ className }) => ( + +); + +export default ArrowLeft; diff --git a/steps/04.02-caching-solution/src/components/Icons/Eye.tsx b/steps/04.02-caching-solution/src/components/Icons/Eye.tsx new file mode 100644 index 0000000..04beb91 --- /dev/null +++ b/steps/04.02-caching-solution/src/components/Icons/Eye.tsx @@ -0,0 +1,11 @@ +type EyeProps = { + className?: string; +}; + +const Eye: React.FC = ({ className }) => ( + + + +); + +export default Eye; diff --git a/steps/04.02-caching-solution/src/components/Icons/Loader.tsx b/steps/04.02-caching-solution/src/components/Icons/Loader.tsx new file mode 100644 index 0000000..9c81994 --- /dev/null +++ b/steps/04.02-caching-solution/src/components/Icons/Loader.tsx @@ -0,0 +1,25 @@ +type LoaderProps = { + className?: string; +}; + +const Loader: React.FC = ({ className }) => ( + +); + +export default Loader; diff --git a/steps/04.02-caching-solution/src/components/NavigationItem.tsx b/steps/04.02-caching-solution/src/components/NavigationItem.tsx new file mode 100644 index 0000000..9f68e10 --- /dev/null +++ b/steps/04.02-caching-solution/src/components/NavigationItem.tsx @@ -0,0 +1,30 @@ +'use client'; + +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; + +import clsx from 'clsx'; + +type NavigationItemsProps = { + href: string; + children: React.ReactNode; +}; + +const NavigationItem: React.FC = ({ href, children }) => { + const pathname = usePathname(); + + return ( + + {children} + + ); +}; + +export default NavigationItem; diff --git a/steps/04.02-caching-solution/src/components/NavigationMenu.tsx b/steps/04.02-caching-solution/src/components/NavigationMenu.tsx new file mode 100644 index 0000000..4b778c6 --- /dev/null +++ b/steps/04.02-caching-solution/src/components/NavigationMenu.tsx @@ -0,0 +1,21 @@ +import NavigationItem from './NavigationItem'; + +const NavigationMenu = () => { + return ( + + ); +}; + +export default NavigationMenu; diff --git a/steps/04.02-caching-solution/src/components/PageTitle.tsx b/steps/04.02-caching-solution/src/components/PageTitle.tsx new file mode 100644 index 0000000..217fe69 --- /dev/null +++ b/steps/04.02-caching-solution/src/components/PageTitle.tsx @@ -0,0 +1,25 @@ +import Link from 'next/link'; + +import ArrowLeft from './Icons/ArrowLeft'; + +type PageTitleProps = { + children: React.ReactNode; + backHref?: string; +}; + +const PageTitle: React.FC = ({ children, backHref }) => ( +
+ {backHref && ( + + + Go back + + )} +

{children}

+
+); + +export default PageTitle; diff --git a/steps/04.02-caching-solution/src/components/Pagination.tsx b/steps/04.02-caching-solution/src/components/Pagination.tsx new file mode 100644 index 0000000..be9a43a --- /dev/null +++ b/steps/04.02-caching-solution/src/components/Pagination.tsx @@ -0,0 +1,95 @@ +'use client'; + +import clsx from 'clsx'; +import Link from 'next/link'; +import { usePathname, useSearchParams } from 'next/navigation'; + +type PaginationProps = { + totalPages: number; + className?: string; +}; + +type PaginationShortcutProps = { + href: string; + disabled?: boolean; + className?: string; + children: React.ReactNode; +}; + +const PaginationShortcut: React.FC = ({ href, disabled, className, children }) => { + const classNames = clsx( + 'block text-center px-3 py-2 ms-0 border bg-white dark:bg-slate-900', + className, + !disabled && + 'hover:bg-gray-100 hover:text-gray-700 text-gray-500 border-gray-300 dark:text-white dark:border-gray-700 dark:hover:bg-slate-950 dark:hover:text-white', + disabled && 'text-gray-300 border-gray-200 dark:text-gray-500 dark:border-gray-600' + ); + + if (disabled) return
{children}
; + + return ( + + {children} + + ); +}; + +const Pagination: React.FC = ({ totalPages, className }) => { + const params = useSearchParams(); + const pathname = usePathname(); + + const currentPage = Number(params.get('page')) || 1; + + const getPageUrl = (page: number): string => { + const newParams = new URLSearchParams(params); + newParams.set('page', page.toString()); + return `${pathname}?${newParams.toString()}`; + }; + + return ( + + ); +}; + +export default Pagination; diff --git a/steps/04.02-caching-solution/src/components/Paper.tsx b/steps/04.02-caching-solution/src/components/Paper.tsx new file mode 100644 index 0000000..8ba656e --- /dev/null +++ b/steps/04.02-caching-solution/src/components/Paper.tsx @@ -0,0 +1,14 @@ +import clsx from 'clsx'; + +type PaperProps = React.HTMLAttributes & { + children: React.ReactNode; + rounded?: boolean; +}; + +const Paper: React.FC = ({ children, rounded = true, ...restProps }) => ( +
+ {children} +
+); + +export default Paper; diff --git a/steps/04.02-caching-solution/src/components/PersonCard.tsx b/steps/04.02-caching-solution/src/components/PersonCard.tsx new file mode 100644 index 0000000..b38ee53 --- /dev/null +++ b/steps/04.02-caching-solution/src/components/PersonCard.tsx @@ -0,0 +1,43 @@ +import Image from 'next/image'; + +import { Person } from '@/types'; + +import placeholderImage from '@/assets/images/profile-placeholder.jpg'; + +type PersonCardProps = React.HTMLAttributes & { + person: Person; + actions?: React.ReactNode; + compact?: boolean; +}; + +const PersonCard: React.FC = ({ person, actions, className, compact = false }) => { + return ( +
+
+ {`Picture + + {person.firstname} {person.lastname} + + {person.position} +
+ + {!compact && ( +
+ {person.phone} + {person.email} + {person.manager && {person.manager}} +
+ )} + + {actions &&
{actions}
} +
+ ); +}; + +export default PersonCard; diff --git a/steps/04.02-caching-solution/src/components/Search.tsx b/steps/04.02-caching-solution/src/components/Search.tsx new file mode 100644 index 0000000..98fe032 --- /dev/null +++ b/steps/04.02-caching-solution/src/components/Search.tsx @@ -0,0 +1,43 @@ +'use client'; + +import { debounce } from '@/functions/timing'; +import clsx from 'clsx'; +import { usePathname, useRouter, useSearchParams } from 'next/navigation'; + +const Search = ({ ...restProps }) => { + const router = useRouter(); + const params = useSearchParams(); + const pathname = usePathname(); + + const handleChange = debounce((event: React.ChangeEvent) => { + const value = event.target?.value; + const newParams = new URLSearchParams(params); + newParams.delete('page'); + if (value) newParams.set('search', value); + else newParams.delete('search'); + router.replace(`${pathname}?${newParams.toString()}`); + }, 200); + + return ( + <> + + + + ); +}; + +export default Search; diff --git a/steps/04.02-caching-solution/src/components/TextField.tsx b/steps/04.02-caching-solution/src/components/TextField.tsx new file mode 100644 index 0000000..d8061e9 --- /dev/null +++ b/steps/04.02-caching-solution/src/components/TextField.tsx @@ -0,0 +1,31 @@ +import clsx from 'clsx'; + +type TextFieldProps = React.InputHTMLAttributes & { + label: string; + id: string; + type?: string; + className?: string; + errorMessages?: Array; +}; + +const TextField: React.FC = ({ label, id, type = 'text', className, errorMessages, ...restProps }) => { + return ( +
+ + + {errorMessages?.length &&

{errorMessages[0]}

} +
+ ); +}; + +export default TextField; diff --git a/steps/04.02-caching-solution/src/functions/timing.ts b/steps/04.02-caching-solution/src/functions/timing.ts new file mode 100644 index 0000000..3b8c6f3 --- /dev/null +++ b/steps/04.02-caching-solution/src/functions/timing.ts @@ -0,0 +1,7 @@ +export const debounce = (fn: Function, ms = 300) => { + let timeoutId: ReturnType; + return function (this: any, ...args: any[]) { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => fn.apply(this, args), ms); + }; +}; diff --git a/steps/04.02-caching-solution/src/styles/global.css b/steps/04.02-caching-solution/src/styles/global.css new file mode 100644 index 0000000..f77ed90 --- /dev/null +++ b/steps/04.02-caching-solution/src/styles/global.css @@ -0,0 +1,41 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --color-bg-global: #e9effc; + --color-bg-primary: #ffffff; + --color-bg-secondary: #f5f5f5; + --color-text-primary: #000000; + + --spacing-sm: 0.5rem; + --spacing-md: 0.75rem; + --spacing-lg: 1rem; + --spacing-xl: 1.5rem; +} + +/* Headings */ + +.heading1 { + font-size: 2rem; + font-weight: bold; + margin-bottom: var(--spacing-lg); +} + +.heading2 { + font-size: 1.5rem; + font-weight: bold; + margin-bottom: var(--spacing-lg); +} + +.heading3 { + font-size: 1.125rem; + font-weight: bold; + margin-bottom: var(--spacing-lg); +} + +.heading4 { + font-size: 1rem; + font-weight: bold; + margin-bottom: var(--spacing-lg); +} diff --git a/steps/04.02-caching-solution/src/types.ts b/steps/04.02-caching-solution/src/types.ts new file mode 100644 index 0000000..82cffb5 --- /dev/null +++ b/steps/04.02-caching-solution/src/types.ts @@ -0,0 +1,39 @@ +export type Person = { + id: string; + photo?: string; + firstname: string; + lastname: string; + position: string; + entryDate: string; + birthDate: string; + gender: string; + email: string; + phone: string; + isManager: boolean; + manager?: string; + managerId?: string; +}; + +export type Expense = { + id: string; + employeeId: string; + price: { + priceIncludingTax: number; + taxAmount: number; + priceExcludingTax: number; + currency: string; + }; + label: string; + description: string; + category: string; + receiptLink: string; + status: 'approved' | 'created' | 'declined'; + creationDate: string; + updateDate: string; +}; + +export type PaginationAttributes = { + per_page?: number; + page: number; + total_pages: number; +}; diff --git a/steps/04.02-caching-solution/tailwind.config.js b/steps/04.02-caching-solution/tailwind.config.js new file mode 100644 index 0000000..eaa361c --- /dev/null +++ b/steps/04.02-caching-solution/tailwind.config.js @@ -0,0 +1,9 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ['./src/**/*.{js,ts,jsx,tsx,mdx}'], + darkMode: 'selector', + theme: { + extend: {}, + }, + plugins: [], +}; diff --git a/steps/04.02-caching-solution/tsconfig.json b/steps/04.02-caching-solution/tsconfig.json new file mode 100644 index 0000000..7b28589 --- /dev/null +++ b/steps/04.02-caching-solution/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/steps/04.02-caching/.env.example b/steps/04.02-caching/.env.example new file mode 100644 index 0000000..1ebabff --- /dev/null +++ b/steps/04.02-caching/.env.example @@ -0,0 +1,2 @@ +API_BASE_URL=http://localhost:3001 +API_KEY=XXXX diff --git a/steps/04.02-caching/.eslintrc.json b/steps/04.02-caching/.eslintrc.json new file mode 100644 index 0000000..bffb357 --- /dev/null +++ b/steps/04.02-caching/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/steps/04.02-caching/.gitignore b/steps/04.02-caching/.gitignore new file mode 100644 index 0000000..fd3dbb5 --- /dev/null +++ b/steps/04.02-caching/.gitignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/steps/04.02-caching/README.md b/steps/04.02-caching/README.md new file mode 100644 index 0000000..3df355a --- /dev/null +++ b/steps/04.02-caching/README.md @@ -0,0 +1 @@ +# 04.02 - Caching diff --git a/steps/04.02-caching/next.config.mjs b/steps/04.02-caching/next.config.mjs new file mode 100644 index 0000000..16343f6 --- /dev/null +++ b/steps/04.02-caching/next.config.mjs @@ -0,0 +1,15 @@ +const apiUrl = new URL(process.env.API_BASE_URL || 'http://localhost:3001'); + +/** @type {import('next').NextConfig} */ +const nextConfig = { + images: { + remotePatterns: [ + { + hostname: apiUrl.hostname, + port: apiUrl.port, + }, + ], + }, +}; + +export default nextConfig; diff --git a/steps/04.02-caching/package.json b/steps/04.02-caching/package.json new file mode 100644 index 0000000..02b0835 --- /dev/null +++ b/steps/04.02-caching/package.json @@ -0,0 +1,38 @@ +{ + "name": "04.02", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "bright": "^0.8.5", + "clsx": "^2.1.1", + "jose": "^5.6.3", + "jsonwebtoken": "^9.0.2", + "next": "14.2.5", + "react": "^18", + "react-dom": "^18", + "react-error-boundary": "^4.0.13", + "rehype-sanitize": "^6.0.0", + "rehype-stringify": "^10.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.0", + "server-only": "^0.0.1", + "showdown": "^2.1.0", + "unified": "^11.0.5" + }, + "devDependencies": { + "@types/jsonwebtoken": "^9.0.6", + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "@types/showdown": "^2.0.6", + "eslint": "^8", + "eslint-config-next": "14.2.5", + "typescript": "^5" + } +} diff --git a/steps/04.02-caching/postcss.config.js b/steps/04.02-caching/postcss.config.js new file mode 100644 index 0000000..33ad091 --- /dev/null +++ b/steps/04.02-caching/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/steps/04.02-caching/public/next.svg b/steps/04.02-caching/public/next.svg new file mode 100644 index 0000000..5174b28 --- /dev/null +++ b/steps/04.02-caching/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/steps/04.02-caching/public/portraits/men/30.jpg b/steps/04.02-caching/public/portraits/men/30.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d04b7a2669245212620be0cbb17922fd1cb3cbfe GIT binary patch literal 4349 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 w3$0jQtjS=6rPW;hG!!51^p@HCR)|Jncu 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 literal 0 HcmV?d00001 diff --git a/steps/04.02-caching/public/portraits/men/78.jpg b/steps/04.02-caching/public/portraits/men/78.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6438e80b9b56fcf7e6e0e9a02eb8cc54a53c91aa GIT binary patch literal 4643 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 Y-IZTg@->$i4XtycyzDV!v4pAr0qF|a9RL6T literal 0 HcmV?d00001 diff --git a/steps/04.02-caching/public/portraits/men/86.jpg b/steps/04.02-caching/public/portraits/men/86.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9358491105b401a359ef698035ab9b05b84e0457 GIT binary patch literal 5433 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 Kdoq>(yYN42Z*!dh literal 0 HcmV?d00001 diff --git a/steps/04.02-caching/public/portraits/women/65.jpg b/steps/04.02-caching/public/portraits/women/65.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3cab57987a6ff10c5639fad656fb0fc77ba569c9 GIT binary patch literal 5972 zcmbt&Wmr^Q)b<$~Bpga=$WdBpNokPIp*sg82L=g6LQ=X*x>1l0m5`Q}7*eDKL_q<` znRj@eAJ3on{qbGj+Sl3ZzSi3Jz1LdTIe!jj9`g;jt*)Y`0)Rju;4yXqn01^&HAO{h zU40!DHBDt~0swH5-0arj#e6r|?q7V<3#&aG;f_7yhQ&~K zHzc-(f9$3cQb!M%0oF79^Y{SzfGVH>umW}f5^w?B0AGL~>pieD``>v&|M0W{Pb|kC zyL$lv00PT!2H;pOA2x~vd;mwRcf!UUvC9p60&6$3zwrR@-%Nd+gm3h)Et9GP0R9FB z^M?lj2y+48ItqiiEXH82O8@|O9ss)2{^NV5VaNFs8&CQ#27L_x6yX5S()nM^t_%QL zu`{Oo>Sc?t{pTKB?2hB)1OUG)0D#OC0I0CDCNcm2&Hp=ZtoDsQP=W#g!yo|A90P#t z900h7y^q2Ivjivt__%m@c)0l34Idw$fRL1k5Ni~-ZV{7$DJUty6ksqFEz=z;Y6coG zn2wE(0RmxRVWGOi4rOPCGBL9---v*)R6+tmav~yfW@<1s^Z&D9x&bf|5CVkYg4h8Z zFbEe6!t~v|5IDHl?+J9%!aqhph>M3$gah1UAKeCUK)5(J#p4s=-Lwh9!Dhh#8v&)D zJe8iUHz9j6HHVNwC=rc*Q9Z34BCKcXP*`N`NYTI^%Vz`u|A_ymj%DM32mnHCk`Ig( z$HVG_i2fPijW~dd2WF!bWXG2ewI!h9(DSbEnG#Aa!Yl%$xFBrRxL`mQV5&wmhiRhu z|5Ukdy4XSnW6(Jl)m--k5i_d4Yj-5CZE*&`t{k!gGRSm@N_JGn_E>e{Eo<%{~HD9Wb@HHat+$*gkJHQ)YDaKYJNviFlo z2M0_Y?h3UgDEiEy^41Gm0upHITo-Kfsd#hgc)4B*IEg1A!{d5!4 z`)W;MV+hgE_n!OaR-LEp2a1U)K+_~@QWu& zCx?&aQ7xJ01@vk;RUhKkm!-!gjpf^X4!mU1z9_M)YcNp3+nP`8%}38qp5^(E&)Are zt@_Z6n+;vJwD>q9FXJ=QuU;-DMeBqC6zhBhuNOewxLIxli&J0k z-Fr$?Ap>Ihih2(fFBzA5f&z!YF~C^W!TneAalhgn8pj+q_KT{Xr?)1SeZZr6=ox|jd@^phSvu3_q3@F}Sq zZ1e0SIdoiBvHcX%+)5b3SxGJKO)joIrr0aCrhyk=^Wj8@cH3!Cd3e8*uxcWssTQ>c zFS+jktMbm^18q*o39nu}VURqbUU%{ik5Tiu%$L75x!iSs@wR5Ymc62W$}nC3liDgL zPX*K&mC3oh_XNf_58f)5f`48!U-|mi=p;-zEK+Ps=e_lZo>fNQiQpZTSAA(sz7zb8 zR<)e&gxf}Bv#v3RWkBv)T1|oa*5lYf`fHm z%Hcg{Cin?tv^<>k8d&>j% zvOIt=KX7MSE6A}tEcwx1zQ`#ik3sz5gqWlyck|z7gt6GZv;+GWL*#w%F)<)u#fx%YBln#K@lX=}auBG9~ zs^z_$kDFC8gO*OpPm^GS9z9#*Ik$e;y&Soc6IObDVm0XiSG8~*!vN^X%e{kgYO%w4 z`hk}R(OdXK0zyO~0hyOOD$)XZX%2yKSY}F#HwO6bYuOvZU_0tXw7>OMpIODPWlm8M zw~OgZ$B(YO3zv#M7{WkN)h!Av`_duVr%NOk^b^EDwMHWTM0{=u&$fPiOggHL3zxaq z=PmDBuYwrf{VnNw6>NSYmMSQKqBt79?XYa>%Z`eM zb~nxPQfcFgjvugbLiB|KDfo_0>E|3R6_)qxO@(MZyV6qQ*aisqBUp;2?}Wh*GWJy0 z8Bf$g9dz1ZsY$B0=b0`rfR?(&YxS9Falwf0E0LD755{rydE|bO4{3p9kZGktM0h+j zGycq}qlhZ%7wYlm&K5bPDceM{o#54R|60(B(ucI!6uxx2h3dV_>IpKZ1 zT$8>-sQn^B@$q;5VFxgI?Fnr*(uem9LI~>p?O%<3X_}t##;+SN2-nfx8<+Wut%LCw z_1=AWmycrNf!7KjQcx(7nr+i#L#-?9UGQPXA9w-(=Xlar`Fnn{3aV|70{y(>ej6OF zxZ8m_qgWovUCAM@krS(pW#f{GuUatxX)<-H;3ntj$+h=u;t%F&y$Kr868*mMt{sND z)hbhHEC0ynjVO;C^6e1IHU7tW%d0Vvw1REb-_yApWd%R3G>oOi*_DlWbQHv(!C4b+ zU(BA@dKdg6Go4U+?4SEVjzdIgz*^$_Jh~%gSbrcYDN#!qDN!nMJX_FsZuYte+vK|aA9tZO>-8N24Z z!UX__6w}s14?32h{?R=ZE^#N~`ai^A!_diecXZT#m2q9>2ANa7M0lPo#V^K>GH-tT zRvXjrM$IA(~EF-^8c@%c;8dAHHaNV3iXhQ(YKBsK>hYC0s>`d;UAF~3?T8*?H& zz=5yZ7xe#)dKTntNj8X5E^X9d5BuR&NPaG&aD4Hrjr;^#`dH@Wj5etacCe$NPfGPBAuXJniIWYObky z_JxqH{6@5(Zc_nSNsjDA^yF~DI^+$8;Lv zUI99k7&B+sSm2sI`x^r|RlkJ;k8Ww=NYh|k0FUy@g>8f5u9Rr^-C%+b$mIUO#L zzvz=gEz4_>jr3HSB)e8E<#5ZLXU?AtwDRYv8Kx;Dv_KJpiL~tUJwv-mPp7K5c$b4n zRqxWD4<6k&EpON$_9_kN-o(!%7|ggRwldS`jm}fUTgz5ICY6lW32rtxiV*635qh_f z9O~+o&oCtad8ZvX-wSYY*|kp~f!l}Cp>8p7fmAa@b3;RbP@{G27pNCAM(9Zkn6Z6B zua=TS;6h=BS2T#JC`Gy@h=NMWGfBkYkSa=t%nR9>Q)*Y_a;4VrLO9=VqErkd|g^ zQ_tCs_Lsvg$ixBrYf2BJ=cjzC+)G3Ix#B4qj&ICE;@Gtxr?H(I$*BsQN`ZeREbe!T zHhxdTVN&dl9jt6|#@{)`JZo&w2iMsyV~|Zi{V}4`MfHEn+liy2YC&W7tgYv*fR0 zZ8}ncIG^yAAh=Tx20(Nl4^KU%Aa|WrqPOcNCDUUTU61vVQz^^O!vJ>hq=E?!(DF~D z-=|KQgqHb!(gu1GwjXs#rI!r_C~^RMS2)S z-pH}bz1S^%w%q=g{#v_mIRQ!cnf9U_&(K@+gASr5tyZF=E}5+DnmhqbF*5<_sE&Yg z{y_3om!egedYLUxjMtHP)F{q3<%{{>tbCUa8$FDVZ7YMxr3<<|?)RM`_*`||;xIsG z%UF)rOeC}K0}@NN+LwxtXp3b+>`@~sO;#k;yw!o8ues4fTS|wN4fns*8N7WDb9}ZJ zJB>*46JIq|@Al)^M~e&Q|27yTP9M@Ge17i*{3~lmZ+COkX=X;gdRVvvn)H^*nJ}t>}Ttwo`FLMdierm?<|H3Nhq$kv(c$iwWZyDQh4J zzb{ejuVVN>04l&3=h>P4fx~#1GYkXVH_r?n72`OW%Dz3@=i-_srn2x;M$~TX3_g?@ zt>K_eoHyRU2aAPT#jb^NObVc*&RyXjOyt3z)vrY-S7|#bK5EwaH};6a8Jnd&@JeRY z7Ct7RJJc=x13M!eolC3YRJ=xbY!c5eE_YEt{HXaO@^Z_}wZGsVsb<&3-5%NeeJyli ziD#7bU6}{eJu+IKrl7j1yrrU3np-LuAVTd|JS=01G;d7;Aw|=}p@ab_{H?Y$(_!1$ zYw>G54dUIyGVfnE?w0=yD;7>TqHGO}>!C3C75!J(K-EBlDVr+sqZ`^{GjS^sr52cz z3Q?Mc(@6pE5NOtr9ggMrE=ytmhS>5Jd3?!?3cPKFRgQ;MEU)I>_v>i2smsf}`J$UC z+eU^qx{@G~C8j1bb(?{aC)O54yS<@H!MYx`DDSRCt~oPA@=ig8iumr<>h4?XsAp1_ z9p%;W-Q*R#$zi>a=aGxD&2+5MiUtf3ka1F;_}#%gEgEmh7YBko=$fgUfz% z7=Sz+EHEl2bJUXR-L7ShD-`6KmBs;aJZdhx-$bFkpI3!@eXK0QtwyUnAYA2KW(tA^ z1wOIMMGCl*ZfbjcJ4KH_Q%G3x)lZ&@tk~utS?+be8m7B&N$+pscYeRvR?U_kN8(y6 z8(#5nat8yLoeX{;)CsOqOM7-m0iMTAXX>Tn_~USp_jC$h#QI4?bH=)&7F6@4z;c6| z-=D#&RZHsFcW&~q$_lw*X={SFu7-)^MoitF;pZmL1%Jx;x;wU&9+k;hbGtb*F?Sa3 z!g>+obGz_8kn4LtJESMtBoqToP2|V%(L@G%O^Roc*Q?cYt~Lf9OS)SV3OzR*ilfu4 z6tfk2PBiy?KZ?(|e5Y-RyCRd0bhitQG+IF_1PJZ*zCrxxot8PNZhpdUBSul@E}lL^ z^y-U^-QRG3%3;sV%G&L=*%0EC!Y$(~?#ZCM*ATV4JSv9l0vvQ4l8{0dN+HNAK?(l( z9i#RH;k^BO-!BK|rsQUZ*K;lTKjrAND%+G+;kNCTtr&VKy<}x16U`$xn(*=|<5*iI z3nNEre8K)%xX=NgZf7Yf6ka4Q1{Bwr1rAx6^s*<9677jTylwqso#zrphU60!oc@s1 zw7D_BW2%3bwMN~rMX(L(T-a~AcDKX5+W0+ROa&d<8R$2{$)`H0)Q9WsR#P*f!Bp`< z!6o``Xrc_A9`#cx-}x}X@X{T6)Xa*%YX)!pPpB5-k;qi$gDv1Lc-Wv`*D>^p94a*_%)uOVS+QkwBrW zGjwJ=E4A*7hH4WrT-eb(6lXZnKF4)8To+BzP&(@27?gUz#}`F5aqhkU`aU%%yRih{ zKe9L@P!a<)Oz6R2$)~Zl>RVtAHnblhzRw71qE%1j@Gt%DcTe8~p}7aE!+3837EF!1 z4)bs-3|0;jJzQ#=kRtAW1Y+u;dTf4bsAfrn{OV{dlf5 zY;f&IZiBCnG6u^9BwfNP;ov@JiSeh`c6Zh5c;B?BbY{uskr z5zGr<*%Pf$<>^Ic5HUe_BQ^7g?WMv*Gd0}dE{kvqj?!tlL3+{;J5G=R3awyRg-|!S zZBFhX)6V9E<^{{9FK;eMbSTQ^r$*z+9*qa{*NRu%949?-Bs^{V99yp?92tE=+h&ud zYob>YcDOdW&?3UlwMQTiANDop1ioi&S5=tnI zv~);HDJ|dN^W$0HTJQVgTi-tGKId9{@9R2y?|a?%>B#9cIET{G)dCO*1n3YJIGrWR z(Y<`x-WYA9rK^915CDKE(bEy*Pb>}q#w)-Nt*ya*)ykTi>^qh z0pkc*#E(!Q0r#HyZ~TMj&#>!1c>fIhnV~NedZQ*_Zr6XX$Qi!z4?gRK+tJMrL&)(Y zU?j%#7NLf}cGd~CvzNIk;e`Kv`~ezh0}a3pZh%|B9e9EOAWAsB2s8W7JjuU2L*Px| zI1+Xr5DfeX3|HVr;EE8uw}3xzCY&w=zcXQZ5;6#OHv6*^fPZJ|?;>@kM`)QE1pvw1 z>FJIT0Av{eoWz`-9_5{$p5y}n9Rc8D%D;T?6v8++2>!%>eaK7z=_jdu0BjcmKw|{}17X&bHvixFKl3JNpXr1AVF1iR0nqOPApJQ2 z7YO&!*`AJr%YXz*OiTotM z!p6bD#l^*No(IXpiG*`gYXte2;URrtcAZu1|=pTB`1PV076*h9DqQfL{LK6gxb%NAVg4NKmwyjkV>eN zF_<{`GV(}9B#|@m=Dyj~xY5*yyo`>VIWOgh&D$dI*&qP=ztjOiorn~2rY!^mL_`pR z81%33Uu^(`64N6jNEmn|)lD378F_spk~UAr05udss2K_aszAK6TI;TwH76SWdHUyb zgJ(f3Ng?K>o-au_}Kq{Cr}Me!AJi?O%8JB6EbpRQ7gF00sYX}4tfPM?C; z^4~PJtB*D9YRD|co`>ZWetsdu_}OpPen{e7!Be;g$M~~f<;Xlo114$I2c-c-?TUcm z%6VRkF=TENkARht63ue*=2c;J+IK_2eC7h*n)QQJnI$aB>kRmB-4!WB+mTfBr{1Z( znKdITcOA^FBRFDwoKgK%bxCzv9w_ZxEUd(E~^VNUTL%U z`-sev>aapQn6jt>O8i$-Lt3@xO-6|q_OdEY7ERuj_==JT)n7|tnxQfh3-vY@{e-WA zd%#b~SbHnDC~LRe-fPy>B*W<273Ydd*h^~+Zs|RxX)j0alBhPC&)6p`HOd|9K9DI8 zAhAv%oxlEb#?VTwdzv!_d0hI;voboPH_v8ZBgqaibds+|{A=mJw51qPFQu-nbqab- zKaZh4r}80h!*nWXTd6tgO^1?S09jmX!1G5XHO?(p{G zWwUK?a`Ss^@UtEERi2@5pD0HS-jONAF=+f5yj?nK+wdXq!-WpFN9RY*tq?sfdG@CG z3X5V261PSxJ7MCosvuMTU1(0UwWig@Y$ulgdP=0xyzo*u>sHF7Oyhf>`r>PyJs~%w zlQ!^W9_Tc!yE3kRVSpW?zijF*!d>Y?+3?8GZ~aokh4C?iMpI|Cy>8q2s%@}+jr7Df z^KkHeMpc4BHOf$~Zd^rJjpWFz4%M6( zUGl=k0{-yBi|MPg9@kip)T|b_sD-I&_Qo|cMa6tcXo5P&zw368_n6*au+R*wZeFfe z#?&gxj$TkK!L;gKpF#TZ=_0T1kR>fyF)Om5v9pO5xi!ft6dL)3}i?3rU0qehyC&Dr+{hl9GOY%#^l%l2l+L1dAXC_!gUfkumyd(YNoBB<@NQ_ z>&ysc!Bt0P!`<(R>MXpRjO&ToygebtT30{WG#RRwH^JZ7;yCW{GEuZmXfr&xt+LSS zkJ2-CEbfi;Va`z=Jnkmzt|kNFo`3o+>3KpG-YWmOVB;{J*MFBZVfp6CNXH*r+UoRK zoD@c46LRfGa z?Q6$$RkDjpu@Gqm1DdN9eN*Od-zC-QjNds3C{}&t!dH;mn^$C?9#I`paChr80`iDP5fs_Dq&I7c|X z#M(gZ>xW=|tCw=@>mqR0yXh@Tx|CZtu_i)_58{aEtG3Z}4|%zt7*P%%vj!c_1gRkK zuhS*UX=PJH$>cZ2ba?0Ua#w_5u4wweLE0e2#cHlmvE!4rXcwyo(JeJgG6mH%yYj<5 zlcbM|$S@BLsN#>T?SKBLDW|QKU>>V>RnR#RdUBzS(|T}MFTZSliNuOMq)+nXLb#uG zk~bZ9@}@Vwz|{3iO_iO4kF8Ug4ToHD4dfI&c%zx{;S$rm<_w3LlxbgG!^~^ab7Fj( zlZlX3+g1t;O$rHPk*6;j5J8`7mzi+r#BYeVaVe-P)taWkQEz+cgs!|;BNIBI5i3Uz zM)9Mxo89Dm4_??_upHwZEJh%);_^d&M_ya>Bhz2*MA0HtZyO9&xlV43vAuml7KYdMhWQxHoPY_*q zt}(wA{%z^rzR>*1oAJ~R9rxbESNuub)Vl?KCdP~t-n73$9pDou5sNQe^nU1A(|vyB zy<}BdRceB_w^-40E=>4D*-q@ga7Ccj|D z<0a+#O6#z>sdVSuyGSMD>-cA_F2{e+wNr1aQ&$Y><6qkgGtaLYq27D{UG<(;{a4yAk4upwTq zHCR6LduT&4)BXcxv2I|fd(cm+5N0wSTJ=wQRxdN+BSrhtqY(Sc)|lFo1~(SH!Y8+43q-t>1hs-O?-&>EX`s^X^mV^|iIgaA zxW~D0ca?N@LOa{fuu3{~Fh5IeiYqnj*E*tuYW8NvlkCumEAy90l4E(cF2TtAZlIpE zcqq^GYMdj}Fq{$#P|u3CnK=+E-0V-=C8ma8gNbV^F8YsUPha^Sd%%rv_$(^mn$qK+>zS;R#s_oay&Rwm8k z{M@%;?cZFhCdu#@ns*mgo*g?efhrvJtr5B`cbkS`(VrKVNQetSOh4U8>kcwF!Ewog zAvnV4b=13Q-&%udS8N2+3aJIdFy-{4cvso0DK)jzFM<=Y?TSCu_NNM3<57OH6Z@u( z@|R8aYn)`irHx#RsF51pm2M`59$1w1hogQZj@_5nX19G*vrj8pV-IOlBN4FHW!afJ z9Iz~_8M!hqA)~IHYvF4bcqKG?&nayg%et3{?QbRx(qedG_jadHOtoc?2>(5{s#qDN z^GVN{dk6hTmfaI-`Y?`B8f(U^i?N}2& z)pyNDDyb_+!xpR{x_ds>px}C(<#oj}ZDR`Fq}=ItSUGGU=xb5n`yh|LkJ#+wVqC$U z5#vqnC9lSfih{8HI+We#U$XA*>0G)gzEkJ>W>Y-HHop#c-BF3J>(>|A>)QqE zj|W7vHZzI0yTlD4!1XKT?a`lnNY9jMr?~>_k{5en>E(*&q{h~w;c2|mvwV%So3bBu z$vZgtwT#NFm#Pl)iFo-Gl2eUW*9yYTTOz!aRBVs+0wPzh|J;zTZOe-|>_df0ePb-3 zg$|?|46;rXNrq(NCttb|gw-&S= z64Kkbgu=;j^mhaCk~|v^9torA)hv#(Y`iU3;jq9NVwGkYRd(lHKDp%|;nw9_qVKDl zqj}vCk)l`OXIN-+UKEQ5LzKPacb! z?8EKPG2Lg08ymTZXpWLqXW`Da?|*C|HuiH1q4mT+hVsYcy|MbH+^(95`?VhzuRvvK z7F$F0N?4xWWW}+d;21u=)A?i6KUSu>u?_2H2u#z;;B4L5w*q}nYX zlBCH!rO=Dcp_plhW2q7OH)wycf41B|mT58d>M6AM=^q*oZt_(}KKNtBpF>(|R}`4+ zFfDAIm5HG1LHm7W9;7M{&fYCNc=AOiDO78n?2%l5C|<9sni#?N>~#iGy4~utbb>28 z*8c^ixrCHIRkCCum!#0p#3bVV#Ls^5`4}g`6IHI;)dEG-w^-Xcq#J53)G}KmKi>Js zTlQ(M7>zP7L%wn?m)-7CSo<@oyj@_Uiy94(Er=nKD3Dh?NQIfwJBeD3h6jg=bW|eV zjg5s{uoxo*i4&!IRM=fZf+AYgPMn37X)jy&i^xZ_1tZ{leDZQzJHLuKdp1a0S`#Zc z`iD8R^_F$_GmJaho-LW+N8|aP<`Cm>sOEs$3JtX;IDWNJVIYCUy}3oC^rBWuAqzyl z)7Frjw>+SfJ7h>QYQ+5I>&Y!sQ`F3=D{PcKojA|*qAc8KhySS8GCoxG?x@HYcS>7v zY=NNm^hc$Y=;tZBQ43C|o zJJYJ}C+r>zwojEC6&Ow3mVYdh+u>)|Qb#d}eYI+_thIk|@I0VZxPsD$YC~zwFI{xS zhLVlBdC&(-w*`$z)ZwW}ATbQv_AZx(Y_~wYA zsqJEZV%Oe3<_`6ZMM@lOW6?JK#`Jg@8g(aFxT@qcg;?K%A8DN9Jpn!;0K$mSwlQTxI0w=n%u&r7POqyGbm C^!eWa literal 0 HcmV?d00001 diff --git a/steps/04.02-caching/public/portraits/women/85.jpg b/steps/04.02-caching/public/portraits/women/85.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0a900f9e8874eddf5978f08e6de03815f23bb1ea GIT binary patch literal 3912 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 literal 0 HcmV?d00001 diff --git a/steps/04.02-caching/public/portraits/women/93.jpg b/steps/04.02-caching/public/portraits/women/93.jpg new file mode 100644 index 0000000000000000000000000000000000000000..81ea0613898f679ad77f8e4563a93be893f38a07 GIT binary patch literal 4871 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 \ No newline at end of file diff --git a/steps/04.02-caching/src/api/common.ts b/steps/04.02-caching/src/api/common.ts new file mode 100644 index 0000000..494935d --- /dev/null +++ b/steps/04.02-caching/src/api/common.ts @@ -0,0 +1,17 @@ +import { ApiError } from './error'; + +export async function fetchJson(url: string, options?: RequestInit): Promise { + const requestOptions = { + ...options, + headers: { + ...options?.headers, + ['x-api-key']: process.env.API_KEY || 'not-set', + ['content-type']: 'application/json', + }, + }; + const response: Response = await fetch(url, requestOptions); + const data: T | unknown = await response.json(); + if (response.ok) return data as T; + + throw new ApiError(response.statusText, data as unknown); +} diff --git a/steps/04.02-caching/src/api/error.ts b/steps/04.02-caching/src/api/error.ts new file mode 100644 index 0000000..bffb65a --- /dev/null +++ b/steps/04.02-caching/src/api/error.ts @@ -0,0 +1,8 @@ +export class ApiError extends Error { + body; + constructor(message: string, body: unknown) { + super(message); + this.name = 'ApiError'; + this.body = body; + } +} diff --git a/steps/04.02-caching/src/api/expenses.ts b/steps/04.02-caching/src/api/expenses.ts new file mode 100644 index 0000000..57ba8f0 --- /dev/null +++ b/steps/04.02-caching/src/api/expenses.ts @@ -0,0 +1,19 @@ +import { Expense } from '@/types'; +import { fetchJson } from './common'; +import qs from 'query-string'; + +const baseUrl = process.env.API_BASE_URL + '/api' || 'http://localhost:3001/api'; +const apiKey = process.env.API_KEY || ''; + +export const findAll = ({ employee }: { employee?: string } = {}): Promise<{ data: Array }> => { + const url = qs.stringifyUrl({ + url: baseUrl + '/expenses', + query: { + employee, + }, + }); + return fetchJson(url, { headers: { ['x-api-key']: apiKey } }); +}; + +export const findOne = (id: string): Promise => + fetchJson(`${baseUrl}/expenses/${id}`, { headers: { ['x-api-key']: apiKey } }); diff --git a/steps/04.02-caching/src/api/people.ts b/steps/04.02-caching/src/api/people.ts new file mode 100644 index 0000000..b4932a7 --- /dev/null +++ b/steps/04.02-caching/src/api/people.ts @@ -0,0 +1,19 @@ +import { Person } from '@/types'; +import { fetchJson } from './common'; +import qs from 'query-string'; + +const baseUrl = process.env.API_BASE_URL + '/api' || 'http://localhost:3001/api'; +const apiKey = process.env.API_KEY || ''; + +export const findAll = ({ search }: { search?: string }): Promise<{ data: Array }> => { + const url = qs.stringifyUrl({ + url: baseUrl + '/people', + query: { + search, + }, + }); + return fetchJson(url, { headers: { ['x-api-key']: apiKey } }); +}; + +export const findOne = (id: string): Promise => + fetchJson(`${baseUrl}/people/${id}`, { headers: { ['x-api-key']: apiKey } }); diff --git a/steps/04.02-caching/src/app/(auth)/layout.tsx b/steps/04.02-caching/src/app/(auth)/layout.tsx new file mode 100644 index 0000000..cac31a7 --- /dev/null +++ b/steps/04.02-caching/src/app/(auth)/layout.tsx @@ -0,0 +1,12 @@ +type AuthLayoutProps = { + children: React.ReactNode; +}; + +const AuthLayout: React.FC = ({ children }) => ( +
+
+
{children}
+
+); + +export default AuthLayout; diff --git a/steps/04.02-caching/src/app/(auth)/login/page.tsx b/steps/04.02-caching/src/app/(auth)/login/page.tsx new file mode 100644 index 0000000..3ae0596 --- /dev/null +++ b/steps/04.02-caching/src/app/(auth)/login/page.tsx @@ -0,0 +1,30 @@ +import { Metadata } from 'next'; + +import TextField from '@/components/TextField'; +import Button from '@/components/Button'; + +export const metadata: Metadata = { + title: 'SFEIR People | Login', +}; + +const LoginPage = () => { + return ( +
+

Welcome !

+ + + + + ); +}; + +export default LoginPage; diff --git a/steps/04.02-caching/src/app/(dashboard)/employees/[id]/edit/page.tsx b/steps/04.02-caching/src/app/(dashboard)/employees/[id]/edit/page.tsx new file mode 100644 index 0000000..e2b65b1 --- /dev/null +++ b/steps/04.02-caching/src/app/(dashboard)/employees/[id]/edit/page.tsx @@ -0,0 +1,24 @@ +import EmployeeForm from '@/components/EmployeeForm'; +import PageTitle from '@/components/PageTitle'; + +import * as peopleApi from '@/api/people'; + +const EmployeeDetail = async ({ params }: { params: { id: string } }) => { + const employee = await peopleApi.findOne(params.id); + + if (!employee) return Single Employee - Not found; + + return ( + <> + + Single Employee - {employee.firstname} {employee.lastname} | Edit + + +
+ +
+ + ); +}; + +export default EmployeeDetail; diff --git a/steps/04.02-caching/src/app/(dashboard)/employees/[id]/page.tsx b/steps/04.02-caching/src/app/(dashboard)/employees/[id]/page.tsx new file mode 100644 index 0000000..ad91c21 --- /dev/null +++ b/steps/04.02-caching/src/app/(dashboard)/employees/[id]/page.tsx @@ -0,0 +1,22 @@ +import PageTitle from '@/components/PageTitle'; +import PersonCard from '@/components/PersonCard'; + +import * as peopleApi from '@/api/people'; +import EmployeeExpenses from '@/components/EmployeeExpenses'; + +const EmployeeDetail = async ({ params }: { params: { id: string } }) => { + const employee = await peopleApi.findOne(params.id); + + if (!employee) return Single Employee - Not found; + + return ( + <> + + Single Employee - {employee.firstname} {employee.lastname} + + } /> + + ); +}; + +export default EmployeeDetail; diff --git a/steps/04.02-caching/src/app/(dashboard)/employees/new/page.tsx b/steps/04.02-caching/src/app/(dashboard)/employees/new/page.tsx new file mode 100644 index 0000000..f1e5157 --- /dev/null +++ b/steps/04.02-caching/src/app/(dashboard)/employees/new/page.tsx @@ -0,0 +1,18 @@ +import EmployeeForm from '@/components/EmployeeForm'; +import PageTitle from '@/components/PageTitle'; + +const EmployeeDetail = async () => { + return ( + <> + + Employees | Create + + +
+ +
+ + ); +}; + +export default EmployeeDetail; diff --git a/steps/04.02-caching/src/app/(dashboard)/employees/page.tsx b/steps/04.02-caching/src/app/(dashboard)/employees/page.tsx new file mode 100644 index 0000000..93c437b --- /dev/null +++ b/steps/04.02-caching/src/app/(dashboard)/employees/page.tsx @@ -0,0 +1,45 @@ +import Link from 'next/link'; + +import Button from '@/components/Button'; +import PageTitle from '@/components/PageTitle'; +import PersonCard from '@/components/PersonCard'; +import Search from '@/components/Search'; + +import * as peopleApi from '@/api/people'; + +const Employees = async ({ searchParams }: { searchParams: { search?: string } }) => { + const search = searchParams.search || undefined; + const employeesData = await peopleApi.findAll({ search }); + + return ( +
+ Employees +
+ + +
+
+ {employeesData?.data?.map((employee) => ( + + + +
+ } + /> + ))} +
+ + ); +}; + +export default Employees; diff --git a/steps/04.02-caching/src/app/(dashboard)/expenses/[id]/page.tsx b/steps/04.02-caching/src/app/(dashboard)/expenses/[id]/page.tsx new file mode 100644 index 0000000..d02e0c7 --- /dev/null +++ b/steps/04.02-caching/src/app/(dashboard)/expenses/[id]/page.tsx @@ -0,0 +1,17 @@ +import ExpenseDetails from '@/components/ExpensesDetails'; +import PageTitle from '@/components/PageTitle'; + +import * as expensesApi from '@/api/expenses'; + +const SingleExpense = async ({ params }: { params: { id: string } }) => { + const expense = await expensesApi.findOne(params.id); + + return ( + <> + Single Expense - {expense?.label || 'Not found'} + {expense && } + + ); +}; + +export default SingleExpense; diff --git a/steps/04.02-caching/src/app/(dashboard)/expenses/page.tsx b/steps/04.02-caching/src/app/(dashboard)/expenses/page.tsx new file mode 100644 index 0000000..8929e32 --- /dev/null +++ b/steps/04.02-caching/src/app/(dashboard)/expenses/page.tsx @@ -0,0 +1,16 @@ +import ExpensesTable from '@/components/ExpensesTable'; +import PageTitle from '@/components/PageTitle'; + +import * as expensesApi from '@/api/expenses'; + +const Expenses = async () => { + const expensesData = await expensesApi.findAll(); + return ( + <> + Expenses + + + ); +}; + +export default Expenses; diff --git a/steps/04.02-caching/src/app/(dashboard)/layout.tsx b/steps/04.02-caching/src/app/(dashboard)/layout.tsx new file mode 100644 index 0000000..f6e4708 --- /dev/null +++ b/steps/04.02-caching/src/app/(dashboard)/layout.tsx @@ -0,0 +1,37 @@ +import { Metadata } from 'next'; +import Link from 'next/link'; +import Image from 'next/image'; + +import { promises as fs } from 'fs'; +import path from 'path'; + +import NavigationMenu from '@/components/NavigationMenu'; + +import logo from '@/assets/svg/logo.svg'; + +type DashboardLayoutProps = { children: React.ReactNode }; + +export const metadata: Metadata = { + title: 'SFEIR People | Dashboard', +}; + +const DashboardLayout: React.FC = async ({ children }) => { + const packageJsonPath = path.join(process.cwd(), 'package.json'); + const packageJsonContent = await fs.readFile(packageJsonPath, 'utf-8'); + const packageJson = JSON.parse(packageJsonContent); + + return ( +
+
+ + People logo + + +
Version: {packageJson.version}
+
+
{children}
+
+ ); +}; + +export default DashboardLayout; diff --git a/steps/04.02-caching/src/app/(dashboard)/page.tsx b/steps/04.02-caching/src/app/(dashboard)/page.tsx new file mode 100644 index 0000000..f581ebb --- /dev/null +++ b/steps/04.02-caching/src/app/(dashboard)/page.tsx @@ -0,0 +1,11 @@ +import PageTitle from '@/components/PageTitle'; + +const HomePage = () => { + return ( + <> + SFEIR People + + ); +}; + +export default HomePage; diff --git a/steps/04.02-caching/src/app/layout.tsx b/steps/04.02-caching/src/app/layout.tsx new file mode 100644 index 0000000..e7d90e9 --- /dev/null +++ b/steps/04.02-caching/src/app/layout.tsx @@ -0,0 +1,21 @@ +import type { Metadata } from 'next'; +import { Inter } from 'next/font/google'; + +const inter = Inter({ subsets: ['latin'] }); + +import '@/styles/global.css'; + +export const metadata: Metadata = { + title: 'SFEIR People', + description: 'SFEIR People dashboard application', +}; + +const RootLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => { + return ( + + {children} + + ); +}; + +export default RootLayout; diff --git a/steps/04.02-caching/src/app/rest/expenses/route.ts b/steps/04.02-caching/src/app/rest/expenses/route.ts new file mode 100644 index 0000000..b26cada --- /dev/null +++ b/steps/04.02-caching/src/app/rest/expenses/route.ts @@ -0,0 +1,9 @@ +import { NextRequest } from 'next/server'; + +import * as expensesApi from '@/api/expenses'; + +export const GET = async (request: NextRequest) => { + const employeeId = request.nextUrl.searchParams.get('employeeId'); + const data = await expensesApi.findAll({ employee: employeeId || undefined }); + return Response.json(data); +}; diff --git a/steps/04.02-caching/src/assets/images/profile-placeholder.jpg b/steps/04.02-caching/src/assets/images/profile-placeholder.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6fa00ea6c9e371e542006bb4bd69ae5e6922a324 GIT binary patch literal 11940 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 m>aIBzX~|65y0*1u%UVegt+JNI)Z4`imH;jI + + + + + + + + diff --git a/steps/04.02-caching/src/assets/svg/logoDark.svg b/steps/04.02-caching/src/assets/svg/logoDark.svg new file mode 100644 index 0000000..06eb6ed --- /dev/null +++ b/steps/04.02-caching/src/assets/svg/logoDark.svg @@ -0,0 +1,28 @@ + + + + + + + + + diff --git a/steps/04.02-caching/src/components/Alert.tsx b/steps/04.02-caching/src/components/Alert.tsx new file mode 100644 index 0000000..1c90895 --- /dev/null +++ b/steps/04.02-caching/src/components/Alert.tsx @@ -0,0 +1,17 @@ +import clsx from 'clsx'; + +type AlertProps = { + children: React.ReactNode; + className?: string; +}; + +const Alert: React.FC = ({ children, className }) => ( +
+ {children} +
+); + +export default Alert; diff --git a/steps/04.02-caching/src/components/Button.tsx b/steps/04.02-caching/src/components/Button.tsx new file mode 100644 index 0000000..2cee623 --- /dev/null +++ b/steps/04.02-caching/src/components/Button.tsx @@ -0,0 +1,34 @@ +import clsx from 'clsx'; + +export type ButtonProps = { + children: React.ReactNode; + className?: string; + variant?: 'primary' | 'secondary'; + component?: C; +} & Omit, 'className' | 'variant'>; + +const classNames = { + primary: 'inline-block text-white bg-blue-700 hover:bg-blue-800 font-medium rounded-lg text-sm px-5 py-2.5', + secondary: [ + 'inline-block py-2.5 px-5 text-sm font-medium text-slate-900 bg-white rounded-lg border border-gray-200', + 'hover:bg-gray-100 hover:text-blue-700', + 'dark:bg-slate-900 dark:text-white dark:hover:bg-slate-950 dark:hover:text-blue-200 dark:hover:border-blue-200', + ].join(' '), +}; + +const Button = ({ + children, + className, + variant = 'secondary', + component, + ...restProps +}: ButtonProps) => { + const Component = component || 'button'; + return ( + + {children} + + ); +}; + +export default Button; diff --git a/steps/04.02-caching/src/components/EmployeeExpenses.tsx b/steps/04.02-caching/src/components/EmployeeExpenses.tsx new file mode 100644 index 0000000..ab5073a --- /dev/null +++ b/steps/04.02-caching/src/components/EmployeeExpenses.tsx @@ -0,0 +1,46 @@ +'use client'; + +import { useState } from 'react'; +import Button from './Button'; +import { Expense } from '@/types'; +import ExpensesTable from './ExpensesTable'; +import Alert from './Alert'; + +type EmployeeExpensesProps = { + employeeId: string; +}; + +const EmployeeExpenses: React.FC = ({ employeeId }) => { + const [loadingStatus, setLoadingStatus] = useState('IDLE'); + const [expenses, setExpenses] = useState>(null); + + const handleOpen = async () => { + setLoadingStatus('LOADING'); + try { + const expensesData = (await fetch(`/rest/expenses?employeeId=${employeeId}`).then((res) => res.json())) as { + data: Array; + }; + setExpenses(expensesData.data); + setLoadingStatus('LOADED'); + } catch (err) { + setLoadingStatus('ERROR'); + } + }; + + if (loadingStatus === 'IDLE') { + return ; + } + + if (loadingStatus === 'LOADING') { + return 'Loading...'; + } + + if (loadingStatus === 'LOADED') { + if (!expenses?.length) return No expenses for this employee; + return ; + } + + return Oops, something went wrong :/; +}; + +export default EmployeeExpenses; diff --git a/steps/04.02-caching/src/components/EmployeeForm.tsx b/steps/04.02-caching/src/components/EmployeeForm.tsx new file mode 100644 index 0000000..dc15055 --- /dev/null +++ b/steps/04.02-caching/src/components/EmployeeForm.tsx @@ -0,0 +1,113 @@ +'use client'; + +import Image from 'next/image'; +import TextField from '@/components/TextField'; +import { Person } from '@/types'; +import { useFormState } from 'react-dom'; + +import placeholderImage from '@/assets/images/profile-placeholder.jpg'; +import Button from './Button'; + +type ActionState = { + validationErrors?: { [key: string]: Array }; +}; + +type Action = (id: string, formData: FormData) => Promise; + +type EmployeeFormProps = { + employee?: Person; + action?: Action; + className?: string; +}; + +const initialState = { + validationErrors: {}, +} as ActionState; + +const EmployeeForm: React.FC = ({ employee, action, className }) => { + // @ts-ignore + const [state, formAction] = useFormState(action, initialState as unknown as void); + + return ( +
+
+ {employee +
+
+
+ + + + + +
+
+ + + +
+
+
+ +
+
+ ); +}; + +export default EmployeeForm; diff --git a/steps/04.02-caching/src/components/ExpensesDetails.tsx b/steps/04.02-caching/src/components/ExpensesDetails.tsx new file mode 100644 index 0000000..f59b51a --- /dev/null +++ b/steps/04.02-caching/src/components/ExpensesDetails.tsx @@ -0,0 +1,56 @@ +import { Expense } from '@/types'; +import Paper from './Paper'; + +type ExpenseDetailsRowProps = { + label: string; + value: string; +}; + +const ExpenseDetailsRow: React.FC = ({ label, value }) => ( +
+ {label} + {value} +
+); + +type ExpenseDetailsProps = { + expense: Expense; +}; + +const ExpenseDetails: React.FC = ({ expense }) => ( + <> +
+
+

Information

+ + + + + +
+
+

Workflow

+ + + + + +
+
+
+
+

Amount

+ + + + + +
+
+ +); + +export default ExpenseDetails; diff --git a/steps/04.02-caching/src/components/ExpensesTable.tsx b/steps/04.02-caching/src/components/ExpensesTable.tsx new file mode 100644 index 0000000..1b2e240 --- /dev/null +++ b/steps/04.02-caching/src/components/ExpensesTable.tsx @@ -0,0 +1,61 @@ +'use client'; + +import { Expense } from '@/types'; +import clsx from 'clsx'; +import { useRouter } from 'next/navigation'; + +type ExpensesTableProps = { + expenses: Array; +}; + +const ExpensesTable: React.FC = ({ expenses }) => { + const router = useRouter(); + + const handleClick = (expenseId: string) => () => { + router.push(`/expenses/${expenseId}`); + }; + + return ( + + + + + + + + + + + {expenses.map((expense, index) => ( + + + + + + + ))} + +
+ Label + + Creation date + + Category + + Price +
{expense.label}{new Date(expense.creationDate).toLocaleDateString()}{expense.category} + {expense.price.priceIncludingTax} {expense.price.currency} +
+ ); +}; + +export default ExpensesTable; diff --git a/steps/04.02-caching/src/components/Icons/ArrowLeft.tsx b/steps/04.02-caching/src/components/Icons/ArrowLeft.tsx new file mode 100644 index 0000000..1cc10c7 --- /dev/null +++ b/steps/04.02-caching/src/components/Icons/ArrowLeft.tsx @@ -0,0 +1,25 @@ +type ArrowLeftProps = { + className?: string; +}; + +const ArrowLeft: React.FC = ({ className }) => ( + +); + +export default ArrowLeft; diff --git a/steps/04.02-caching/src/components/Icons/Eye.tsx b/steps/04.02-caching/src/components/Icons/Eye.tsx new file mode 100644 index 0000000..04beb91 --- /dev/null +++ b/steps/04.02-caching/src/components/Icons/Eye.tsx @@ -0,0 +1,11 @@ +type EyeProps = { + className?: string; +}; + +const Eye: React.FC = ({ className }) => ( + + + +); + +export default Eye; diff --git a/steps/04.02-caching/src/components/Icons/Loader.tsx b/steps/04.02-caching/src/components/Icons/Loader.tsx new file mode 100644 index 0000000..9c81994 --- /dev/null +++ b/steps/04.02-caching/src/components/Icons/Loader.tsx @@ -0,0 +1,25 @@ +type LoaderProps = { + className?: string; +}; + +const Loader: React.FC = ({ className }) => ( + +); + +export default Loader; diff --git a/steps/04.02-caching/src/components/NavigationItem.tsx b/steps/04.02-caching/src/components/NavigationItem.tsx new file mode 100644 index 0000000..9f68e10 --- /dev/null +++ b/steps/04.02-caching/src/components/NavigationItem.tsx @@ -0,0 +1,30 @@ +'use client'; + +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; + +import clsx from 'clsx'; + +type NavigationItemsProps = { + href: string; + children: React.ReactNode; +}; + +const NavigationItem: React.FC = ({ href, children }) => { + const pathname = usePathname(); + + return ( + + {children} + + ); +}; + +export default NavigationItem; diff --git a/steps/04.02-caching/src/components/NavigationMenu.tsx b/steps/04.02-caching/src/components/NavigationMenu.tsx new file mode 100644 index 0000000..4b778c6 --- /dev/null +++ b/steps/04.02-caching/src/components/NavigationMenu.tsx @@ -0,0 +1,21 @@ +import NavigationItem from './NavigationItem'; + +const NavigationMenu = () => { + return ( + + ); +}; + +export default NavigationMenu; diff --git a/steps/04.02-caching/src/components/PageTitle.tsx b/steps/04.02-caching/src/components/PageTitle.tsx new file mode 100644 index 0000000..217fe69 --- /dev/null +++ b/steps/04.02-caching/src/components/PageTitle.tsx @@ -0,0 +1,25 @@ +import Link from 'next/link'; + +import ArrowLeft from './Icons/ArrowLeft'; + +type PageTitleProps = { + children: React.ReactNode; + backHref?: string; +}; + +const PageTitle: React.FC = ({ children, backHref }) => ( +
+ {backHref && ( + + + Go back + + )} +

{children}

+
+); + +export default PageTitle; diff --git a/steps/04.02-caching/src/components/Pagination.tsx b/steps/04.02-caching/src/components/Pagination.tsx new file mode 100644 index 0000000..be9a43a --- /dev/null +++ b/steps/04.02-caching/src/components/Pagination.tsx @@ -0,0 +1,95 @@ +'use client'; + +import clsx from 'clsx'; +import Link from 'next/link'; +import { usePathname, useSearchParams } from 'next/navigation'; + +type PaginationProps = { + totalPages: number; + className?: string; +}; + +type PaginationShortcutProps = { + href: string; + disabled?: boolean; + className?: string; + children: React.ReactNode; +}; + +const PaginationShortcut: React.FC = ({ href, disabled, className, children }) => { + const classNames = clsx( + 'block text-center px-3 py-2 ms-0 border bg-white dark:bg-slate-900', + className, + !disabled && + 'hover:bg-gray-100 hover:text-gray-700 text-gray-500 border-gray-300 dark:text-white dark:border-gray-700 dark:hover:bg-slate-950 dark:hover:text-white', + disabled && 'text-gray-300 border-gray-200 dark:text-gray-500 dark:border-gray-600' + ); + + if (disabled) return
{children}
; + + return ( + + {children} + + ); +}; + +const Pagination: React.FC = ({ totalPages, className }) => { + const params = useSearchParams(); + const pathname = usePathname(); + + const currentPage = Number(params.get('page')) || 1; + + const getPageUrl = (page: number): string => { + const newParams = new URLSearchParams(params); + newParams.set('page', page.toString()); + return `${pathname}?${newParams.toString()}`; + }; + + return ( + + ); +}; + +export default Pagination; diff --git a/steps/04.02-caching/src/components/Paper.tsx b/steps/04.02-caching/src/components/Paper.tsx new file mode 100644 index 0000000..8ba656e --- /dev/null +++ b/steps/04.02-caching/src/components/Paper.tsx @@ -0,0 +1,14 @@ +import clsx from 'clsx'; + +type PaperProps = React.HTMLAttributes & { + children: React.ReactNode; + rounded?: boolean; +}; + +const Paper: React.FC = ({ children, rounded = true, ...restProps }) => ( +
+ {children} +
+); + +export default Paper; diff --git a/steps/04.02-caching/src/components/PersonCard.tsx b/steps/04.02-caching/src/components/PersonCard.tsx new file mode 100644 index 0000000..b38ee53 --- /dev/null +++ b/steps/04.02-caching/src/components/PersonCard.tsx @@ -0,0 +1,43 @@ +import Image from 'next/image'; + +import { Person } from '@/types'; + +import placeholderImage from '@/assets/images/profile-placeholder.jpg'; + +type PersonCardProps = React.HTMLAttributes & { + person: Person; + actions?: React.ReactNode; + compact?: boolean; +}; + +const PersonCard: React.FC = ({ person, actions, className, compact = false }) => { + return ( +
+
+ {`Picture + + {person.firstname} {person.lastname} + + {person.position} +
+ + {!compact && ( +
+ {person.phone} + {person.email} + {person.manager && {person.manager}} +
+ )} + + {actions &&
{actions}
} +
+ ); +}; + +export default PersonCard; diff --git a/steps/04.02-caching/src/components/Search.tsx b/steps/04.02-caching/src/components/Search.tsx new file mode 100644 index 0000000..98fe032 --- /dev/null +++ b/steps/04.02-caching/src/components/Search.tsx @@ -0,0 +1,43 @@ +'use client'; + +import { debounce } from '@/functions/timing'; +import clsx from 'clsx'; +import { usePathname, useRouter, useSearchParams } from 'next/navigation'; + +const Search = ({ ...restProps }) => { + const router = useRouter(); + const params = useSearchParams(); + const pathname = usePathname(); + + const handleChange = debounce((event: React.ChangeEvent) => { + const value = event.target?.value; + const newParams = new URLSearchParams(params); + newParams.delete('page'); + if (value) newParams.set('search', value); + else newParams.delete('search'); + router.replace(`${pathname}?${newParams.toString()}`); + }, 200); + + return ( + <> + + + + ); +}; + +export default Search; diff --git a/steps/04.02-caching/src/components/TextField.tsx b/steps/04.02-caching/src/components/TextField.tsx new file mode 100644 index 0000000..d8061e9 --- /dev/null +++ b/steps/04.02-caching/src/components/TextField.tsx @@ -0,0 +1,31 @@ +import clsx from 'clsx'; + +type TextFieldProps = React.InputHTMLAttributes & { + label: string; + id: string; + type?: string; + className?: string; + errorMessages?: Array; +}; + +const TextField: React.FC = ({ label, id, type = 'text', className, errorMessages, ...restProps }) => { + return ( +
+ + + {errorMessages?.length &&

{errorMessages[0]}

} +
+ ); +}; + +export default TextField; diff --git a/steps/04.02-caching/src/functions/timing.ts b/steps/04.02-caching/src/functions/timing.ts new file mode 100644 index 0000000..3b8c6f3 --- /dev/null +++ b/steps/04.02-caching/src/functions/timing.ts @@ -0,0 +1,7 @@ +export const debounce = (fn: Function, ms = 300) => { + let timeoutId: ReturnType; + return function (this: any, ...args: any[]) { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => fn.apply(this, args), ms); + }; +}; diff --git a/steps/04.02-caching/src/styles/global.css b/steps/04.02-caching/src/styles/global.css new file mode 100644 index 0000000..f77ed90 --- /dev/null +++ b/steps/04.02-caching/src/styles/global.css @@ -0,0 +1,41 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --color-bg-global: #e9effc; + --color-bg-primary: #ffffff; + --color-bg-secondary: #f5f5f5; + --color-text-primary: #000000; + + --spacing-sm: 0.5rem; + --spacing-md: 0.75rem; + --spacing-lg: 1rem; + --spacing-xl: 1.5rem; +} + +/* Headings */ + +.heading1 { + font-size: 2rem; + font-weight: bold; + margin-bottom: var(--spacing-lg); +} + +.heading2 { + font-size: 1.5rem; + font-weight: bold; + margin-bottom: var(--spacing-lg); +} + +.heading3 { + font-size: 1.125rem; + font-weight: bold; + margin-bottom: var(--spacing-lg); +} + +.heading4 { + font-size: 1rem; + font-weight: bold; + margin-bottom: var(--spacing-lg); +} diff --git a/steps/04.02-caching/src/types.ts b/steps/04.02-caching/src/types.ts new file mode 100644 index 0000000..82cffb5 --- /dev/null +++ b/steps/04.02-caching/src/types.ts @@ -0,0 +1,39 @@ +export type Person = { + id: string; + photo?: string; + firstname: string; + lastname: string; + position: string; + entryDate: string; + birthDate: string; + gender: string; + email: string; + phone: string; + isManager: boolean; + manager?: string; + managerId?: string; +}; + +export type Expense = { + id: string; + employeeId: string; + price: { + priceIncludingTax: number; + taxAmount: number; + priceExcludingTax: number; + currency: string; + }; + label: string; + description: string; + category: string; + receiptLink: string; + status: 'approved' | 'created' | 'declined'; + creationDate: string; + updateDate: string; +}; + +export type PaginationAttributes = { + per_page?: number; + page: number; + total_pages: number; +}; diff --git a/steps/04.02-caching/tailwind.config.js b/steps/04.02-caching/tailwind.config.js new file mode 100644 index 0000000..eaa361c --- /dev/null +++ b/steps/04.02-caching/tailwind.config.js @@ -0,0 +1,9 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ['./src/**/*.{js,ts,jsx,tsx,mdx}'], + darkMode: 'selector', + theme: { + extend: {}, + }, + plugins: [], +}; diff --git a/steps/04.02-caching/tsconfig.json b/steps/04.02-caching/tsconfig.json new file mode 100644 index 0000000..7b28589 --- /dev/null +++ b/steps/04.02-caching/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/steps/05.01-server-actions-solution/.gitkeep b/steps/05.01-server-actions-solution/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/steps/05.01-server-actions/.gitkeep b/steps/05.01-server-actions/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/steps/05.02-form-hooks-solution/.gitkeep b/steps/05.02-form-hooks-solution/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/steps/05.02-form-hooks/.gitkeep b/steps/05.02-form-hooks/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/steps/06.01-error-boundaries-solution/.gitkeep b/steps/06.01-error-boundaries-solution/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/steps/06.01-error-boundaries/.gitkeep b/steps/06.01-error-boundaries/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/steps/06.02-expected-errors-solution/.gitkeep b/steps/06.02-expected-errors-solution/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/steps/06.02-expected-errors/.gitkeep b/steps/06.02-expected-errors/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/steps/07.01-lifecycles-solution/.gitkeep b/steps/07.01-lifecycles-solution/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/steps/07.01-lifecycles/.gitkeep b/steps/07.01-lifecycles/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/steps/07.02-middleware-solution/.gitkeep b/steps/07.02-middleware-solution/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/steps/07.02-middleware/.gitkeep b/steps/07.02-middleware/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/steps/08.01-rendering-methods-solution/.gitkeep b/steps/08.01-rendering-methods-solution/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/steps/08.01-rendering-methods/.gitkeep b/steps/08.01-rendering-methods/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/steps/08.02-suspense-solution/.gitkeep b/steps/08.02-suspense-solution/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/steps/08.02-suspense/.gitkeep b/steps/08.02-suspense/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/steps/api/controllers/employees.js b/steps/api/controllers/employees.js index e8e32ba..a84fc39 100644 --- a/steps/api/controllers/employees.js +++ b/steps/api/controllers/employees.js @@ -37,7 +37,7 @@ export const verifyExists = (request, reply, done) => { export const findAll = (request, reply) => { const page = Number(request.query?.page) || 1; - const per_page = Number(request.query?.per_page) || 1; + const per_page = Number(request.query?.per_page) || 100; const search = request.query?.search; const sortBy = request.query?.sort_by; const order = request.query?.order?.toLowerCase() === 'desc' ? 'desc' : 'asc'; diff --git a/steps/api/controllers/expenses.js b/steps/api/controllers/expenses.js index df61577..0699740 100644 --- a/steps/api/controllers/expenses.js +++ b/steps/api/controllers/expenses.js @@ -13,7 +13,7 @@ export const verifyExists = (request, reply, done) => { export const findAll = async (request, reply) => { const page = Number(request.query?.page) || 1; - const per_page = Number(request.query?.per_page) || 1; + const per_page = Number(request.query?.per_page) || 100; const employee = request.query?.employee; const sortBy = request.query?.sort_by; const order = request.query?.order?.toLowerCase() === 'desc' ? 'desc' : 'asc';