├── .claude ├── tasks │ └── .gitkeep ├── agents │ └── code-reviewer.md ├── commands │ ├── create-PR.md │ └── push-changes.md └── settings.json ├── src ├── lib │ ├── date │ │ ├── testOutput.ts │ │ ├── useFormatDateTime.test.ts │ │ ├── useRelativeTime.ts │ │ ├── useFormatDate.ts │ │ ├── useFormatDateTime.ts │ │ ├── useFormatDate.test.ts │ │ └── Date.ts │ ├── http │ │ ├── IHttpServiceOptions.ts │ │ ├── Options.ts │ │ ├── index.ts │ │ ├── exceptions │ │ │ ├── InternalServerException.ts │ │ │ └── ResourceNotFoundException.ts │ │ ├── IHttpService.ts │ │ ├── IHttpServiceClient.ts │ │ ├── AjaxError.ts │ │ └── HttpService.ts │ ├── api │ │ ├── auth │ │ │ ├── login │ │ │ │ ├── login-dto.ts │ │ │ │ └── login-command.ts │ │ │ └── users │ │ │ │ └── {user-id} │ │ │ │ ├── user-query.ts │ │ │ │ └── user-dto.ts │ │ ├── carts │ │ │ ├── {cart-id} │ │ │ │ ├── cart-product-dto.ts │ │ │ │ ├── cart-dto.ts │ │ │ │ ├── purchase-command.ts │ │ │ │ ├── cart-query.ts │ │ │ │ ├── clear-cart-command.ts │ │ │ │ ├── add-to-cart-command.ts │ │ │ │ └── cart-products-query.ts │ │ │ └── cart-query-keys.ts │ │ └── products │ │ │ ├── products-list │ │ │ ├── products-list-dto.ts │ │ │ └── products-list-query.ts │ │ │ ├── products-query-keys.ts │ │ │ └── {product-id} │ │ │ ├── product-dto.ts │ │ │ └── product-query.ts │ ├── isEmpty.ts │ ├── components │ │ ├── Layout │ │ │ ├── Navbar │ │ │ │ ├── INavItem.ts │ │ │ │ ├── LoaderBar.tsx │ │ │ │ ├── Navbar.stories.tsx │ │ │ │ └── useNavItems.ts │ │ │ ├── Footer │ │ │ │ ├── Footer.stories.tsx │ │ │ │ └── Logo.tsx │ │ │ ├── Layout.tsx │ │ │ ├── ToggleModeButton.tsx │ │ │ ├── Page.tsx │ │ │ ├── Page.stories.tsx │ │ │ ├── PageHeader.tsx │ │ │ └── PageHeader.stories.tsx │ │ ├── Result │ │ │ ├── Icons │ │ │ │ ├── ErrorIcon.tsx │ │ │ │ ├── InfoIcon.tsx │ │ │ │ ├── SuccessIcon.tsx │ │ │ │ └── WarningIcon.tsx │ │ │ ├── EmptyStateResult.stories.tsx │ │ │ ├── NotFoundResult.stories.tsx │ │ │ ├── InternalErrorResult.stories.tsx │ │ │ ├── Buttons │ │ │ │ ├── ContactUsButton.tsx │ │ │ │ └── ResetFiltersButton.tsx │ │ │ ├── InternalServerErrorResult.stories.tsx │ │ │ ├── NotFoundResult.tsx │ │ │ ├── InternalErrorResult.tsx │ │ │ ├── EmptyStateResult.tsx │ │ │ ├── InternalServerErrorResult.tsx │ │ │ ├── Result.tsx │ │ │ └── ErrorPageStrategy.tsx │ │ ├── Toast │ │ │ ├── useToast.ts │ │ │ └── useNotImplementedYetToast.ts │ │ ├── Suspense │ │ │ └── with-suspense.tsx │ │ ├── Form │ │ │ ├── Select.stories.tsx │ │ │ ├── TextInput.stories.tsx │ │ │ ├── Select.tsx │ │ │ └── TextInput.tsx │ │ ├── Modal │ │ │ └── createModalStore.ts │ │ └── ErrorBoundary │ │ │ ├── with-error-boundary.tsx │ │ │ └── ErrorBoundary.tsx │ ├── theme │ │ ├── useBrandColor.ts │ │ ├── useSecondaryTextColor.ts │ │ └── theme.ts │ ├── types │ │ └── one-of-union.ts │ ├── is-guid.ts │ ├── assert-value.ts │ ├── router │ │ ├── useRouteError.ts │ │ ├── routes.ts │ │ ├── handleLazyImportError.ts │ │ ├── routePath.ts │ │ └── index.ts │ ├── get.ts │ ├── sleep.ts │ ├── to-kebab-case.ts │ ├── buildUrl.ts │ ├── machine │ │ ├── provided-action.ts │ │ ├── replace.ts │ │ ├── union-context-selector.ts │ │ └── use-actor-ref.ts │ ├── i18n │ │ ├── useTransations.ts │ │ └── i18n.ts │ ├── permissions │ │ ├── use-has-all-permissions.ts │ │ └── use-has-permission.ts │ ├── query.ts │ ├── logger │ │ ├── MockLogger.ts │ │ ├── StorybookLogger.ts │ │ ├── ConsoleLogger.ts │ │ ├── ILogger.ts │ │ └── index.ts │ ├── get.test.ts │ ├── to-kebab-case.test.ts │ ├── assert-value.test.tsx │ ├── is-guid.test.ts │ ├── format │ │ ├── Number.ts │ │ └── Money.ts │ ├── compose.ts │ └── debounce.ts ├── types │ ├── NonEmptyArray.ts │ ├── Autocomplete.ts │ ├── DeepPartial.ts │ ├── IQueryParams.ts │ ├── IMeta.ts │ ├── NonNullableProps.ts │ ├── Branded.ts │ ├── OneOfUnion.ts │ ├── AllOrNothing.ts │ ├── OneOfUnion.test.ts │ └── AllOrNothing.test.ts ├── test-lib │ ├── generateUuid.ts │ ├── storybook │ │ ├── sleep.ts │ │ ├── withReactQuery.tsx │ │ ├── with-suspense-decorator.tsx │ │ ├── withoutAuth.tsx │ │ ├── withI18Next.tsx │ │ └── withAuth.tsx │ ├── fixtures │ │ ├── MetaFixture.ts │ │ ├── CartFixture.ts │ │ ├── UserFixture.ts │ │ ├── ProductFixture.ts │ │ └── createFixture.ts │ ├── handlers │ │ ├── resolvers.ts │ │ ├── getAddToCartHandler.ts │ │ ├── getClearCartHandler.ts │ │ ├── signInHandler.ts │ │ ├── getCartHandler.ts │ │ ├── getUserHandler.ts │ │ ├── getProductHandler.ts │ │ └── getProductsHandler.ts │ ├── initI18n.ts │ └── date │ │ └── generateDate.ts ├── features │ ├── carts │ │ ├── types │ │ │ ├── ICart.ts │ │ │ └── ICartProduct.ts │ │ ├── infrastructure │ │ │ ├── usePurchase.ts │ │ │ ├── useCartProductsQuery.ts │ │ │ ├── useClearCart.ts │ │ │ └── useAddToCart.ts │ │ └── presentation │ │ │ ├── CheckoutButton │ │ │ ├── usePurchaseDialogStore.ts │ │ │ ├── CheckoutButton.tsx │ │ │ ├── CheckoutButton.stories.tsx │ │ │ └── CheckoutDialog.tsx │ │ │ ├── AddToCartButton │ │ │ ├── useProductAddedDialogStore.ts │ │ │ ├── useAddToCartNotifications.ts │ │ │ └── AddToCartButton.tsx │ │ │ ├── ClearCartButton │ │ │ ├── useConfirmClearCartDialogStore.ts │ │ │ ├── useClearCartNotifications.ts │ │ │ ├── ClearCartButton.tsx │ │ │ └── ClearCartButton.stories.tsx │ │ │ ├── useCheckoutNotifications.ts │ │ │ ├── CartItem.stories.tsx │ │ │ ├── CartsList.stories.tsx │ │ │ ├── CheckoutForm.stories.tsx │ │ │ ├── CartsList.tsx │ │ │ └── CheckoutForm.tsx │ ├── auth │ │ ├── infrastructure │ │ │ ├── getUser.ts │ │ │ └── loginUser.ts │ │ ├── types │ │ │ └── IUser.ts │ │ ├── application │ │ │ ├── AuthProvider.tsx │ │ │ ├── RequirePub.tsx │ │ │ ├── RequireAuth.tsx │ │ │ ├── withRequirePub.tsx │ │ │ └── withRequireAuth.tsx │ │ └── presentation │ │ │ ├── useSignInNotifications.ts │ │ │ └── SignInForm.stories.tsx │ ├── products │ │ ├── types │ │ │ ├── Category.ts │ │ │ ├── IRating.ts │ │ │ └── IProduct.ts │ │ ├── infrastructure │ │ │ ├── productsQuery.ts │ │ │ └── productQuery.ts │ │ └── presentation │ │ │ ├── StarRating.stories.tsx │ │ │ ├── useCategoryLabel.ts │ │ │ ├── ProductNotFoundResult.stories.tsx │ │ │ ├── ProductDetails.stories.tsx │ │ │ ├── StarRating.tsx │ │ │ ├── ProductCard.stories.tsx │ │ │ ├── ProductNotFoundResult.tsx │ │ │ ├── ProductsList.stories.tsx │ │ │ ├── ProductsList.tsx │ │ │ └── ProductCard.tsx │ ├── authv2 │ │ ├── types │ │ │ └── UserRoles.ts │ │ ├── infrastructure │ │ │ └── getRoles.ts │ │ └── application │ │ │ ├── use-authorized-context-selector.ts │ │ │ ├── auth-context.tsx │ │ │ └── storage-machine.ts │ └── demo │ │ ├── presentation │ │ ├── Demo.stories.tsx │ │ └── Demo.tsx │ │ └── application │ │ ├── useCounter.test.ts │ │ └── useCounter.ts ├── example.test.ts ├── vite-env.d.ts ├── pages │ ├── Home │ │ ├── loader.ts │ │ ├── Home.stories.tsx │ │ └── index.tsx │ ├── Products │ │ ├── loader.ts │ │ ├── Products.stories.tsx │ │ └── index.tsx │ ├── Product │ │ ├── loader.ts │ │ ├── Product.stories.tsx │ │ └── index.tsx │ ├── Cart │ │ ├── loader.ts │ │ ├── Cart.stories.tsx │ │ └── index.tsx │ ├── SignIn │ │ ├── SignIn.stories.tsx │ │ └── index.tsx │ └── router.tsx ├── main.tsx ├── app │ ├── Providers.tsx │ └── App.tsx └── typings │ └── react-router-config.d.ts ├── .prettierignore ├── .storybook ├── preview-head.html ├── vitest.setup.ts ├── main.ts └── preview.ts ├── vitest.shims.d.ts ├── test-globals.ts ├── .husky └── pre-commit ├── .prettierrc ├── .env ├── tsconfig.node.json ├── test-setup.ts ├── index.html ├── .gitignore ├── .vscode ├── extensions.json └── custom.code-snippets ├── .github ├── prompts │ ├── create-pr.prompt.md │ └── push-changes.prompt.md ├── instructions │ ├── vitest-tests.instructions.md │ └── code-generation.instructions.md └── workflows │ └── run-tests.yml ├── LICENSE ├── tsconfig.json ├── public └── vite.svg ├── vite.config.ts └── .devcontainer ├── Dockerfile └── devcontainer.json /.claude/tasks/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/lib/date/testOutput.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | **/public/mockServiceWorker.js 3 | dist/ -------------------------------------------------------------------------------- /src/types/NonEmptyArray.ts: -------------------------------------------------------------------------------- 1 | export type NonEmptyArray = [T, ...T[]]; 2 | -------------------------------------------------------------------------------- /src/lib/http/IHttpServiceOptions.ts: -------------------------------------------------------------------------------- 1 | export interface IHttpServiceOptions {} 2 | -------------------------------------------------------------------------------- /.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/test-lib/generateUuid.ts: -------------------------------------------------------------------------------- 1 | export const generateUuid = () => crypto.randomUUID(); 2 | -------------------------------------------------------------------------------- /vitest.shims.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/types/Autocomplete.ts: -------------------------------------------------------------------------------- 1 | export type Autocomplete = T & (string & {}); 2 | -------------------------------------------------------------------------------- /test-globals.ts: -------------------------------------------------------------------------------- 1 | export const setup = () => { 2 | process.env.TZ = "Europe/London"; 3 | }; 4 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx pretty-quick --staged 5 | -------------------------------------------------------------------------------- /src/types/DeepPartial.ts: -------------------------------------------------------------------------------- 1 | export type DeepPartial = { 2 | [P in keyof T]?: DeepPartial; 3 | }; 4 | -------------------------------------------------------------------------------- /src/features/carts/types/ICart.ts: -------------------------------------------------------------------------------- 1 | export type { CartDto as ICart } from "@/lib/api/carts/{cart-id}/cart-dto"; 2 | -------------------------------------------------------------------------------- /src/types/IQueryParams.ts: -------------------------------------------------------------------------------- 1 | export interface IQueryParams { 2 | limit: number; 3 | sort?: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/features/auth/infrastructure/getUser.ts: -------------------------------------------------------------------------------- 1 | export { getUser } from "@/lib/api/auth/users/{user-id}/user-query"; 2 | -------------------------------------------------------------------------------- /src/features/products/types/Category.ts: -------------------------------------------------------------------------------- 1 | export { Category } from "@/lib/api/products/{product-id}/product-dto"; 2 | -------------------------------------------------------------------------------- /src/features/products/types/IRating.ts: -------------------------------------------------------------------------------- 1 | export type { IRating } from "@/lib/api/products/{product-id}/product-dto"; 2 | -------------------------------------------------------------------------------- /src/types/IMeta.ts: -------------------------------------------------------------------------------- 1 | export interface IMeta { 2 | total: number; 3 | limit: number; 4 | sort?: string; 5 | } 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": false 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/api/auth/login/login-dto.ts: -------------------------------------------------------------------------------- 1 | export interface LoginDto { 2 | username: string; 3 | password: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/types/NonNullableProps.ts: -------------------------------------------------------------------------------- 1 | export type NonNullableProps = { 2 | [P in keyof T]-?: NonNullable; 3 | }; 4 | -------------------------------------------------------------------------------- /src/test-lib/storybook/sleep.ts: -------------------------------------------------------------------------------- 1 | export const sleep = (ms: number) => 2 | new Promise((resolve) => setTimeout(resolve, ms)); 3 | -------------------------------------------------------------------------------- /src/features/products/types/IProduct.ts: -------------------------------------------------------------------------------- 1 | export type { ProductDto as IProduct } from "@/lib/api/products/{product-id}/product-dto"; 2 | -------------------------------------------------------------------------------- /src/features/carts/types/ICartProduct.ts: -------------------------------------------------------------------------------- 1 | export type { CartProductDto as ICartProduct } from "@/lib/api/carts/{cart-id}/cart-product-dto"; 2 | -------------------------------------------------------------------------------- /src/lib/isEmpty.ts: -------------------------------------------------------------------------------- 1 | import { either, isNil, isEmpty as _isEmpty } from "ramda"; 2 | 3 | export const isEmpty = either(isNil, _isEmpty); 4 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | VITE_HTTP_LOGGER_DEBUG_MODE=false 2 | VITE_FAKE_STORE_API_HOST=https://fakestoreapi.com 3 | STORYBOOK_DISABLE_TELEMETRY=1 4 | VERSION=v0.0.0 -------------------------------------------------------------------------------- /src/features/carts/infrastructure/usePurchase.ts: -------------------------------------------------------------------------------- 1 | export { usePurchaseMutation as usePurchase } from "@/lib/api/carts/{cart-id}/purchase-command"; 2 | -------------------------------------------------------------------------------- /src/example.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | 3 | test("should work as expected", () => { 4 | expect(Math.sqrt(4)).toBe(2); 5 | }); 6 | -------------------------------------------------------------------------------- /src/types/Branded.ts: -------------------------------------------------------------------------------- 1 | declare const __brand: unique symbol; 2 | 3 | interface Brand { 4 | [__brand]: B; 5 | } 6 | export type Branded = T & Brand; 7 | -------------------------------------------------------------------------------- /src/types/OneOfUnion.ts: -------------------------------------------------------------------------------- 1 | export type OneOfUnion< 2 | TUnion extends { type: string }, 3 | TType extends TUnion["type"], 4 | > = Extract; 5 | -------------------------------------------------------------------------------- /src/lib/components/Layout/Navbar/INavItem.ts: -------------------------------------------------------------------------------- 1 | export interface INavItem { 2 | label: string; 3 | subLabel?: string; 4 | children?: INavItem[]; 5 | href?: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/features/products/infrastructure/productsQuery.ts: -------------------------------------------------------------------------------- 1 | export { 2 | useProductsQuery, 3 | productsLoader, 4 | } from "@/lib/api/products/products-list/products-list-query"; 5 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface ImportMetaEnv { 4 | readonly VITE_FAKE_STORE_API_HOST: string; 5 | readonly VERSION: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/features/auth/types/IUser.ts: -------------------------------------------------------------------------------- 1 | export type { IUser } from "@/lib/api/auth/users/{user-id}/user-query"; 2 | export type { IAddress } from "@/lib/api/auth/users/{user-id}/user-dto"; 3 | -------------------------------------------------------------------------------- /src/features/carts/infrastructure/useCartProductsQuery.ts: -------------------------------------------------------------------------------- 1 | export { 2 | useCartProductsQuery, 3 | cartProductsLoader, 4 | } from "@/lib/api/carts/{cart-id}/cart-products-query"; 5 | -------------------------------------------------------------------------------- /src/features/products/infrastructure/productQuery.ts: -------------------------------------------------------------------------------- 1 | export { 2 | getProductQuery, 3 | useProductQuery, 4 | productLoader, 5 | } from "@/lib/api/products/{product-id}/product-query"; 6 | -------------------------------------------------------------------------------- /src/features/auth/infrastructure/loginUser.ts: -------------------------------------------------------------------------------- 1 | export { loginUser } from "@/lib/api/auth/login/login-command"; 2 | export type { LoginDto as ICredentials } from "@/lib/api/auth/login/login-dto"; 3 | -------------------------------------------------------------------------------- /src/pages/Home/loader.ts: -------------------------------------------------------------------------------- 1 | import { productsLoader } from "@/features/products/infrastructure/productsQuery"; 2 | 3 | export const homePageLoader = () => { 4 | return productsLoader(); 5 | }; 6 | -------------------------------------------------------------------------------- /src/lib/theme/useBrandColor.ts: -------------------------------------------------------------------------------- 1 | import { useColorModeValue } from "@chakra-ui/react"; 2 | 3 | export const useBrandColor = () => { 4 | return useColorModeValue("orange.400", "orange.300"); 5 | }; 6 | -------------------------------------------------------------------------------- /src/lib/types/one-of-union.ts: -------------------------------------------------------------------------------- 1 | // AIDEV-NOTE: Utility type to extract a specific variant from a discriminated union by its type field 2 | export type OneOfUnion = T extends { type: K } ? T : never; 3 | -------------------------------------------------------------------------------- /src/pages/Products/loader.ts: -------------------------------------------------------------------------------- 1 | import { productsLoader } from "@/features/products/infrastructure/productsQuery"; 2 | 3 | export const productsPageLoader = () => { 4 | return productsLoader(); 5 | }; 6 | -------------------------------------------------------------------------------- /src/lib/theme/useSecondaryTextColor.ts: -------------------------------------------------------------------------------- 1 | import { useColorModeValue } from "@chakra-ui/react"; 2 | 3 | export const useSecondaryTextColor = () => { 4 | return useColorModeValue("gray.500", "gray.300"); 5 | }; 6 | -------------------------------------------------------------------------------- /src/lib/api/carts/{cart-id}/cart-product-dto.ts: -------------------------------------------------------------------------------- 1 | import type { ProductDto } from "../../products/{product-id}/product-dto"; 2 | 3 | export interface CartProductDto extends ProductDto { 4 | quantity: number; 5 | } 6 | -------------------------------------------------------------------------------- /src/features/carts/presentation/CheckoutButton/usePurchaseDialogStore.ts: -------------------------------------------------------------------------------- 1 | import { createModalStore } from "@/lib/components/Modal/createModalStore"; 2 | 3 | export const usePurchaseDialogStore = createModalStore(); 4 | -------------------------------------------------------------------------------- /src/lib/is-guid.ts: -------------------------------------------------------------------------------- 1 | export const isGuid = (value?: string) => { 2 | const regex = 3 | /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; 4 | return !!value && regex.test(value); 5 | }; 6 | -------------------------------------------------------------------------------- /src/features/carts/presentation/AddToCartButton/useProductAddedDialogStore.ts: -------------------------------------------------------------------------------- 1 | import { createModalStore } from "@/lib/components/Modal/createModalStore"; 2 | 3 | export const useProductAddedDialogStore = createModalStore(); 4 | -------------------------------------------------------------------------------- /src/features/carts/presentation/ClearCartButton/useConfirmClearCartDialogStore.ts: -------------------------------------------------------------------------------- 1 | import { createModalStore } from "@/lib/components/Modal/createModalStore"; 2 | 3 | export const useConfirmClearCartDialogStore = createModalStore(); 4 | -------------------------------------------------------------------------------- /src/lib/http/Options.ts: -------------------------------------------------------------------------------- 1 | export interface FetchOptions { 2 | host?: string; 3 | cache?: RequestCache; 4 | headers?: HeadersInit; 5 | responseType?: "json" | "text" | "arrayBuffer" | "blob" | "formData"; 6 | signal?: AbortSignal; 7 | } 8 | -------------------------------------------------------------------------------- /src/lib/api/auth/login/login-command.ts: -------------------------------------------------------------------------------- 1 | import { httpService } from "@/lib/http"; 2 | 3 | import type { LoginDto } from "./login-dto"; 4 | 5 | export const loginUser = (body: LoginDto) => { 6 | return httpService.post("auth/login", body); 7 | }; 8 | -------------------------------------------------------------------------------- /src/lib/api/carts/{cart-id}/cart-dto.ts: -------------------------------------------------------------------------------- 1 | interface IProduct { 2 | productId: number; 3 | quantity: number; 4 | } 5 | 6 | export interface CartDto { 7 | id: number; 8 | userId: number; 9 | date: string; 10 | products: IProduct[]; 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts", "vitest.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /src/test-lib/fixtures/MetaFixture.ts: -------------------------------------------------------------------------------- 1 | import type { IMeta } from "@/types/IMeta"; 2 | 3 | import { createFixture } from "./createFixture"; 4 | 5 | export const MetaFixture = createFixture({ 6 | limit: 10, 7 | total: 20, 8 | sort: "asc", 9 | }); 10 | -------------------------------------------------------------------------------- /src/lib/api/products/products-list/products-list-dto.ts: -------------------------------------------------------------------------------- 1 | import type { IMeta } from "@/types/IMeta"; 2 | 3 | import type { ProductDto } from "../{product-id}/product-dto"; 4 | 5 | export interface ProductsListDto { 6 | products: ProductDto[]; 7 | meta: IMeta; 8 | } 9 | -------------------------------------------------------------------------------- /src/lib/assert-value.ts: -------------------------------------------------------------------------------- 1 | export function assertValue( 2 | value: T, 3 | error?: Error 4 | ): asserts value is NonNullable { 5 | if (value === null || value === undefined) { 6 | throw error ?? new Error("Value must not be null or undefined"); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/types/AllOrNothing.ts: -------------------------------------------------------------------------------- 1 | export type AllOrNothing = T | ToUndefinedObject; 2 | 3 | type ToUndefinedObject = Partial< 4 | Record 5 | >; 6 | 7 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 8 | type AnyObject = Record; 9 | -------------------------------------------------------------------------------- /src/lib/router/useRouteError.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-restricted-imports 2 | import { useRouteError as useReactRouterError } from "react-router"; 3 | 4 | import type { AjaxError } from "@/lib/http/AjaxError"; 5 | 6 | export const useRouteError = () => { 7 | return useReactRouterError() as AjaxError; 8 | }; 9 | -------------------------------------------------------------------------------- /src/test-lib/handlers/resolvers.ts: -------------------------------------------------------------------------------- 1 | import { http } from "msw"; 2 | 3 | export type GetResolver = Parameters[1]; 4 | export type DeleteResolver = Parameters[1]; 5 | export type PutResolver = Parameters[1]; 6 | export type PostResolver = Parameters[1]; 7 | -------------------------------------------------------------------------------- /src/pages/Product/loader.ts: -------------------------------------------------------------------------------- 1 | import { productLoader } from "@/features/products/infrastructure/productQuery"; 2 | import type { LoaderFunctionArgs } from "@/lib/router"; 3 | 4 | export const productPageLoader = ({ params }: LoaderFunctionArgs) => { 5 | return productLoader((params as { productId: string }).productId); 6 | }; 7 | -------------------------------------------------------------------------------- /src/pages/Cart/loader.ts: -------------------------------------------------------------------------------- 1 | import { cartProductsLoader } from "@/features/carts/infrastructure/useCartProductsQuery"; 2 | import type { LoaderFunctionArgs } from "@/lib/router"; 3 | 4 | export const cartPageLoader = ({ params }: LoaderFunctionArgs) => { 5 | return cartProductsLoader((params as { cartId: string }).cartId); 6 | }; 7 | -------------------------------------------------------------------------------- /src/lib/get.ts: -------------------------------------------------------------------------------- 1 | import { lensPath, view, split } from "ramda"; 2 | 3 | export const get = ( 4 | obj: TObject, 5 | path: TPath 6 | ): TResult => { 7 | const pathArray = split(/[[\].]/, path); 8 | const lens = lensPath(pathArray); 9 | return view(lens, obj) as TResult; 10 | }; 11 | -------------------------------------------------------------------------------- /src/lib/sleep.ts: -------------------------------------------------------------------------------- 1 | export async function sleep(ms: number): Promise; 2 | export async function sleep(ms: number, returnValue: T): Promise; 3 | export async function sleep(ms: number, returnValue?: T): Promise { 4 | return new Promise((resolve) => { 5 | setTimeout(() => resolve(returnValue), ms); 6 | }); 7 | } 8 | -------------------------------------------------------------------------------- /src/lib/to-kebab-case.ts: -------------------------------------------------------------------------------- 1 | import { compose, join, split, replace, toLower } from "ramda"; 2 | 3 | export const toKebabCase = compose( 4 | join("-"), 5 | split(/(?<=[a-z])(?=[A-Z])|\W+|\s+/), // split on camelCase boundaries, non-alphanumeric characters and spaces 6 | replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, "$1 $2"), 7 | toLower 8 | ); 9 | -------------------------------------------------------------------------------- /src/features/authv2/types/UserRoles.ts: -------------------------------------------------------------------------------- 1 | export enum Permission { 2 | Read = "read", 3 | Write = "write", 4 | Edit = "edit", 5 | } 6 | 7 | export enum Role { 8 | Reader = "reader", 9 | Editor = "editor", 10 | Manager = "manager", 11 | } 12 | 13 | export interface UserRoles { 14 | role: Role; 15 | permissions: Permission[]; 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/components/Result/Icons/ErrorIcon.tsx: -------------------------------------------------------------------------------- 1 | import { WarningIcon } from "@chakra-ui/icons"; 2 | import { useColorModeValue } from "@chakra-ui/react"; 3 | 4 | const ErrorIcon = () => { 5 | const color = useColorModeValue("red.500", "red.300"); 6 | 7 | return ; 8 | }; 9 | 10 | export { ErrorIcon }; 11 | -------------------------------------------------------------------------------- /test-setup.ts: -------------------------------------------------------------------------------- 1 | import "@testing-library/jest-dom/vitest"; 2 | import { cleanup } from "@testing-library/react"; 3 | import { afterEach, beforeAll } from "vitest"; 4 | 5 | import { initializeI18n } from "@/test-lib/initI18n"; 6 | 7 | beforeAll(async () => { 8 | await initializeI18n(); 9 | }); 10 | 11 | afterEach(() => { 12 | cleanup(); 13 | }); 14 | -------------------------------------------------------------------------------- /src/lib/components/Result/Icons/InfoIcon.tsx: -------------------------------------------------------------------------------- 1 | import { InfoIcon as ChInfoIcon } from "@chakra-ui/icons"; 2 | import { useColorModeValue } from "@chakra-ui/react"; 3 | 4 | const InfoIcon = () => { 5 | const color = useColorModeValue("blue.500", "blue.300"); 6 | 7 | return ; 8 | }; 9 | 10 | export { InfoIcon }; 11 | -------------------------------------------------------------------------------- /src/lib/components/Result/Icons/SuccessIcon.tsx: -------------------------------------------------------------------------------- 1 | import { CheckCircleIcon } from "@chakra-ui/icons"; 2 | import { useColorModeValue } from "@chakra-ui/react"; 3 | 4 | const SuccessIcon = () => { 5 | const color = useColorModeValue("green.500", "green.300"); 6 | 7 | return ; 8 | }; 9 | 10 | export { SuccessIcon }; 11 | -------------------------------------------------------------------------------- /src/lib/components/Result/Icons/WarningIcon.tsx: -------------------------------------------------------------------------------- 1 | import { WarningTwoIcon } from "@chakra-ui/icons"; 2 | import { useColorModeValue } from "@chakra-ui/react"; 3 | 4 | const WarningIcon = () => { 5 | const color = useColorModeValue("orange.400", "orange.300"); 6 | 7 | return ; 8 | }; 9 | 10 | export { WarningIcon }; 11 | -------------------------------------------------------------------------------- /src/lib/http/index.ts: -------------------------------------------------------------------------------- 1 | import { HttpService } from "./HttpService"; 2 | import { KyClient } from "./KyClient"; 3 | 4 | const headers = { 5 | "Content-Type": "application/json", 6 | }; 7 | 8 | export const host = import.meta.env.VITE_FAKE_STORE_API_HOST; 9 | 10 | export const httpService = new HttpService( 11 | new KyClient({ prefixUrl: host, headers }) 12 | ); 13 | -------------------------------------------------------------------------------- /src/test-lib/fixtures/CartFixture.ts: -------------------------------------------------------------------------------- 1 | import type { ICart } from "@/features/carts/types/ICart"; 2 | import { DateVO } from "@/lib/date/Date"; 3 | 4 | import { createFixture } from "./createFixture"; 5 | 6 | export const CartFixture = createFixture({ 7 | id: 1, 8 | date: DateVO.past(), 9 | userId: 1, 10 | products: [{ productId: 1, quantity: 2 }], 11 | }); 12 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | 4 | import { App } from "@/app/App"; 5 | import { Providers } from "@/app/Providers"; 6 | import "@/lib/i18n/i18n"; 7 | 8 | ReactDOM.createRoot(document.getElementById("root")!).render( 9 | 10 | 11 | 12 | 13 | 14 | ); 15 | -------------------------------------------------------------------------------- /.storybook/vitest.setup.ts: -------------------------------------------------------------------------------- 1 | import { setProjectAnnotations } from "@storybook/react-vite"; 2 | 3 | import * as projectAnnotations from "./preview"; 4 | 5 | // This is an important step to apply the right configuration when testing your stories. 6 | // More info at: https://storybook.js.org/docs/api/portable-stories/portable-stories-vitest#setprojectannotations 7 | setProjectAnnotations([projectAnnotations]); 8 | -------------------------------------------------------------------------------- /src/lib/api/carts/cart-query-keys.ts: -------------------------------------------------------------------------------- 1 | export const cartQueryKeys = { 2 | all: ["carts"] as const, 3 | lists: () => [...cartQueryKeys.all, "list"] as const, 4 | details: () => [...cartQueryKeys.all, "detail"] as const, 5 | detail: (cartId: string) => [...cartQueryKeys.details(), cartId] as const, 6 | products: (cartId: string) => 7 | [...cartQueryKeys.detail(cartId), "products"] as const, 8 | }; 9 | -------------------------------------------------------------------------------- /src/lib/components/Layout/Navbar/LoaderBar.tsx: -------------------------------------------------------------------------------- 1 | import { Progress } from "@chakra-ui/react"; 2 | 3 | import { useNavigation } from "@/lib/router"; 4 | 5 | const LoaderBar = () => { 6 | const { state } = useNavigation(); 7 | 8 | if (state === "loading") { 9 | return ; 10 | } 11 | 12 | return null; 13 | }; 14 | 15 | export { LoaderBar }; 16 | -------------------------------------------------------------------------------- /src/test-lib/handlers/getAddToCartHandler.ts: -------------------------------------------------------------------------------- 1 | import { http, HttpResponse } from "msw"; 2 | 3 | import { host } from "@/lib/http"; 4 | 5 | import type { PutResolver } from "./resolvers"; 6 | 7 | export const getAddToCartHandler = (resolver?: PutResolver) => 8 | http.put(`${host}/carts/:cartId`, (req) => { 9 | if (resolver) return resolver(req); 10 | 11 | return HttpResponse.json({}); 12 | }); 13 | -------------------------------------------------------------------------------- /src/lib/components/Layout/Footer/Footer.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react-vite"; 2 | 3 | import { Footer } from "./index"; 4 | 5 | const meta = { 6 | component: Footer, 7 | parameters: { 8 | layout: "fullscreen", 9 | }, 10 | } satisfies Meta; 11 | 12 | export default meta; 13 | type Story = StoryObj; 14 | 15 | export const Default: Story = {}; 16 | -------------------------------------------------------------------------------- /src/test-lib/handlers/getClearCartHandler.ts: -------------------------------------------------------------------------------- 1 | import { http, HttpResponse } from "msw"; 2 | 3 | import { host } from "@/lib/http"; 4 | 5 | import type { DeleteResolver } from "./resolvers"; 6 | 7 | export const getClearCartHandler = (resolver?: DeleteResolver) => 8 | http.delete(`${host}/carts/:cartId`, (req) => { 9 | if (resolver) return resolver(req); 10 | 11 | return HttpResponse.json({}); 12 | }); 13 | -------------------------------------------------------------------------------- /src/test-lib/handlers/signInHandler.ts: -------------------------------------------------------------------------------- 1 | import { http, HttpResponse } from "msw"; 2 | 3 | import { host } from "@/lib/http"; 4 | 5 | import type { PostResolver } from "./resolvers"; 6 | 7 | export const getSignInHandler = (resolver?: PostResolver) => 8 | http.post(`${host}/auth/login`, (req) => { 9 | if (resolver) return resolver(req); 10 | 11 | return HttpResponse.json({ token: "authtoken" }); 12 | }); 13 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/features/authv2/infrastructure/getRoles.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Permission, 3 | Role, 4 | type UserRoles, 5 | } from "@/features/authv2/types/UserRoles"; 6 | import { sleep } from "@/lib/sleep"; 7 | 8 | export async function getRoles(): Promise { 9 | await sleep(500); 10 | 11 | return { 12 | role: Role.Manager, 13 | permissions: [Permission.Read, Permission.Write, Permission.Edit], 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /src/test-lib/storybook/withReactQuery.tsx: -------------------------------------------------------------------------------- 1 | import type { Decorator } from "@storybook/react-vite"; 2 | import { QueryClientProvider } from "@tanstack/react-query"; 3 | 4 | import { queryClient } from "@/lib/query"; 5 | 6 | export const withReactQuery: Decorator = (story) => { 7 | queryClient.clear(); 8 | 9 | return ( 10 | {story()} 11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /src/features/demo/presentation/Demo.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react-vite"; 2 | 3 | import { Demo } from "./Demo"; 4 | 5 | const meta = { 6 | title: "modules/Demo", 7 | component: Demo, 8 | parameters: { 9 | layout: "centered", 10 | }, 11 | } satisfies Meta; 12 | 13 | export default meta; 14 | type Story = StoryObj; 15 | 16 | export const Default: Story = {}; 17 | -------------------------------------------------------------------------------- /src/test-lib/storybook/with-suspense-decorator.tsx: -------------------------------------------------------------------------------- 1 | import type { Decorator } from "@storybook/react-vite"; 2 | import { Suspense, type SuspenseProps } from "react"; 3 | 4 | export function withSuspenseDecorator(props?: SuspenseProps) { 5 | const Decorator: Decorator = (Story) => ( 6 | {"loading ..."}} {...props}> 7 | 8 | 9 | ); 10 | 11 | return Decorator; 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | storybook-static 15 | coverage 16 | storybook-report.json 17 | 18 | # Editor directories and files 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | 27 | .pnpm-store 28 | tsconfig.tsbuildinfo 29 | reports -------------------------------------------------------------------------------- /src/lib/components/Toast/useToast.ts: -------------------------------------------------------------------------------- 1 | import { useToast as chUseToast, type UseToastOptions } from "@chakra-ui/react"; 2 | 3 | const defaultOptions: UseToastOptions = { 4 | duration: 5000, 5 | isClosable: true, 6 | }; 7 | 8 | export const useToast = () => { 9 | const toast = chUseToast(); 10 | 11 | return (options: UseToastOptions) => { 12 | toast({ 13 | ...defaultOptions, 14 | ...options, 15 | }); 16 | }; 17 | }; 18 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "editorconfig.editorconfig", 5 | "esbenp.prettier-vscode", 6 | "lokalise.i18n-ally", 7 | "streetsidesoftware.code-spell-checker", 8 | "ms-azuretools.vscode-containers", 9 | "ms-vscode-remote.vscode-remote-extensionpack", 10 | "ms-vscode-remote.remote-ssh-edit", 11 | "ms-vscode.remote-explorer", 12 | "vitest.explorer" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /src/lib/buildUrl.ts: -------------------------------------------------------------------------------- 1 | import queryString from "query-string"; 2 | 3 | import { isEmpty } from "@/lib/isEmpty"; 4 | import type { IQueryParams } from "@/types/IQueryParams"; 5 | 6 | export const buildUrl = ( 7 | path: string, 8 | params?: Params 9 | ) => { 10 | if (isEmpty(params)) { 11 | return path; 12 | } 13 | 14 | return `${path}?${queryString.stringify(params ?? {}, { 15 | arrayFormat: "comma", 16 | })}`; 17 | }; 18 | -------------------------------------------------------------------------------- /src/lib/components/Result/EmptyStateResult.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react-vite"; 2 | 3 | import { EmptyStateResult } from "./EmptyStateResult"; 4 | 5 | const meta = { 6 | component: EmptyStateResult, 7 | parameters: { 8 | layout: "centered", 9 | }, 10 | } satisfies Meta; 11 | 12 | export default meta; 13 | type Story = StoryObj; 14 | 15 | export const Default: Story = {}; 16 | -------------------------------------------------------------------------------- /src/lib/http/exceptions/InternalServerException.ts: -------------------------------------------------------------------------------- 1 | import { HTTPError } from "ky"; 2 | 3 | import { AjaxError } from "../AjaxError"; 4 | 5 | export class InternalServerException extends AjaxError { 6 | constructor( 7 | response: Response, 8 | request: Request, 9 | options: HTTPError["options"] 10 | ) { 11 | super(500, response, request, options, `Unknown internal server error`); 12 | this.name = "InternalServerException"; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/features/demo/application/useCounter.test.ts: -------------------------------------------------------------------------------- 1 | import { act, renderHook } from "@testing-library/react"; 2 | import { expect, it } from "vitest"; 3 | 4 | import { useCounter } from "./useCounter"; 5 | 6 | it("should increment the count", () => { 7 | const { result } = renderHook(() => useCounter()); 8 | 9 | act(() => { 10 | result.current.increment(); 11 | result.current.increment(); 12 | }); 13 | 14 | expect(result.current.count).toBe(2); 15 | }); 16 | -------------------------------------------------------------------------------- /src/features/demo/application/useCounter.ts: -------------------------------------------------------------------------------- 1 | import { useState, useCallback } from "react"; 2 | 3 | import { Logger } from "@/lib/logger"; 4 | 5 | const useCounter = () => { 6 | const [count, setCount] = useState(0); 7 | 8 | const increment = useCallback(() => { 9 | Logger.info("Debugging useCounter hook", { value: count }); 10 | setCount((x) => x + 1); 11 | }, [count]); 12 | 13 | return { count, increment }; 14 | }; 15 | 16 | export { useCounter }; 17 | -------------------------------------------------------------------------------- /src/lib/components/Toast/useNotImplementedYetToast.ts: -------------------------------------------------------------------------------- 1 | import { useTranslations } from "@/lib/i18n/useTransations"; 2 | 3 | import { useToast } from "./useToast"; 4 | 5 | export const useNotImplementedYetToast = () => { 6 | const toast = useToast(); 7 | const t = useTranslations("shared.toast.not-implemented"); 8 | 9 | return () => 10 | toast({ 11 | status: "info", 12 | title: t("title"), 13 | description: t("description"), 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /src/test-lib/storybook/withoutAuth.tsx: -------------------------------------------------------------------------------- 1 | import type { Decorator } from "@storybook/react-vite"; 2 | 3 | import { 4 | initializeAuthStore, 5 | Provider, 6 | } from "@/features/auth/application/authStore"; 7 | 8 | export const withoutAuth: Decorator = (story) => { 9 | const store = initializeAuthStore({ 10 | isAuthenticated: false, 11 | isError: false, 12 | state: "finished", 13 | }); 14 | 15 | return {story()}; 16 | }; 17 | -------------------------------------------------------------------------------- /src/lib/api/auth/users/{user-id}/user-query.ts: -------------------------------------------------------------------------------- 1 | import { omit } from "ramda"; 2 | 3 | import { httpService } from "@/lib/http"; 4 | 5 | import type { UserDto } from "./user-dto"; 6 | 7 | export type IUser = Omit; 8 | 9 | export const getUser = () => { 10 | // mocking current user and its cartId by passing id=1 11 | return httpService 12 | .get("users/1") 13 | .then((res) => ({ ...(omit(["password"], res) as IUser), cartId: 1 })); 14 | }; 15 | -------------------------------------------------------------------------------- /src/lib/components/Suspense/with-suspense.tsx: -------------------------------------------------------------------------------- 1 | import { type ComponentType, Suspense, type SuspenseProps } from "react"; 2 | 3 | export const withSuspense = 4 | (fallback?: SuspenseProps["fallback"]) => 5 | (Component: ComponentType) => { 6 | // eslint-disable-next-line react/display-name 7 | return (props: TProps) => ( 8 | 9 | 10 | 11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /src/test-lib/handlers/getCartHandler.ts: -------------------------------------------------------------------------------- 1 | import { http, HttpResponse } from "msw"; 2 | 3 | import { host } from "@/lib/http"; 4 | import { CartFixture } from "@/test-lib/fixtures/CartFixture"; 5 | 6 | import type { GetResolver } from "./resolvers"; 7 | 8 | export const getCartHandler = (resolver?: GetResolver) => 9 | http.get(`${host}/carts/:cartId`, (req) => { 10 | if (resolver) return resolver(req); 11 | 12 | return HttpResponse.json(CartFixture.toStructure()); 13 | }); 14 | -------------------------------------------------------------------------------- /src/test-lib/handlers/getUserHandler.ts: -------------------------------------------------------------------------------- 1 | import { http, HttpResponse } from "msw"; 2 | 3 | import { host } from "@/lib/http"; 4 | import { UserFixture } from "@/test-lib/fixtures/UserFixture"; 5 | 6 | import type { GetResolver } from "./resolvers"; 7 | 8 | export const getUserHandler = (resolver?: GetResolver) => 9 | http.get(`${host}/users/:userId`, (req) => { 10 | if (resolver) return resolver(req); 11 | 12 | return HttpResponse.json(UserFixture.toStructure()); 13 | }); 14 | -------------------------------------------------------------------------------- /src/features/auth/application/AuthProvider.tsx: -------------------------------------------------------------------------------- 1 | import { type PropsWithChildren, useRef } from "react"; 2 | 3 | import { type AuthStore, initializeAuthStore, Provider } from "./authStore"; 4 | 5 | const AuthProvider = ({ children, ...props }: PropsWithChildren) => { 6 | const storeRef = useRef(null); 7 | 8 | storeRef.current ??= initializeAuthStore(props); 9 | 10 | return {children}; 11 | }; 12 | 13 | export { AuthProvider }; 14 | -------------------------------------------------------------------------------- /src/lib/components/Layout/Layout.tsx: -------------------------------------------------------------------------------- 1 | import { chakra } from "@chakra-ui/react"; 2 | 3 | import { Outlet } from "@/lib/router"; 4 | 5 | import { Footer } from "./Footer"; 6 | import { Navbar } from "./Navbar"; 7 | 8 | export const Layout = () => { 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 |