├── .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(Primary );
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 |
20 | Primary
21 |
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(Primary );
37 |
38 | expect(ref.current).toBeTruthy();
39 |
40 | screen.debug();
41 | });
42 |
43 | it("renders default props", () => {
44 | render(Default Button );
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 |
44 | );
45 | }
46 | );
47 |
48 | Button.displayName = "Button";
49 |
50 | export { Button };
51 |
--------------------------------------------------------------------------------
/src/components/card/card.tsx:
--------------------------------------------------------------------------------
1 | interface CardProps {
2 | imageUrl: string;
3 | name: string;
4 | email: string;
5 | }
6 |
7 | export function Card({ email, imageUrl, name }: CardProps) {
8 | return (
9 |
10 |
15 |
16 |
17 | {name}
18 |
19 | {email}
20 |
21 |
22 | );
23 | }
24 |
25 | export function LoadingCard() {
26 | return (
27 |
36 | );
37 | }
38 |
39 | export function LoadingCardGrid() {
40 | return (
41 |
42 | {Array.from(Array(4)).map((_, index) => (
43 |
44 | ))}
45 |
46 | );
47 | }
48 |
--------------------------------------------------------------------------------
/src/components/input/input-error-message.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from "react";
2 | import { ErrorMessage } from "@hookform/error-message";
3 | import { InputErrorProps } from "@/components/input/input";
4 |
5 | export const InputErrorMessage: FC = ({ errors, name }) => {
6 | return (
7 | {message}
}
11 | />
12 | );
13 | };
14 |
--------------------------------------------------------------------------------
/src/components/input/input.test.tsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nativeTiger/jwt-react-example/f6cf27b8ddcf0ae9651dcceb7f3b7d5833bdc2a8/src/components/input/input.test.tsx
--------------------------------------------------------------------------------
/src/components/input/input.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from "react";
2 | import { cn } from "@/utils/tailwind-merge";
3 | import { FieldErrors, useFormContext } from "react-hook-form";
4 | import { InputErrorMessage } from "@/components/input/input-error-message";
5 | import { SignUpFieldName, SignUpFormType } from "@/pages/sign-up/schema";
6 |
7 | export interface InputErrorProps {
8 | name: SignUpFieldName;
9 | errors: FieldErrors;
10 | }
11 |
12 | export interface InputProps
13 | extends InputErrorProps,
14 | Omit, "name"> {
15 | label: string;
16 | }
17 |
18 | export const Input: FC = ({
19 | className,
20 | name,
21 | label,
22 | errors,
23 | ...props
24 | }) => {
25 | const { register } = useFormContext();
26 |
27 | const hasError = Object.prototype.hasOwnProperty.call(errors, name);
28 |
29 | return (
30 |
31 |
32 |
33 | {label}
34 |
35 |
43 |
44 |
45 |
46 | );
47 | };
48 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer utilities {
6 | .scrollbar::-webkit-scrollbar {
7 | width: 10px;
8 | height: 10px;
9 | }
10 |
11 | .scrollbar::-webkit-scrollbar-track {
12 | border-radius: 100vh;
13 | --tw-text-opacity: 1;
14 | background: rgb(249 249 249 / var(--tw-text-opacity));
15 | }
16 |
17 | .scrollbar::-webkit-scrollbar-thumb {
18 | --tw-text-opacity: 1;
19 | background: rgb(219 234 254 / var(--tw-text-opacity));
20 | border-radius: 100vh;
21 | border: 3px solid #f6f7ed;
22 | }
23 | .form {
24 | @apply space-y-3;
25 | &__title {
26 | @apply text-3xl text-gray-600 font-semibold text-center mb-4;
27 | }
28 | &__label {
29 | @apply text-sm font-medium pb-2 text-gray-600;
30 | }
31 | &__input {
32 | @apply outline-none w-full mt-1 text-gray-500 border-2 rounded-md px-4 py-1.5 duration-200 focus:border-blue-600;
33 | }
34 | &__error {
35 | @apply text-red-500 py-1 text-sm;
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/layouts/header/header.tsx:
--------------------------------------------------------------------------------
1 | import { Menu, Transition } from "@headlessui/react";
2 | import { Fragment } from "react";
3 | import { RxChevronDown, RxMix } from "react-icons/rx";
4 | import { RiUserFill, RiLogoutBoxRLine } from "react-icons/ri";
5 | import { Button } from "@/components/button/button";
6 | import { Link, useNavigate } from "react-router-dom";
7 | import { Routes } from "@/utils/routes-constants";
8 | import { useLogOut } from "@/services/logout/api";
9 |
10 | export const Header = () => {
11 | const navigate = useNavigate();
12 |
13 | const logoutMutation = useLogOut();
14 |
15 | return (
16 |
17 |
18 |
19 |
20 |
21 |
22 | Hello World
23 |
24 |
25 |
26 |
27 |
32 |
33 |
34 |
35 |
36 |
45 |
46 |
47 | navigate(Routes.CUSTOMER)}
52 | >
53 | Customers
54 |
55 |
56 |
57 | logoutMutation.mutate()}
62 | >
63 | Logout
64 |
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 |
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 |
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 |
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 |
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 |
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 |
93 |
Autodocs
94 |
Auto-generate living,
95 | interactive reference documentation from your components and stories.
96 |
Learn more
100 |
101 |
102 |
103 |
Publish to Chromatic
104 |
Publish your Storybook to review and collaborate with your entire team.
105 |
Learn more
109 |
110 |
111 |
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 |
122 |
Testing
123 |
Use stories to test a component in all its variations, no matter how
124 | complex.
125 |
Learn more
129 |
130 |
131 |
132 |
Accessibility
133 |
Automatically test your components for a11y issues as you develop.
134 |
Learn more
138 |
139 |
140 |
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 |
162 |
163 |
164 |
165 |
166 |
167 |
168 | Join our contributors building the future of UI development.
169 |
170 |
Star on GitHub
174 |
175 |
176 |
177 |
185 |
186 |
187 |
188 |
189 | Watch tutorials, feature previews and interviews.
190 |
191 |
Watch on YouTube
195 |
196 |
197 |
198 |
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 |
--------------------------------------------------------------------------------