├── .eslintrc.cjs ├── .gitignore ├── .husky └── pre-commit ├── .storybook ├── main.ts └── preview.ts ├── README.md ├── env-examples ├── index.html ├── package-lock.json ├── package.json ├── postcss.config.js ├── public └── vite.svg ├── src ├── App.tsx ├── assets │ └── react.svg ├── components │ ├── button │ │ ├── button.test.tsx │ │ └── button.tsx │ ├── card │ │ └── card.tsx │ └── input │ │ ├── input-error-message.tsx │ │ ├── input.test.tsx │ │ └── input.tsx ├── index.css ├── layouts │ ├── header │ │ └── header.tsx │ ├── layout.css │ └── layout.tsx ├── lib │ └── axios.ts ├── main.tsx ├── pages │ ├── customer │ │ ├── page.tsx │ │ ├── query-slice.ts │ │ └── query.ts │ ├── sign-in │ │ ├── api │ │ │ ├── query-slice.ts │ │ │ └── query.ts │ │ ├── form.tsx │ │ ├── page.tsx │ │ └── schema.ts │ └── sign-up │ │ ├── api │ │ ├── query-slice.ts │ │ └── query.ts │ │ ├── form.tsx │ │ ├── page.tsx │ │ └── schema.ts ├── routes │ ├── error-page.tsx │ ├── route-guard.tsx │ └── router.tsx ├── services │ ├── logout │ │ ├── api.ts │ │ └── schema.ts │ └── refresh-token │ │ ├── api.ts │ │ └── schema.ts ├── store │ └── user-store.ts ├── stories │ ├── Button.stories.ts │ ├── Configure.mdx │ └── assets │ │ ├── accessibility.png │ │ ├── accessibility.svg │ │ ├── addon-library.png │ │ ├── assets.png │ │ ├── avif-test-image.avif │ │ ├── context.png │ │ ├── discord.svg │ │ ├── docs.png │ │ ├── figma-plugin.png │ │ ├── github.svg │ │ ├── share.png │ │ ├── styling.png │ │ ├── testing.png │ │ ├── theming.png │ │ ├── tutorials.svg │ │ └── youtube.svg ├── utils │ ├── api.ts │ ├── endpoints-constant.ts │ ├── role-enum.ts │ ├── routes-constants.ts │ └── tailwind-merge.ts └── vite-env.d.ts ├── tailwind.config.js ├── tests └── setup.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | "eslint:recommended", 6 | "eslint:recommended", 7 | "plugin:@typescript-eslint/recommended", 8 | "plugin:react-hooks/recommended", 9 | "plugin:storybook/recommended", 10 | ], 11 | ignorePatterns: ["dist", ".eslintrc.cjs"], 12 | parser: "@typescript-eslint/parser", 13 | plugins: ["react-refresh"], 14 | rules: { 15 | "react-refresh/only-export-components": [ 16 | "warn", 17 | { allowConstantExport: true }, 18 | ], 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | *storybook.log 27 | 28 | # Env 29 | .env -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npm run lint 2 | -------------------------------------------------------------------------------- /.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from "@storybook/react-vite"; 2 | 3 | const config: StorybookConfig = { 4 | stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"], 5 | addons: [ 6 | "@storybook/addon-onboarding", 7 | "@storybook/addon-links", 8 | "@storybook/addon-essentials", 9 | "@chromatic-com/storybook", 10 | "@storybook/addon-interactions", 11 | ], 12 | framework: { 13 | name: "@storybook/react-vite", 14 | options: {}, 15 | }, 16 | docs: { 17 | autodocs: "tag", 18 | }, 19 | }; 20 | export default config; 21 | -------------------------------------------------------------------------------- /.storybook/preview.ts: -------------------------------------------------------------------------------- 1 | import type { Preview } from "@storybook/react"; 2 | import "../src/index.css"; 3 | 4 | const preview: Preview = { 5 | parameters: { 6 | controls: { 7 | matchers: { 8 | color: /(background|color)$/i, 9 | date: /Date$/i, 10 | }, 11 | }, 12 | }, 13 | }; 14 | 15 | export default preview; 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Refresh and Access Token Implementation 2 | 3 | A minimal demo application showcasing the implementation of access tokens and refresh tokens, built with modern frontend technologies for a fast and efficient development experience. 4 | 5 | ## Tools Used 6 | 7 | - [Vite](https://vitejs.dev/): Next-generation frontend tooling with blazing fast HMR. 8 | - [Vitest](https://vitest.dev/): A lightweight test runner for modern JavaScript projects. 9 | - [Storybook](https://storybook.js.org/): An open-source tool for building UI components and documenting them. 10 | - [Tailwind-CSS](https://tailwindcss.com/): A utility-first CSS framework for rapidly building custom designs.with minimal setup to get working in 11 | - [Axios](https://axios-http.com/): A promise-based HTTP client for the browser and Node.js. 12 | - [Zustand](https://zustand-demo.pmnd.rs/): A small, fast, and scalable bearbones state-management solution. 13 | - [React Query](https://tanstack.com/query/latest): Powerful asynchronous state management for React. 14 | - [Zod](https://zod.dev/): TypeScript-first schema declaration and validation library. 15 | 16 | ## Development 17 | 18 | ```sh 19 | npm run dev 20 | ``` 21 | 22 | ## Test 23 | 24 | ```sh 25 | npm run test 26 | ``` 27 | 28 | ## Storybook 29 | 30 | ```sh 31 | npm run storybook 32 | ``` 33 | -------------------------------------------------------------------------------- /env-examples: -------------------------------------------------------------------------------- 1 | VITE_BASE_URL = "YOUR_BASE_URL" 2 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "refresh-accesstoken-token", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview", 11 | "test": "vitest", 12 | "storybook": "storybook dev -p 6006", 13 | "build-storybook": "storybook build", 14 | "prepare": "husky" 15 | }, 16 | "dependencies": { 17 | "@headlessui/react": "^1.7.18", 18 | "@hookform/error-message": "^2.0.1", 19 | "@hookform/resolvers": "^3.3.4", 20 | "@tanstack/react-query": "^5.28.9", 21 | "@tanstack/react-query-devtools": "^5.28.10", 22 | "axios": "^1.6.8", 23 | "class-variance-authority": "^0.7.0", 24 | "clsx": "^2.1.0", 25 | "jwt-decode": "^4.0.0", 26 | "react": "^18.2.0", 27 | "react-dom": "^18.2.0", 28 | "react-hook-form": "^7.51.1", 29 | "react-icons": "^5.0.1", 30 | "react-router-dom": "^6.22.3", 31 | "sonner": "^1.4.41", 32 | "tailwind-merge": "^2.2.2", 33 | "zod": "^3.22.4", 34 | "zustand": "^4.5.2" 35 | }, 36 | "devDependencies": { 37 | "@chromatic-com/storybook": "^1.2.25", 38 | "@storybook/addon-essentials": "^8.0.4", 39 | "@storybook/addon-interactions": "^8.0.4", 40 | "@storybook/addon-links": "^8.0.4", 41 | "@storybook/addon-onboarding": "^8.0.4", 42 | "@storybook/blocks": "^8.0.4", 43 | "@storybook/react": "^8.0.4", 44 | "@storybook/react-vite": "^8.0.4", 45 | "@storybook/test": "^8.0.4", 46 | "@testing-library/jest-dom": "^6.4.2", 47 | "@testing-library/react": "^14.2.2", 48 | "@testing-library/user-event": "^14.5.2", 49 | "@types/react": "^18.2.66", 50 | "@types/react-dom": "^18.2.22", 51 | "@typescript-eslint/eslint-plugin": "^7.2.0", 52 | "@typescript-eslint/parser": "^7.2.0", 53 | "@vitejs/plugin-react-swc": "^3.5.0", 54 | "autoprefixer": "^10.4.19", 55 | "eslint": "^8.57.0", 56 | "eslint-plugin-react": "^7.34.1", 57 | "eslint-plugin-react-hooks": "^4.6.0", 58 | "eslint-plugin-react-refresh": "^0.4.6", 59 | "eslint-plugin-storybook": "^0.8.0", 60 | "husky": "^9.0.11", 61 | "jsdom": "^24.0.0", 62 | "postcss": "^8.4.38", 63 | "storybook": "^8.0.4", 64 | "tailwindcss": "^3.4.1", 65 | "typescript": "^5.2.2", 66 | "vite": "^5.2.0", 67 | "vitest": "^1.4.0" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | "postcss-import": {}, 4 | "tailwindcss/nesting": {}, 5 | tailwindcss: {}, 6 | autoprefixer: {}, 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Layout } from "@/layouts/layout"; 2 | 3 | function App() { 4 | return ; 5 | } 6 | 7 | export default App; 8 | -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/button/button.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { describe, it, expect, vi } from "vitest"; 3 | import { fireEvent, render, screen } from "@testing-library/react"; 4 | import { Button } from "@/components/button/button"; 5 | 6 | describe("Button Component Test", () => { 7 | it("renders button label", () => { 8 | render(); 9 | 10 | expect(screen.getByTestId("button").textContent).toBe("Primary"); 11 | 12 | screen.debug(); 13 | }); 14 | 15 | it("call onClick handle when clicked", () => { 16 | const handleClick = vi.fn(); 17 | 18 | render( 19 | 22 | ); 23 | 24 | const buttonElement = screen.getByTestId("button"); 25 | 26 | fireEvent.click(buttonElement); 27 | 28 | expect(handleClick).toHaveBeenCalledTimes(1); 29 | 30 | screen.debug(); 31 | }); 32 | 33 | it("forward ref to button element", () => { 34 | const ref = React.createRef(); 35 | 36 | render(); 37 | 38 | expect(ref.current).toBeTruthy(); 39 | 40 | screen.debug(); 41 | }); 42 | 43 | it("renders default props", () => { 44 | render(); 45 | 46 | expect( 47 | screen.getByTestId("button").classList.contains("bg-blue-500") 48 | ).toBeTruthy(); 49 | 50 | expect( 51 | screen.getByTestId("button").classList.contains("px-3") 52 | ).toBeTruthy(); 53 | 54 | screen.debug(); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /src/components/button/button.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { cva, VariantProps } from "class-variance-authority"; 3 | import { cn } from "@/utils/tailwind-merge"; 4 | 5 | const buttonVariants = cva( 6 | "w-full flex space-x-4 gap-x-4 items-center justify-center rounded-md text-white text-center duration-300", 7 | { 8 | variants: { 9 | variant: { 10 | primary: "bg-blue-500 hover:bg-blue-500 hover:shadow-lg", 11 | secondary: "bg-blue-100 text-blue-500 hover:bg-blue-200", 12 | destructive: "bg-red-500 hover:bg-red-500 hover:shadow-lg", 13 | transparent: 14 | "bg-transparent text-gray-700 hover:bg-blue-500 hover:text-white duration-300", 15 | }, 16 | size: { 17 | default: "px-3 py-2", 18 | sm: "px-2.5 py-1.5", 19 | lg: "px-6 py-3.5", 20 | icon: "p-2.5", 21 | }, 22 | }, 23 | defaultVariants: { 24 | variant: "primary", 25 | size: "default", 26 | }, 27 | } 28 | ); 29 | 30 | interface ButtonProps 31 | extends React.ButtonHTMLAttributes, 32 | VariantProps { 33 | onClick?: () => void; 34 | } 35 | 36 | const Button = React.forwardRef( 37 | ({ className, size, variant, ...props }, ref) => { 38 | return ( 39 | 55 | 56 | 57 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | ); 73 | }; 74 | -------------------------------------------------------------------------------- /src/layouts/layout.css: -------------------------------------------------------------------------------- 1 | .header { 2 | @apply text-gray-700; 3 | .nav { 4 | @apply bg-white drop-shadow-sm px-8 py-1.5 flex justify-between items-center; 5 | &__menu-image { 6 | @apply w-[2.3rem] rounded-full border border-blue-200; 7 | } 8 | &__menu-icon { 9 | @apply absolute -right-1 bottom-2 bg-blue-200 rounded-full text-blue-500; 10 | } 11 | &__menu-items { 12 | @apply p-2 absolute right-0 mt-2 w-56 origin-top-right divide-y divide-gray-100 rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/layouts/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Outlet } from "react-router-dom"; 2 | import { Header } from "@/layouts/header/header"; 3 | import "@/layouts/layout.css"; 4 | 5 | export function Layout() { 6 | return ( 7 |
8 |
9 |
10 | 11 |
12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/lib/axios.ts: -------------------------------------------------------------------------------- 1 | import { getRefreshToken } from "@/services/refresh-token/api"; 2 | import { useUserStore } from "@/store/user-store"; 3 | import axios, { 4 | AxiosError, 5 | CreateAxiosDefaults, 6 | InternalAxiosRequestConfig, 7 | } from "axios"; 8 | 9 | interface CustomAxiosRequestConfig extends InternalAxiosRequestConfig { 10 | _retry?: boolean; 11 | } 12 | 13 | const baseConfig: CreateAxiosDefaults = { 14 | baseURL: `${import.meta.env.VITE_BASE_URL}`, 15 | withCredentials: true, 16 | }; 17 | 18 | export const instanceWithoutInterceptors = axios.create(baseConfig); 19 | 20 | export const instance = axios.create(baseConfig); 21 | 22 | instance.interceptors.request.use( 23 | function (config) { 24 | const accessToken = useUserStore.getState().user?.accessToken; 25 | 26 | if (accessToken) { 27 | config.headers.Authorization = `Bearer ${accessToken}`; 28 | } 29 | 30 | return config; 31 | }, 32 | function (error) { 33 | return Promise.reject(error); 34 | } 35 | ); 36 | 37 | instance.interceptors.response.use( 38 | function (response) { 39 | return response; 40 | }, 41 | async function (error: AxiosError) { 42 | const originalRequest: CustomAxiosRequestConfig | undefined = error.config; 43 | 44 | if ( 45 | error.response?.status === 401 && 46 | originalRequest && 47 | !originalRequest._retry 48 | ) { 49 | originalRequest._retry = true; 50 | try { 51 | const response = await getRefreshToken(); 52 | 53 | const { payload } = response; 54 | 55 | useUserStore.setState({ user: { accessToken: payload.accessToken } }); 56 | 57 | originalRequest.headers.Authorization = `Bearer ${payload.accessToken}`; 58 | 59 | return instance(originalRequest); 60 | } catch (error) { 61 | if (error instanceof AxiosError && error.response?.status === 403) { 62 | useUserStore.getState().removeCredentials(); 63 | return; 64 | } 65 | } 66 | } 67 | 68 | return Promise.reject(error); 69 | } 70 | ); 71 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import { RouterProvider } from "react-router-dom"; 4 | import "@/index.css"; 5 | import { router } from "@/routes/router"; 6 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 7 | import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; 8 | import { Toaster } from "sonner"; 9 | 10 | const queryClient = new QueryClient(); 11 | 12 | ReactDOM.createRoot(document.getElementById("root")!).render( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | -------------------------------------------------------------------------------- /src/pages/customer/page.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import { useGetUsers } from "@/pages/customer/query"; 3 | import { Card, LoadingCardGrid } from "@/components/card/card"; 4 | 5 | export const CustomerPage: FC = () => { 6 | const { data, isError, isLoading } = useGetUsers(); 7 | 8 | if (isLoading) { 9 | return ; 10 | } 11 | 12 | if (isError) { 13 | return

Error Occured

; 14 | } 15 | 16 | const users = data?.payload.users; 17 | 18 | return ( 19 |
20 | {users?.map((user) => )} 21 |
22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /src/pages/customer/query-slice.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { api } from "@/utils/api"; 3 | import { API_ENDPOINT } from "@/utils/endpoints-constant"; 4 | 5 | enum Role { 6 | ADMIN = "ADMIN", 7 | USER = "USER", 8 | } 9 | 10 | const SingleUserAPISchema = z.object({ 11 | id: z.string(), 12 | name: z.string(), 13 | imageUrl: z.string(), 14 | phoneNumber: z.union([z.string(), z.null()]), 15 | email: z.string().email(), 16 | role: z.nativeEnum(Role), 17 | }); 18 | 19 | const UserAPISchema = z.object({ 20 | status: z.boolean(), 21 | message: z.string(), 22 | payload: z.object({ 23 | users: z.array(SingleUserAPISchema), 24 | }), 25 | }); 26 | 27 | const GetAllUserRequest = z.void(); 28 | 29 | const GetAllUserResponse = UserAPISchema; 30 | 31 | const getAllUser = api< 32 | z.infer, 33 | z.infer 34 | >({ 35 | path: API_ENDPOINT.USERS, 36 | method: "GET", 37 | requestSchema: GetAllUserRequest, 38 | responseSchema: GetAllUserResponse, 39 | }); 40 | 41 | export const UserAPI = { 42 | getAllUser, 43 | }; 44 | -------------------------------------------------------------------------------- /src/pages/customer/query.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from "@tanstack/react-query"; 2 | import { UserAPI } from "@/pages/customer/query-slice"; 3 | 4 | export function useGetUsers() { 5 | return useQuery({ 6 | queryKey: ["customer"], 7 | queryFn: () => { 8 | return UserAPI.getAllUser(); 9 | }, 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /src/pages/sign-in/api/query-slice.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { api } from "@/utils/api"; 3 | import { API_ENDPOINT } from "@/utils/endpoints-constant"; 4 | import { 5 | SignInAPIResponseSchema, 6 | SignInFormSchema, 7 | } from "@/pages/sign-in/schema"; 8 | 9 | const SignInRequest = SignInFormSchema; 10 | 11 | const SignInResponse = SignInAPIResponseSchema; 12 | 13 | const signIn = api< 14 | z.infer, 15 | z.infer 16 | >({ 17 | method: "POST", 18 | path: API_ENDPOINT.SIGN_IN, 19 | requestSchema: SignInRequest, 20 | responseSchema: SignInResponse, 21 | type: "public" 22 | }); 23 | 24 | export const SignInAPI = { 25 | signIn, 26 | }; 27 | -------------------------------------------------------------------------------- /src/pages/sign-in/api/query.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { AxiosError } from "axios"; 3 | import { useUserStore } from "@/store/user-store"; 4 | import { useMutation } from "@tanstack/react-query"; 5 | import { SignInAPI } from "@/pages/sign-in/api/query-slice"; 6 | import { 7 | SignInAPIResponseSchema, 8 | SignInFormType, 9 | } from "@/pages/sign-in/schema"; 10 | import { toast } from "sonner"; 11 | 12 | interface ErrorResponse { 13 | message: string; 14 | } 15 | 16 | export function useSignIn() { 17 | const { setCredentials } = useUserStore(); 18 | return useMutation< 19 | z.infer, 20 | AxiosError, 21 | SignInFormType 22 | >({ 23 | mutationFn: (user) => SignInAPI.signIn(user), 24 | onSuccess: (data) => { 25 | const { payload, message } = data; 26 | 27 | setCredentials({ 28 | accessToken: payload.accessToken, 29 | }); 30 | 31 | toast.success(message); 32 | }, 33 | onError: (error) => { 34 | const errorMessage = error.response?.data.message; 35 | 36 | toast.error(errorMessage); 37 | }, 38 | }); 39 | } 40 | -------------------------------------------------------------------------------- /src/pages/sign-in/form.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import { zodResolver } from "@hookform/resolvers/zod"; 3 | import { FormProvider, SubmitHandler, useForm } from "react-hook-form"; 4 | import { Input } from "@/components/input/input"; 5 | import { Button } from "@/components/button/button"; 6 | import { useSignIn } from "@/pages/sign-in/api/query"; 7 | import { SignInFormSchema, SignInFormType } from "@/pages/sign-in/schema"; 8 | 9 | export const SignInForm: FC = () => { 10 | const methods = useForm({ 11 | resolver: zodResolver(SignInFormSchema), 12 | }); 13 | 14 | const { 15 | handleSubmit, 16 | formState: { errors }, 17 | } = methods; 18 | 19 | const signIn = useSignIn(); 20 | 21 | const onSubmit: SubmitHandler = (data) => { 22 | signIn.mutate(data); 23 | }; 24 | 25 | return ( 26 | 27 |
28 | 35 | 42 | 43 | 44 |
45 |
46 | ); 47 | }; 48 | -------------------------------------------------------------------------------- /src/pages/sign-in/page.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import { Link } from "react-router-dom"; 3 | import { SignInForm } from "@/pages/sign-in/form"; 4 | import { Routes } from "@/utils/routes-constants"; 5 | 6 | export const SignInPage: FC = () => { 7 | return ( 8 |
9 |
10 |

Sign In

11 | 12 |
13 |

14 | Don't you have an account?{" "} 15 | 19 | Sign Up 20 | 21 |

22 |
23 |
24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /src/pages/sign-in/schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const SignInFormSchema = z.object({ 4 | email: z 5 | .string() 6 | .min(1, "Email is required") 7 | .trim() 8 | .email({ message: "Invalid Email" }) 9 | .toLowerCase(), 10 | password: z 11 | .string() 12 | .trim() 13 | .min(6, { message: "Password must be at least 6 characters long" }), 14 | }); 15 | 16 | export type SignInFormType = z.infer; 17 | 18 | export type SignInFieldName = keyof SignInFormType; 19 | 20 | export const SignInAPIResponseSchema = z.object({ 21 | status: z.boolean(), 22 | message: z.string(), 23 | payload: z.object({ 24 | accessToken: z.string(), 25 | }), 26 | }); 27 | -------------------------------------------------------------------------------- /src/pages/sign-up/api/query-slice.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { api } from "@/utils/api"; 3 | import { BaseSignUpFormSchema, SignUpAPIResponseSchema } from "../schema"; 4 | import { API_ENDPOINT } from "@/utils/endpoints-constant"; 5 | 6 | const SignUpRequest = BaseSignUpFormSchema; 7 | 8 | const SignUpResponse = SignUpAPIResponseSchema; 9 | 10 | const signUp = api< 11 | z.infer, 12 | z.infer 13 | >({ 14 | method: "POST", 15 | path: API_ENDPOINT.SIGN_UP, 16 | requestSchema: SignUpRequest, 17 | responseSchema: SignUpResponse, 18 | type:"public" 19 | }); 20 | 21 | export const SignUpAPI = { 22 | signUp, 23 | }; 24 | -------------------------------------------------------------------------------- /src/pages/sign-up/api/query.ts: -------------------------------------------------------------------------------- 1 | import { useMutation } from "@tanstack/react-query"; 2 | import { SignUpAPI } from "./query-slice"; 3 | import { z } from "zod"; 4 | import { SignUpAPIResponseSchema, SignUpFormType } from "../schema"; 5 | import { AxiosError } from "axios"; 6 | import { toast } from "sonner"; 7 | import { useNavigate } from "react-router-dom"; 8 | import { Routes } from "@/utils/routes-constants"; 9 | 10 | interface ErrorResponse { 11 | message: string; 12 | } 13 | 14 | export function useSignUp() { 15 | const navigate = useNavigate(); 16 | return useMutation< 17 | z.infer, 18 | AxiosError, 19 | Omit 20 | >({ 21 | mutationFn: (user) => SignUpAPI.signUp(user), 22 | onSuccess: (data) => { 23 | toast.success(data.message); 24 | 25 | navigate(Routes.SIGNIN); 26 | }, 27 | onError: (error) => { 28 | const errorMessage = error.response?.data.message; 29 | 30 | toast.error(errorMessage); 31 | }, 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /src/pages/sign-up/form.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import { zodResolver } from "@hookform/resolvers/zod"; 3 | import { FormProvider, SubmitHandler, useForm } from "react-hook-form"; 4 | import { SignUpFormSchema, SignUpFormType } from "./schema"; 5 | import { Input } from "@/components/input/input"; 6 | import { Button } from "@/components/button/button"; 7 | import { useSignUp } from "./api/query"; 8 | 9 | export const SignUpForm: FC = () => { 10 | const methods = useForm({ 11 | resolver: zodResolver(SignUpFormSchema), 12 | }); 13 | 14 | const { 15 | handleSubmit, 16 | formState: { errors }, 17 | } = methods; 18 | 19 | const signUp = useSignUp(); 20 | 21 | const onSubmit: SubmitHandler = ({ 22 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 23 | confirmPassword, 24 | ...rest 25 | }) => { 26 | signUp.mutate(rest); 27 | }; 28 | 29 | return ( 30 | 31 |
32 | 39 | 46 | 53 | 60 | 61 |
62 |
63 | ); 64 | }; 65 | -------------------------------------------------------------------------------- /src/pages/sign-up/page.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import { Link } from "react-router-dom"; 3 | import { SignUpForm } from "@/pages/sign-up/form"; 4 | import { Routes } from "@/utils/routes-constants"; 5 | 6 | export const SignUpPage: FC = () => { 7 | return ( 8 |
9 |
10 |

Sign Up

11 | 12 |
13 |

14 | Already have an account?{" "} 15 | 19 | Sign In 20 | 21 |

22 |
23 |
24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /src/pages/sign-up/schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { Role } from "@/utils/role-enum"; 3 | 4 | export const BaseSignUpFormSchema = z.object({ 5 | name: z.string().trim().min(1, { message: "Full Name is required" }), 6 | email: z 7 | .string() 8 | .min(1, "Email is required") 9 | .trim() 10 | .email({ message: "Invalid Email" }) 11 | .toLowerCase(), 12 | password: z.string().trim().min(1, { message: "Password is required" }), 13 | }); 14 | 15 | export const SignUpFormSchema = BaseSignUpFormSchema.extend({ 16 | confirmPassword: z 17 | .string() 18 | .trim() 19 | .min(1, { message: "Confirm password is required" }), 20 | }).refine(({ password, confirmPassword }) => password === confirmPassword, { 21 | path: ["confirmPassword"], 22 | message: "Password don't match", 23 | }); 24 | 25 | export type SignUpFormType = z.infer; 26 | 27 | export type SignUpFieldName = keyof SignUpFormType; 28 | 29 | export const SignUpAPIResponseSchema = z.object({ 30 | status: z.boolean(), 31 | message: z.string(), 32 | payload: z.object({ 33 | user: z.object({ 34 | id: z.string(), 35 | name: z.string(), 36 | email: z.string().email(), 37 | phoneNumber: z.union([z.string(), z.null()]), 38 | imageUrl: z.string(), 39 | role: z.nativeEnum(Role), 40 | }), 41 | }), 42 | }); 43 | -------------------------------------------------------------------------------- /src/routes/error-page.tsx: -------------------------------------------------------------------------------- 1 | import { useRouteError } from "react-router-dom"; 2 | 3 | interface RouteError { 4 | statusText?: string; 5 | message?: string; 6 | } 7 | 8 | const ErrorPage: React.FC = () => { 9 | const error = useRouteError(); 10 | 11 | const routeError = error as RouteError; 12 | 13 | return ( 14 |
15 |

Oops!

16 |

Sorry, an unexpected error has occurred.

17 |

18 | {routeError.statusText || routeError.message} 19 |

20 |
21 | ); 22 | }; 23 | 24 | export default ErrorPage; 25 | -------------------------------------------------------------------------------- /src/routes/route-guard.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import { Navigate } from "react-router-dom"; 3 | import { useUserStore } from "@/store/user-store"; 4 | 5 | type RouteProps = { 6 | children: React.ReactNode; 7 | }; 8 | 9 | export const PrivateRoute: FC = ({ children }) => { 10 | const { user } = useUserStore(); 11 | return user ? children : ; 12 | }; 13 | 14 | export const PublicRoute: FC = ({ children }) => { 15 | const { user } = useUserStore(); 16 | return user ? : children; 17 | }; 18 | -------------------------------------------------------------------------------- /src/routes/router.tsx: -------------------------------------------------------------------------------- 1 | import { createBrowserRouter } from "react-router-dom"; 2 | import App from "@/App"; 3 | import ErrorPage from "@/routes/error-page"; 4 | import { Routes } from "@/utils/routes-constants"; 5 | import { SignUpPage } from "@/pages/sign-up/page"; 6 | import { SignInPage } from "@/pages/sign-in/page"; 7 | import { CustomerPage } from "@/pages/customer/page"; 8 | import { PrivateRoute, PublicRoute } from "@/routes/route-guard"; 9 | 10 | export const router = createBrowserRouter([ 11 | { 12 | path: "/", 13 | element: ( 14 | 15 | 16 | 17 | ), 18 | errorElement: , 19 | children: [ 20 | { 21 | index: true, 22 | element:

Home Page

, 23 | }, 24 | { 25 | path: "customers", 26 | element: , 27 | }, 28 | ], 29 | }, 30 | { 31 | path: Routes.SIGNUP, 32 | element: ( 33 | 34 | 35 | 36 | ), 37 | }, 38 | { 39 | path: Routes.SIGNIN, 40 | 41 | element: ( 42 | 43 | 44 | 45 | ), 46 | }, 47 | ]); 48 | -------------------------------------------------------------------------------- /src/services/logout/api.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { api } from "@/utils/api"; 3 | import { 4 | LogoutAPIRequestSchema, 5 | LogoutAPIResponseSchema, 6 | } from "@/services/logout/schema"; 7 | import { API_ENDPOINT } from "@/utils/endpoints-constant"; 8 | import { useMutation } from "@tanstack/react-query"; 9 | import { AxiosError } from "axios"; 10 | import { useUserStore } from "@/store/user-store"; 11 | import { toast } from "sonner"; 12 | import { useNavigate } from "react-router-dom"; 13 | import { Routes } from "@/utils/routes-constants"; 14 | 15 | const LogoutRequest = LogoutAPIRequestSchema; 16 | 17 | const LogoutResponse = LogoutAPIResponseSchema; 18 | 19 | interface ErrorResponse { 20 | message: string; 21 | } 22 | 23 | const logout = api< 24 | z.infer, 25 | z.infer 26 | >({ 27 | method: "POST", 28 | path: API_ENDPOINT.SIGN_OUT, 29 | requestSchema: LogoutRequest, 30 | responseSchema: LogoutResponse, 31 | }); 32 | 33 | export function useLogOut() { 34 | const navigate = useNavigate(); 35 | 36 | const { removeCredentials } = useUserStore(); 37 | return useMutation< 38 | z.infer, 39 | AxiosError 40 | >({ 41 | mutationFn: logout, 42 | onSuccess: (data) => { 43 | const { message } = data; 44 | removeCredentials(); 45 | toast.success(message); 46 | navigate(Routes.SIGNIN); 47 | }, 48 | onError: (error) => { 49 | const errorMessage = error.response?.data.message; 50 | toast.error(errorMessage); 51 | }, 52 | }); 53 | } 54 | -------------------------------------------------------------------------------- /src/services/logout/schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const LogoutAPIRequestSchema = z.void(); 4 | 5 | export const LogoutAPIResponseSchema = z.object({ 6 | status: z.boolean(), 7 | message: z.string(), 8 | payload: z.object({}), 9 | }); 10 | -------------------------------------------------------------------------------- /src/services/refresh-token/api.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { api } from "@/utils/api"; 3 | import { API_ENDPOINT } from "@/utils/endpoints-constant"; 4 | import { 5 | RefreshTokenAPIRequestSchema, 6 | RefreshTokenAPIResponseSchema, 7 | } from "@/services/refresh-token/schema"; 8 | 9 | const RefreshTokenRequest = RefreshTokenAPIRequestSchema; 10 | 11 | const RefreshTokenResponse = RefreshTokenAPIResponseSchema; 12 | 13 | export const getRefreshToken = api< 14 | z.infer, 15 | z.infer 16 | >({ 17 | method: "GET", 18 | path: API_ENDPOINT.REFRESH_TOKEN, 19 | requestSchema: RefreshTokenRequest, 20 | responseSchema: RefreshTokenResponse, 21 | type: "public", 22 | }); 23 | -------------------------------------------------------------------------------- /src/services/refresh-token/schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const RefreshTokenAPIRequestSchema = z.void() 4 | 5 | export const RefreshTokenAPIResponseSchema = z.object({ 6 | status: z.boolean(), 7 | message: z.string(), 8 | payload: z.object({ 9 | accessToken: z.string(), 10 | }), 11 | }); 12 | -------------------------------------------------------------------------------- /src/store/user-store.ts: -------------------------------------------------------------------------------- 1 | import { create, StateCreator } from "zustand"; 2 | import { persist } from "zustand/middleware"; 3 | 4 | interface User { 5 | accessToken: string; 6 | } 7 | 8 | interface UserState { 9 | user: User | null; 10 | setCredentials: (user: User) => void; 11 | removeCredentials: () => void; 12 | } 13 | 14 | const userStoreSlice: StateCreator = (set) => ({ 15 | user: null, 16 | setCredentials: (user) => set({ user }), 17 | removeCredentials: () => set({ user: null }), 18 | }); 19 | 20 | const persistedUserStore = persist(userStoreSlice, { 21 | name: "user", 22 | }); 23 | 24 | export const useUserStore = create(persistedUserStore); 25 | -------------------------------------------------------------------------------- /src/stories/Button.stories.ts: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/button/button"; 2 | import type { Meta, StoryObj } from "@storybook/react"; 3 | import { fn } from "@storybook/test"; 4 | 5 | // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export 6 | const meta = { 7 | title: "Example/Button", 8 | component: Button, 9 | parameters: { 10 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout 11 | layout: "centered", 12 | }, 13 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs 14 | tags: ["autodocs"], 15 | // More on argTypes: https://storybook.js.org/docs/api/argtypes 16 | 17 | // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args 18 | args: { onClick: fn() }, 19 | } satisfies Meta; 20 | 21 | export default meta; 22 | type Story = StoryObj; 23 | 24 | // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args 25 | export const Default: Story = { 26 | args: { 27 | variant: "primary", 28 | size: "default", 29 | children: "Default Button", 30 | }, 31 | }; 32 | 33 | export const Secondary: Story = { 34 | args: { 35 | variant: "secondary", 36 | size: "default", 37 | children: "Secondary Button", 38 | }, 39 | }; 40 | 41 | export const Destructive: Story = { 42 | args: { 43 | variant: "destructive", 44 | size: "default", 45 | children: "Button", 46 | }, 47 | }; 48 | 49 | export const Large: Story = { 50 | args: { 51 | size: "lg", 52 | children: "Button", 53 | }, 54 | }; 55 | 56 | export const Small: Story = { 57 | args: { 58 | size: "sm", 59 | children: "Button", 60 | }, 61 | }; 62 | export const Icon: Story = { 63 | args: { 64 | size: "icon", 65 | variant: "secondary", 66 | children: "+", 67 | }, 68 | }; 69 | -------------------------------------------------------------------------------- /src/stories/Configure.mdx: -------------------------------------------------------------------------------- 1 | import { Meta } from "@storybook/blocks"; 2 | 3 | import Github from "./assets/github.svg"; 4 | import Discord from "./assets/discord.svg"; 5 | import Youtube from "./assets/youtube.svg"; 6 | import Tutorials from "./assets/tutorials.svg"; 7 | import Styling from "./assets/styling.png"; 8 | import Context from "./assets/context.png"; 9 | import Assets from "./assets/assets.png"; 10 | import Docs from "./assets/docs.png"; 11 | import Share from "./assets/share.png"; 12 | import FigmaPlugin from "./assets/figma-plugin.png"; 13 | import Testing from "./assets/testing.png"; 14 | import Accessibility from "./assets/accessibility.png"; 15 | import Theming from "./assets/theming.png"; 16 | import AddonLibrary from "./assets/addon-library.png"; 17 | 18 | export const RightArrow = () => 31 | 32 | 33 | 34 | 35 | 36 |
37 |
38 | # Configure your project 39 | 40 | Because Storybook works separately from your app, you'll need to configure it for your specific stack and setup. Below, explore guides for configuring Storybook with popular frameworks and tools. If you get stuck, learn how you can ask for help from our community. 41 |
42 |
43 |
44 | A wall of logos representing different styling technologies 48 |

Add styling and CSS

49 |

Like with web applications, there are many ways to include CSS within Storybook. Learn more about setting up styling within Storybook.

50 | Learn more 54 |
55 |
56 | An abstraction representing the composition of data for a component 60 |

Provide context and mocking

61 |

Often when a story doesn't render, it's because your component is expecting a specific environment or context (like a theme provider) to be available.

62 | Learn more 66 |
67 |
68 | A representation of typography and image assets 69 |
70 |

Load assets and resources

71 |

To link static files (like fonts) to your projects and stories, use the 72 | `staticDirs` configuration option to specify folders to load when 73 | starting Storybook.

74 | Learn more 78 |
79 |
80 |
81 |
82 |
83 |
84 | # Do more with Storybook 85 | 86 | Now that you know the basics, let's explore other parts of Storybook that will improve your experience. This list is just to get you started. You can customise Storybook in many ways to fit your needs. 87 |
88 | 89 |
90 |
91 |
92 | A screenshot showing the autodocs tag being set, pointing a docs page being generated 93 |

Autodocs

94 |

Auto-generate living, 95 | interactive reference documentation from your components and stories.

96 | Learn more 100 |
101 |
102 | A browser window showing a Storybook being published to a chromatic.com URL 103 |

Publish to Chromatic

104 |

Publish your Storybook to review and collaborate with your entire team.

105 | Learn more 109 |
110 |
111 | Windows showing the Storybook plugin in Figma 112 |

Figma Plugin

113 |

Embed your stories into Figma to cross-reference the design and live 114 | implementation in one place.

115 | Learn more 119 |
120 |
121 | Screenshot of tests passing and failing 122 |

Testing

123 |

Use stories to test a component in all its variations, no matter how 124 | complex.

125 | Learn more 129 |
130 |
131 | Screenshot of accessibility tests passing and failing 132 |

Accessibility

133 |

Automatically test your components for a11y issues as you develop.

134 | Learn more 138 |
139 |
140 | Screenshot of Storybook in light and dark mode 141 |

Theming

142 |

Theme Storybook's UI to personalize it to your project.

143 | Learn more 147 |
148 |
149 |
150 |
151 |
152 |
153 |

Addons

154 |

Integrate your tools with Storybook to connect workflows.

155 | Discover all addons 159 |
160 |
161 | Integrate your tools with Storybook to connect workflows. 162 |
163 |
164 | 165 |
166 |
167 | Github logo 168 | Join our contributors building the future of UI development. 169 | 170 | Star on GitHub 174 |
175 |
176 | Discord logo 177 |
178 | Get support and chat with frontend developers. 179 | 180 | Join Discord server 184 |
185 |
186 |
187 | Youtube logo 188 |
189 | Watch tutorials, feature previews and interviews. 190 | 191 | Watch on YouTube 195 |
196 |
197 |
198 | A book 199 |

Follow guided walkthroughs on for key workflows.

200 | 201 | Discover tutorials 205 |
206 |
207 | 208 | 365 | -------------------------------------------------------------------------------- /src/stories/assets/accessibility.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeTiger/jwt-react-example/f6cf27b8ddcf0ae9651dcceb7f3b7d5833bdc2a8/src/stories/assets/accessibility.png -------------------------------------------------------------------------------- /src/stories/assets/accessibility.svg: -------------------------------------------------------------------------------- 1 | 2 | Accessibility 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/stories/assets/addon-library.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeTiger/jwt-react-example/f6cf27b8ddcf0ae9651dcceb7f3b7d5833bdc2a8/src/stories/assets/addon-library.png -------------------------------------------------------------------------------- /src/stories/assets/assets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeTiger/jwt-react-example/f6cf27b8ddcf0ae9651dcceb7f3b7d5833bdc2a8/src/stories/assets/assets.png -------------------------------------------------------------------------------- /src/stories/assets/avif-test-image.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeTiger/jwt-react-example/f6cf27b8ddcf0ae9651dcceb7f3b7d5833bdc2a8/src/stories/assets/avif-test-image.avif -------------------------------------------------------------------------------- /src/stories/assets/context.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeTiger/jwt-react-example/f6cf27b8ddcf0ae9651dcceb7f3b7d5833bdc2a8/src/stories/assets/context.png -------------------------------------------------------------------------------- /src/stories/assets/discord.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/stories/assets/docs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeTiger/jwt-react-example/f6cf27b8ddcf0ae9651dcceb7f3b7d5833bdc2a8/src/stories/assets/docs.png -------------------------------------------------------------------------------- /src/stories/assets/figma-plugin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeTiger/jwt-react-example/f6cf27b8ddcf0ae9651dcceb7f3b7d5833bdc2a8/src/stories/assets/figma-plugin.png -------------------------------------------------------------------------------- /src/stories/assets/github.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/stories/assets/share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeTiger/jwt-react-example/f6cf27b8ddcf0ae9651dcceb7f3b7d5833bdc2a8/src/stories/assets/share.png -------------------------------------------------------------------------------- /src/stories/assets/styling.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeTiger/jwt-react-example/f6cf27b8ddcf0ae9651dcceb7f3b7d5833bdc2a8/src/stories/assets/styling.png -------------------------------------------------------------------------------- /src/stories/assets/testing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeTiger/jwt-react-example/f6cf27b8ddcf0ae9651dcceb7f3b7d5833bdc2a8/src/stories/assets/testing.png -------------------------------------------------------------------------------- /src/stories/assets/theming.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativeTiger/jwt-react-example/f6cf27b8ddcf0ae9651dcceb7f3b7d5833bdc2a8/src/stories/assets/theming.png -------------------------------------------------------------------------------- /src/stories/assets/tutorials.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/stories/assets/youtube.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/utils/api.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { instance, instanceWithoutInterceptors } from "@/lib/axios"; 3 | import { AxiosRequestConfig, Method } from "axios"; 4 | 5 | interface APICallPayload { 6 | method: Method; 7 | path: string; 8 | requestSchema: z.ZodType; 9 | responseSchema: z.ZodType; 10 | type?: "private" | "public"; 11 | } 12 | 13 | export function api({ 14 | type = "private", 15 | method, 16 | path, 17 | requestSchema, 18 | responseSchema, 19 | }: APICallPayload) { 20 | return async (requestData: Request) => { 21 | // Validate request data 22 | requestSchema.parse(requestData); 23 | 24 | // Prepare API call 25 | let url = path; 26 | let data = null; 27 | 28 | if (requestData) { 29 | if (method === "GET" || method === "DELETE") { 30 | url += `${requestData}`; 31 | } else { 32 | data = requestData; 33 | } 34 | } 35 | 36 | const config: AxiosRequestConfig = { 37 | method, 38 | url, 39 | data, 40 | }; 41 | 42 | // Make API call base on the type of request 43 | const response = 44 | type === "private" 45 | ? await instance(config) 46 | : await instanceWithoutInterceptors(config); 47 | 48 | // Parse and validate response data 49 | const result = responseSchema.safeParse(response.data); 50 | 51 | if (!result.success) { 52 | console.error("🚨 Safe-Parsing Failed ", result.error); 53 | throw new Error(result.error.message); 54 | } else { 55 | return result.data; 56 | } 57 | }; 58 | } 59 | -------------------------------------------------------------------------------- /src/utils/endpoints-constant.ts: -------------------------------------------------------------------------------- 1 | export const API_ENDPOINT = { 2 | SIGN_IN: "auth/login", 3 | SIGN_UP: "users", 4 | SIGN_OUT: "auth/logout", 5 | REFRESH_TOKEN: "auth/refresh", 6 | USERS: "users", 7 | }; 8 | -------------------------------------------------------------------------------- /src/utils/role-enum.ts: -------------------------------------------------------------------------------- 1 | export enum Role { 2 | ADMIN = "ADMIN", 3 | USER = "USER", 4 | } 5 | -------------------------------------------------------------------------------- /src/utils/routes-constants.ts: -------------------------------------------------------------------------------- 1 | export const Routes = { 2 | HOME: "/", 3 | SIGNIN: "/sign-in", 4 | SIGNUP: "/sign-up", 5 | CUSTOMER: "/customers" 6 | }; 7 | -------------------------------------------------------------------------------- /src/utils/tailwind-merge.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | interface ImportMetaEnv { 3 | readonly VITE_BASE_URL: string; 4 | } 5 | 6 | interface ImportMeta { 7 | readonly env: ImportMetaEnv; 8 | } 9 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], 4 | theme: { 5 | extend: { 6 | boxShadow: { 7 | base: "rgba(149, 157, 165, 0.2) 0px 8px 24px", 8 | }, 9 | }, 10 | }, 11 | plugins: [], 12 | }; 13 | -------------------------------------------------------------------------------- /tests/setup.ts: -------------------------------------------------------------------------------- 1 | import { afterEach } from "vitest"; 2 | import { cleanup } from "@testing-library/react"; 3 | import "@testing-library/jest-dom/vitest"; 4 | 5 | afterEach(() => { 6 | cleanup(); 7 | }); 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true, 22 | 23 | "paths": { 24 | "@/*": ["./src/*"] 25 | } 26 | }, 27 | "include": ["src"], 28 | "references": [{ "path": "./tsconfig.node.json" }] 29 | } 30 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true, 8 | "strict": true 9 | }, 10 | "include": ["vite.config.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react-swc"; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig(() => ({ 6 | resolve: { 7 | alias: [{ find: "@", replacement: "/src" }], 8 | }, 9 | plugins: [react()], 10 | test: { 11 | globals: true, 12 | environment: "jsdom", 13 | setupFiles: "./tests/setup", 14 | }, 15 | })); 16 | --------------------------------------------------------------------------------