├── .env.example ├── .editorconfig ├── .prettierignore ├── src ├── utils │ ├── index.ts │ └── string.ts ├── ts │ └── types │ │ ├── index.ts │ │ └── common.ts ├── data │ ├── index.ts │ └── constant │ │ ├── path.ts │ │ ├── color.ts │ │ ├── type-navs.ts │ │ └── navs.tsx ├── features │ └── todo │ │ ├── index.ts │ │ ├── services │ │ ├── types.ts │ │ └── todo.api.ts │ │ └── hooks │ │ └── use-todo-query.ts ├── components │ ├── index.ts │ └── common │ │ ├── toaster │ │ └── toaster-config.tsx │ │ └── button │ │ └── button-theme.tsx ├── vite-env.d.ts ├── hooks │ ├── toast │ │ └── use-toast.ts │ ├── index.ts │ ├── use-active-menu.ts │ ├── use-modal-store.ts │ └── theme-store │ │ └── use-theme-store.ts ├── pages │ ├── home.tsx │ ├── users │ │ └── index.tsx │ ├── index.ts │ ├── not-found.tsx │ └── todos │ │ └── index.tsx ├── routes │ ├── index.tsx │ └── render-router.tsx ├── layout │ ├── footer │ │ └── index.tsx │ ├── error-boundary │ │ └── fallbackRender.tsx │ ├── index.tsx │ └── header │ │ └── index.tsx ├── main.tsx ├── provider │ ├── query-provider.tsx │ └── theme-config-provider.tsx ├── apis │ └── axios-client.ts ├── index.css └── assets │ └── react.svg ├── .env ├── .prettierrc ├── .huskyrc ├── .husky └── pre-commit ├── postcss.config.js ├── public ├── assets │ └── imgs │ │ └── banner.png └── vite.svg ├── .eslintignore ├── tsconfig.node.json ├── postinstall.sh ├── .gitignore ├── index.html ├── vite.config.ts ├── .github └── FUNDING.yml ├── tsconfig.json ├── tailwind.config.js ├── .eslintrc.cjs ├── package.json └── README.md /.env.example: -------------------------------------------------------------------------------- 1 | VITE_API_URL= -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_size = 2 -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build 2 | node_modules -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './string'; 2 | -------------------------------------------------------------------------------- /src/ts/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './common'; 2 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | VITE_API_URL=https://public-api-crud-todo-app.vercel.app/ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "endOfLine": "auto", 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /.huskyrc: -------------------------------------------------------------------------------- 1 | export NVM_DIR="$HOME/.nvm" 2 | [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" -------------------------------------------------------------------------------- /src/data/index.ts: -------------------------------------------------------------------------------- 1 | export * from './constant/path'; 2 | export * from './constant/color'; 3 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /src/features/todo/index.ts: -------------------------------------------------------------------------------- 1 | export * from './services/types'; 2 | 3 | export * from './hooks/use-todo-query'; 4 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './common/button/button-theme'; 2 | export * from './common/toaster/toaster-config'; 3 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface ImportMetaEnv { 4 | VITE_API_URL: string; 5 | } 6 | -------------------------------------------------------------------------------- /public/assets/imgs/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonht113/react-boilerplate-for-starter/HEAD/public/assets/imgs/banner.png -------------------------------------------------------------------------------- /src/utils/string.ts: -------------------------------------------------------------------------------- 1 | export function capitalizeFirstLetter(str: string) { 2 | return str.charAt(0).toUpperCase() + str.slice(1); 3 | } 4 | -------------------------------------------------------------------------------- /src/hooks/toast/use-toast.ts: -------------------------------------------------------------------------------- 1 | import { toast } from 'react-hot-toast'; 2 | 3 | function useToast() { 4 | return { toast }; 5 | } 6 | 7 | export default useToast; 8 | -------------------------------------------------------------------------------- /src/pages/home.tsx: -------------------------------------------------------------------------------- 1 | const Home = () => { 2 | return ( 3 |
4 | Home 5 |
6 | ); 7 | }; 8 | 9 | export default Home; 10 | -------------------------------------------------------------------------------- /src/data/constant/path.ts: -------------------------------------------------------------------------------- 1 | export const LOGIN_PATH = '/login'; 2 | export const HOME_PATH = '/home'; 3 | export const USER_PATH = '/users'; 4 | export const TODO_PATH = '/todo'; 5 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | node_modules 4 | *.test.js 5 | vite.config.ts 6 | postcss.config.js 7 | tailwind.config.js 8 | i18n.ts 9 | public/firebase-messaging-sw.js 10 | src/assets -------------------------------------------------------------------------------- /src/pages/users/index.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | 3 | const Users: FC = () => { 4 | return ( 5 |
6 | Users 7 |
8 | ); 9 | }; 10 | 11 | export default Users; 12 | -------------------------------------------------------------------------------- /src/pages/index.ts: -------------------------------------------------------------------------------- 1 | import { lazy } from 'react'; 2 | 3 | const Home = lazy(() => import('@/pages/home')); 4 | 5 | const Users = lazy(() => import('@/pages/users')); 6 | 7 | const Todos = lazy(() => import('@/pages/todos')); 8 | 9 | export { Home, Users, Todos }; 10 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /src/data/constant/color.ts: -------------------------------------------------------------------------------- 1 | const COLOR = { 2 | ACTIVE: '#34A853', 3 | LOCKED: '#F31111', 4 | PAUSE: '#FA8C16', 5 | DISABLED: '#96969B', 6 | LIGHT_PRIMARY: '#C67F03', 7 | DARK_PRIMARY: '#3b78f9', 8 | LOGIN_BG: '#162C5B', 9 | }; 10 | 11 | export default COLOR; 12 | -------------------------------------------------------------------------------- /postinstall.sh: -------------------------------------------------------------------------------- 1 | HUSKYFOLDER=.husky 2 | FILE=.husky/pre-commit 3 | FILE2=.husky/_ 4 | if [ ! -d "$HUSKYFOLDER" ] || [ ! -f "$FILE" ] || [ ! -d "$FILE2" ]; then 5 | git init & mkdir .husky & npx husky install & rm -rf .husky/pre-commit & npx husky add .husky/pre-commit 'pnpm lint-staged' 6 | fi -------------------------------------------------------------------------------- /src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | import useThemeStore from './theme-store/use-theme-store'; 2 | import useToast from './toast/use-toast'; 3 | import { useActiveMenu } from './use-active-menu'; 4 | import useModalStore from './use-modal-store'; 5 | 6 | export { useThemeStore, useModalStore, useToast, useActiveMenu }; 7 | -------------------------------------------------------------------------------- /src/hooks/use-active-menu.ts: -------------------------------------------------------------------------------- 1 | import { useLocation } from 'react-router-dom'; 2 | 3 | export const useActiveMenu = () => { 4 | const router = useLocation(); 5 | const path = router.pathname; 6 | 7 | const checkActive = (link: string) => { 8 | return path === link || path.includes(link); 9 | }; 10 | 11 | return { checkActive }; 12 | }; 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /src/features/todo/services/types.ts: -------------------------------------------------------------------------------- 1 | export type TodoData = { 2 | _id: string; 3 | todoName: string; 4 | isComplete: boolean; 5 | createdAt: string; 6 | updatedAt: string; 7 | }; 8 | 9 | export type ResponseData = { 10 | code: number; 11 | data: TodoData[]; 12 | }; 13 | 14 | export type TodoDataMutation = Partial< 15 | Pick 16 | >; 17 | -------------------------------------------------------------------------------- /src/hooks/use-modal-store.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand'; 2 | 3 | type ModalStore = { 4 | isOpen: boolean; 5 | open: () => void; 6 | close: () => void; 7 | }; 8 | 9 | const useModalStore = create()((set) => ({ 10 | isOpen: false, 11 | open: () => set({ isOpen: true }), 12 | close: () => set({ isOpen: false }), 13 | })); 14 | 15 | export default useModalStore; 16 | -------------------------------------------------------------------------------- /src/routes/index.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from 'react'; 2 | 3 | import { BrowserRouter } from 'react-router-dom'; 4 | 5 | import RenderRouter from './render-router'; 6 | 7 | const Routes = () => { 8 | return ( 9 | 10 | 11 | 12 | 13 | 14 | ); 15 | }; 16 | 17 | export default Routes; 18 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ReactJS Boilerplate By TrongSon 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/data/constant/type-navs.ts: -------------------------------------------------------------------------------- 1 | import { ReactElement } from 'react'; 2 | 3 | type NavsChild = { 4 | key: string; 5 | label?: string | ReactElement; 6 | element?: ReactElement; 7 | }; 8 | 9 | export type TypeNavs = NavsChild & { 10 | children?: TypeNavs[]; 11 | }; 12 | 13 | type RouteChild = { 14 | path: string; 15 | element: ReactElement; 16 | }; 17 | 18 | export type TypeRoutes = RouteChild & { 19 | children?: RouteChild[]; 20 | }; 21 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | import path from 'path'; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | resolve: { 8 | alias: { 9 | '@': path.join(__dirname, 'src'), 10 | }, 11 | }, 12 | plugins: [ 13 | react({ 14 | jsxImportSource: '@emotion/react', 15 | babel: { 16 | plugins: ['@emotion/babel-plugin'], 17 | }, 18 | }), 19 | ], 20 | }); 21 | -------------------------------------------------------------------------------- /src/layout/footer/index.tsx: -------------------------------------------------------------------------------- 1 | const FooterComponent = () => { 2 | return ( 3 |
4 |
5 |
6 | logo 7 | Footer-ReactJS Boilerplate By TrongSon 8 |
9 |
10 |
11 | ) 12 | } 13 | 14 | export default FooterComponent; -------------------------------------------------------------------------------- /src/components/common/toaster/toaster-config.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | 3 | import { Toaster } from 'react-hot-toast'; 4 | 5 | type Props = { 6 | position?: 7 | | 'top-left' 8 | | 'top-center' 9 | | 'top-right' 10 | | 'bottom-left' 11 | | 'bottom-center' 12 | | 'bottom-right'; 13 | reverseOrder?: boolean; 14 | }; 15 | 16 | export const ToasterConfig: FC = ({ 17 | position = 'top-center', 18 | reverseOrder = false, 19 | }) => { 20 | return ; 21 | }; 22 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import ReactDOM from 'react-dom/client'; 4 | 5 | import './index.css'; 6 | import QueryProvider from './provider/query-provider.tsx'; 7 | import LayoutConfigProvider from './provider/theme-config-provider.tsx'; 8 | import Routes from './routes/index.tsx'; 9 | import { ToasterConfig } from '@/components'; 10 | 11 | ReactDOM.createRoot(document.getElementById('root')!).render( 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | , 20 | ); 21 | -------------------------------------------------------------------------------- /src/layout/error-boundary/fallbackRender.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | 3 | import { FallbackProps } from 'react-error-boundary'; 4 | 5 | const fallbackRender: (props: FallbackProps) => ReactNode = ({ 6 | error, 7 | resetErrorBoundary, 8 | }: { 9 | error: Record<'message', string>; 10 | resetErrorBoundary: FallbackProps['resetErrorBoundary']; 11 | }) => { 12 | return ( 13 |
14 | Something went wrong: 15 |
{error.message}
16 | 17 |
18 | ); 19 | }; 20 | 21 | export default fallbackRender; 22 | -------------------------------------------------------------------------------- /src/routes/render-router.tsx: -------------------------------------------------------------------------------- 1 | import { FC, lazy } from 'react'; 2 | 3 | import { Navigate, useRoutes } from 'react-router-dom'; 4 | 5 | import { routeList } from '@/data/constant/navs'; 6 | import LayoutComponent from '@/layout'; 7 | 8 | const NotFound = lazy(() => import('@/pages/not-found')); 9 | 10 | const routes = [ 11 | { 12 | path: '/', 13 | element: , 14 | children: [ 15 | { 16 | path: '', 17 | element: , 18 | }, 19 | ...routeList, 20 | { 21 | path: '*', 22 | element: , 23 | }, 24 | ], 25 | }, 26 | ]; 27 | 28 | const RenderRouter: FC = () => { 29 | const element = useRoutes(routes); 30 | 31 | return element; 32 | }; 33 | 34 | export default RenderRouter; 35 | -------------------------------------------------------------------------------- /src/ts/types/common.ts: -------------------------------------------------------------------------------- 1 | import { QueryKey, UseQueryOptions } from '@tanstack/react-query'; 2 | 3 | export type PageParams = { 4 | page?: number; 5 | limit?: number; 6 | }; 7 | 8 | export type QueryOptions = Omit< 9 | UseQueryOptions, 10 | | 'queryKey' 11 | | 'queryFn' 12 | | 'refetchInterval' 13 | | 'refetchOnMount' 14 | | 'refetchOnReconnect' 15 | | 'refetchOnWindowFocus' 16 | | 'useErrorBoundary' 17 | >; 18 | 19 | export type ValueOf = T[keyof T]; 20 | 21 | // K is the union keyof T whose type is required, 22 | // the remaining keys of T have the same type 23 | export type RequiredKeys = Required< 24 | Pick> 25 | > & 26 | Omit>; 27 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [sonht113] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: [sonht113] 14 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 15 | -------------------------------------------------------------------------------- /src/features/todo/services/todo.api.ts: -------------------------------------------------------------------------------- 1 | import { ResponseData, TodoData, TodoDataMutation } from './types'; 2 | import axiosClient from '@/apis/axios-client'; 3 | 4 | const baseUrl = 'todos'; 5 | 6 | const todoApi = { 7 | getList: (): Promise => axiosClient.get(baseUrl), 8 | getDetail: (id: string): Promise<{ code: number; data: TodoData }> => 9 | axiosClient.get(`${baseUrl}/${id}`), 10 | add: (body: TodoDataMutation): Promise<{ code: number; data: TodoData }> => 11 | axiosClient.post(baseUrl, body), 12 | update: (body: { 13 | id: string; 14 | data: TodoDataMutation; 15 | }): Promise<{ code: number; data: TodoData }> => 16 | axiosClient.put(baseUrl + `/${body.id}`, body.data), 17 | delete: (id: string): Promise<{ code: number; message: string }> => 18 | axiosClient.delete(baseUrl + `/${id}`), 19 | }; 20 | 21 | export default todoApi; 22 | -------------------------------------------------------------------------------- /src/hooks/theme-store/use-theme-store.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand'; 2 | 3 | type ThemeStore = { 4 | theme: 'dark' | 'light'; 5 | setTheme: (_: { theme: ThemeStore['theme'] }) => void; 6 | }; 7 | 8 | const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches 9 | ? 'dark' 10 | : 'light'; 11 | const userTheme = localStorage.getItem('theme') as ThemeStore['theme']; 12 | 13 | const useThemeStore = create()((set) => ({ 14 | theme: userTheme || 'dark' || systemTheme, 15 | setTheme: ({ theme }: { theme: ThemeStore['theme'] }) => { 16 | const body = document.body; 17 | 18 | if (theme === 'dark') { 19 | if (!body.hasAttribute('class')) { 20 | body.setAttribute('class', 'dark'); 21 | } 22 | } else { 23 | if (body.hasAttribute('class')) { 24 | body.removeAttribute('class'); 25 | } 26 | } 27 | 28 | set({ 29 | theme, 30 | }); 31 | }, 32 | })); 33 | 34 | export default useThemeStore; 35 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "types": ["vite/client"], 5 | "useDefineForClassFields": true, 6 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 7 | "allowJs": false, 8 | "module": "ESNext", 9 | "skipLibCheck": true, 10 | "esModuleInterop": false, 11 | 12 | /* Bundler mode */ 13 | "moduleResolution": "bundler", 14 | "allowImportingTsExtensions": true, 15 | "allowSyntheticDefaultImports": true, 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "noEmit": true, 19 | "jsx": "react-jsx", 20 | 21 | /* Linting */ 22 | "strict": true, 23 | "forceConsistentCasingInFileNames": true, 24 | "noUnusedLocals": true, 25 | "noUnusedParameters": true, 26 | "noFallthroughCasesInSwitch": true, 27 | 28 | "baseUrl": "./", 29 | "paths": { 30 | "@/*": ["src/*"] 31 | } 32 | }, 33 | "include": ["src", "tsconfig.json"], 34 | "references": [{ "path": "./tsconfig.node.json" }] 35 | } 36 | -------------------------------------------------------------------------------- /src/layout/index.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from 'react'; 2 | 3 | import { ErrorBoundary } from 'react-error-boundary'; 4 | import { Outlet } from 'react-router-dom'; 5 | 6 | import fallbackRender from './error-boundary/fallbackRender'; 7 | import FooterComponent from './footer'; 8 | import HeaderComponent from './header'; 9 | 10 | const LayoutComponent = () => { 11 | return ( 12 |
13 | 14 |
15 | 16 | 19 | Loading... 20 |
21 | } 22 | > 23 | 24 | 25 | 26 |
27 | 28 | 29 | ); 30 | }; 31 | 32 | export default LayoutComponent; 33 | -------------------------------------------------------------------------------- /src/pages/not-found.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-member-access */ 2 | /* eslint-disable @typescript-eslint/no-unsafe-assignment */ 3 | /* eslint-disable @typescript-eslint/no-unsafe-call */ 4 | import styled from 'styled-components'; 5 | 6 | const NotFound = () => { 7 | return ( 8 | 9 | 10 | 4 11 | <span role="img" aria-label="Crying Face"> 12 | 😢 13 | </span> 14 | 4 15 | 16 |

Page not found.

17 |
18 | ); 19 | }; 20 | 21 | export default NotFound; 22 | 23 | const Wrapper = styled.div` 24 | height: 100vh; 25 | display: flex; 26 | align-items: center; 27 | justify-content: center; 28 | flex-direction: column; 29 | min-height: 320px; 30 | `; 31 | 32 | const Title = styled.p` 33 | margin-top: -8vh; 34 | font-weight: bold; 35 | font-size: 3.375rem; 36 | 37 | span { 38 | font-size: 3.125rem; 39 | } 40 | `; 41 | 42 | export const P = styled.p` 43 | font-size: 1rem; 44 | line-height: 1.5; 45 | margin: 0.625rem 0 1.5rem 0; 46 | `; 47 | -------------------------------------------------------------------------------- /src/provider/query-provider.tsx: -------------------------------------------------------------------------------- 1 | import { FC, ReactNode, useState } from 'react'; 2 | 3 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; 4 | import { AxiosError } from 'axios'; 5 | 6 | import { useToast } from '@/hooks'; 7 | 8 | type Props = { 9 | children: ReactNode; 10 | }; 11 | 12 | const QueryProvider: FC = ({ children }) => { 13 | const { toast } = useToast(); 14 | const [queryClient] = useState( 15 | () => 16 | new QueryClient({ 17 | defaultOptions: { 18 | queries: { 19 | refetchOnWindowFocus: false, 20 | retry: false, 21 | onError: (error: unknown) => { 22 | void toast.error( 23 | `Something went wrong: ${ 24 | ( 25 | error as AxiosError<{ 26 | message: string; 27 | }> 28 | )?.response?.data.message || 'unknown' 29 | }`, 30 | ); 31 | }, 32 | }, 33 | }, 34 | }), 35 | ); 36 | 37 | return ( 38 | {children} 39 | ); 40 | }; 41 | 42 | export default QueryProvider; 43 | -------------------------------------------------------------------------------- /src/layout/header/index.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from 'react-router-dom'; 2 | 3 | import { ButtonTheme } from '@/components'; 4 | import { navList } from '@/data/constant/navs'; 5 | import { useActiveMenu } from '@/hooks'; 6 | 7 | const HeaderComponent = () => { 8 | const { checkActive } = useActiveMenu(); 9 | 10 | return ( 11 |
12 |
13 |
14 |
15 | {navList.map((item) => ( 16 | 17 | 24 | {item.label} 25 | 26 | 27 | ))} 28 |
29 | 30 |
31 |
32 |
33 | ); 34 | }; 35 | 36 | export default HeaderComponent; 37 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/apis/axios-client.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-member-access */ 2 | /* eslint-disable @typescript-eslint/no-unsafe-return */ 3 | import axios, { AxiosError } from 'axios'; 4 | 5 | import { LOGIN_PATH } from '@/data'; 6 | 7 | const axiosClient = axios.create({ 8 | baseURL: import.meta.env.VITE_API_URL, 9 | headers: { 10 | 'Content-Type': 'application/json', 11 | }, 12 | }); 13 | // Interceptors 14 | // Add a request interceptor 15 | axiosClient.interceptors.request.use( 16 | function (config) { 17 | // Do something before request is sent 18 | const token = localStorage.getItem('token'); 19 | if (token) { 20 | config.headers['Authorization'] = 'Bearer ' + token; 21 | } 22 | 23 | return config; 24 | }, 25 | function (error) { 26 | // Do something with request error 27 | return Promise.reject(error); 28 | }, 29 | ); 30 | 31 | // Add a response interceptor 32 | axiosClient.interceptors.response.use( 33 | function (response) { 34 | // Any status code that lie within the range of 2xx cause this function to trigger 35 | // Do something with response data 36 | return response.data; 37 | }, 38 | function (error: AxiosError) { 39 | // Any status codes that falls outside the range of 2xx cause this function to trigger 40 | // Do something with response error 41 | if (error.response?.status === 401) { 42 | // clear token ... 43 | localStorage.removeItem('token'); 44 | window.location.replace(LOGIN_PATH); 45 | } 46 | 47 | return Promise.reject(error); 48 | }, 49 | ); 50 | 51 | export default axiosClient; 52 | -------------------------------------------------------------------------------- /src/provider/theme-config-provider.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement, useCallback, useEffect } from 'react'; 2 | 3 | import CssBaseline from '@mui/material/CssBaseline'; 4 | import { ThemeProvider, createTheme } from '@mui/material/styles'; 5 | 6 | import { useThemeStore } from '@/hooks'; 7 | 8 | type Props = { 9 | children: ReactElement; 10 | }; 11 | 12 | function LayoutConfigProvider({ children }: Props) { 13 | const theme = useThemeStore((state) => state.theme); 14 | const setTheme = useThemeStore((state) => state.setTheme); 15 | 16 | const setThemeState = useCallback( 17 | (dark = true) => { 18 | setTheme({ 19 | theme: dark ? 'dark' : 'light', 20 | }); 21 | }, 22 | [setTheme], 23 | ); 24 | 25 | const matchMode = useCallback( 26 | (e: MediaQueryListEvent) => { 27 | setThemeState(e.matches); 28 | }, 29 | [setThemeState], 30 | ); 31 | 32 | useEffect(() => { 33 | const root = window.document.documentElement; 34 | 35 | root.classList.remove('light', 'dark'); 36 | setThemeState(theme === 'dark'); 37 | 38 | // watch system theme change 39 | if (!localStorage.getItem('theme')) { 40 | const mql = window.matchMedia('(prefers-color-scheme: dark)'); 41 | 42 | mql.addEventListener('change', matchMode); 43 | } 44 | 45 | root.classList.add(theme); 46 | }, [matchMode, setThemeState, theme]); 47 | 48 | return ( 49 | 56 | {' '} 57 | 58 | {children} 59 | 60 | ); 61 | } 62 | 63 | export default LayoutConfigProvider; -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 240 10% 3.9%; 9 | --card: 0 0% 100%; 10 | --card-foreground: 240 10% 3.9%; 11 | --popover: 0 0% 100%; 12 | --popover-foreground: 240 10% 3.9%; 13 | --primary: 346.8 77.2% 49.8%; 14 | --primary-foreground: 355.7 100% 97.3%; 15 | --secondary: 240 4.8% 95.9%; 16 | --secondary-foreground: 240 5.9% 10%; 17 | --muted: 240 4.8% 95.9%; 18 | --muted-foreground: 240 3.8% 46.1%; 19 | --accent: 240 4.8% 95.9%; 20 | --accent-foreground: 240 5.9% 10%; 21 | --destructive: 0 84.2% 60.2%; 22 | --destructive-foreground: 0 0% 98%; 23 | --border: 240 5.9% 90%; 24 | --input: 240 5.9% 90%; 25 | --ring: 346.8 77.2% 49.8%; 26 | --radius: 0.5rem; 27 | } 28 | 29 | .dark { 30 | --background: 20 14.3% 4.1%; 31 | --foreground: 0 0% 95%; 32 | --card: 24 9.8% 10%; 33 | --card-foreground: 0 0% 95%; 34 | --popover: 0 0% 9%; 35 | --popover-foreground: 0 0% 95%; 36 | --primary: 346.8 77.2% 49.8%; 37 | --primary-foreground: 355.7 100% 97.3%; 38 | --secondary: 240 3.7% 15.9%; 39 | --secondary-foreground: 0 0% 98%; 40 | --muted: 0 0% 15%; 41 | --muted-foreground: 240 5% 64.9%; 42 | --accent: 12 6.5% 15.1%; 43 | --accent-foreground: 0 0% 98%; 44 | --destructive: 0 62.8% 30.6%; 45 | --destructive-foreground: 0 85.7% 97.3%; 46 | --border: 240 3.7% 15.9%; 47 | --input: 240 3.7% 15.9%; 48 | --ring: 346.8 77.2% 49.8%; 49 | } 50 | } 51 | 52 | @layer base { 53 | * { 54 | @apply border-border; 55 | } 56 | body { 57 | @apply bg-background text-foreground; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/features/todo/hooks/use-todo-query.ts: -------------------------------------------------------------------------------- 1 | import { createQueryKeys } from '@lukemorales/query-key-factory'; 2 | import { useMutation, useQuery } from '@tanstack/react-query'; 3 | import { toast } from 'react-hot-toast'; 4 | 5 | import todoApi from '../services/todo.api'; 6 | import { ResponseData, TodoData } from '../services/types'; 7 | import { QueryOptions } from '@/ts/types'; 8 | 9 | const todos = createQueryKeys('todos', { 10 | list: () => ({ 11 | queryKey: ['todos'], 12 | queryFn: () => todoApi.getList(), 13 | }), 14 | detail: (id: string) => ({ 15 | queryKey: [id], 16 | queryFn: () => todoApi.getDetail(id), 17 | }), 18 | }); 19 | 20 | export const useTodoListQuery = ( 21 | options: QueryOptions = {}, 22 | ) => { 23 | return useQuery({ 24 | ...todos.list(), 25 | keepPreviousData: true, 26 | ...options, 27 | }); 28 | }; 29 | 30 | export const useTodoDetailQuery = ( 31 | id: string, 32 | options: QueryOptions = {}, 33 | ) => { 34 | return useQuery({ 35 | ...todos.detail(id), 36 | ...options, 37 | }); 38 | }; 39 | 40 | export const useAddTodoMutation = () => { 41 | return useMutation({ 42 | mutationFn: todoApi.add, 43 | onSuccess: () => { 44 | void toast.success('Create new Todo successfully'); 45 | }, 46 | onError: () => { 47 | void toast.error('Create new Todo failed'); 48 | }, 49 | }); 50 | }; 51 | 52 | export const useUpdateTodoMutation = () => { 53 | return useMutation({ 54 | mutationFn: todoApi.update, 55 | onSuccess: () => { 56 | void toast.success('Update Todo successfully'); 57 | }, 58 | onError: () => { 59 | void toast.error('Update Todo failed'); 60 | }, 61 | }); 62 | }; 63 | 64 | export const useDeleteTodoMutation = () => { 65 | return useMutation({ 66 | mutationFn: todoApi.delete, 67 | onSuccess: () => { 68 | void toast.success('Delete Todo successfully'); 69 | }, 70 | onError: () => { 71 | void toast.error('Delete Todo failed'); 72 | }, 73 | }); 74 | }; 75 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | darkMode: ['class'], 4 | content: [ 5 | './pages/**/*.{ts,tsx}', 6 | './components/**/*.{ts,tsx}', 7 | './app/**/*.{ts,tsx}', 8 | './src/**/*.{ts,tsx}', 9 | ], 10 | theme: { 11 | container: { 12 | center: true, 13 | padding: '2rem', 14 | screens: { 15 | '2xl': '1400px', 16 | }, 17 | }, 18 | extend: { 19 | colors: { 20 | border: 'hsl(var(--border))', 21 | input: 'hsl(var(--input))', 22 | ring: 'hsl(var(--ring))', 23 | background: 'hsl(var(--background))', 24 | foreground: 'hsl(var(--foreground))', 25 | primary: { 26 | DEFAULT: 'hsl(var(--primary))', 27 | foreground: 'hsl(var(--primary-foreground))', 28 | }, 29 | secondary: { 30 | DEFAULT: 'hsl(var(--secondary))', 31 | foreground: 'hsl(var(--secondary-foreground))', 32 | }, 33 | destructive: { 34 | DEFAULT: 'hsl(var(--destructive))', 35 | foreground: 'hsl(var(--destructive-foreground))', 36 | }, 37 | muted: { 38 | DEFAULT: 'hsl(var(--muted))', 39 | foreground: 'hsl(var(--muted-foreground))', 40 | }, 41 | accent: { 42 | DEFAULT: 'hsl(var(--accent))', 43 | foreground: 'hsl(var(--accent-foreground))', 44 | }, 45 | popover: { 46 | DEFAULT: 'hsl(var(--popover))', 47 | foreground: 'hsl(var(--popover-foreground))', 48 | }, 49 | card: { 50 | DEFAULT: 'hsl(var(--card))', 51 | foreground: 'hsl(var(--card-foreground))', 52 | }, 53 | }, 54 | borderRadius: { 55 | lg: 'var(--radius)', 56 | md: 'calc(var(--radius) - 2px)', 57 | sm: 'calc(var(--radius) - 4px)', 58 | }, 59 | keyframes: { 60 | 'accordion-down': { 61 | from: { height: 0 }, 62 | to: { height: 'var(--radix-accordion-content-height)' }, 63 | }, 64 | 'accordion-up': { 65 | from: { height: 'var(--radix-accordion-content-height)' }, 66 | to: { height: 0 }, 67 | }, 68 | }, 69 | animation: { 70 | 'accordion-down': 'accordion-down 0.2s ease-out', 71 | 'accordion-up': 'accordion-up 0.2s ease-out', 72 | }, 73 | }, 74 | }, 75 | plugins: [require('tailwindcss-animate')], 76 | }; 77 | -------------------------------------------------------------------------------- /src/data/constant/navs.tsx: -------------------------------------------------------------------------------- 1 | import { cloneDeep } from 'lodash'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | import { HOME_PATH, USER_PATH, TODO_PATH } from './path'; 5 | import { TypeNavs, TypeRoutes } from './type-navs'; 6 | import { Home, Users, Todos } from '@/pages'; 7 | import { capitalizeFirstLetter } from '@/utils'; 8 | 9 | const navs: TypeNavs[] = [ 10 | { 11 | key: HOME_PATH, 12 | label: 'home', 13 | element: , 14 | }, 15 | { 16 | key: USER_PATH, 17 | label: 'user', 18 | element: , 19 | }, 20 | { 21 | key: TODO_PATH, 22 | label: 'todo', 23 | element: , 24 | }, 25 | ]; 26 | 27 | const getRoutes = (arr: TypeRoutes[], nav: TypeNavs, basePath = '') => { 28 | if (nav.children) { 29 | for (const n of nav.children) { 30 | getRoutes(arr, n, basePath + nav.key); 31 | } 32 | } 33 | if (!nav.element) return; 34 | 35 | arr.push({ 36 | path: basePath + nav.key, 37 | // element: nav.element && ( 38 | // {nav.element} 39 | // ), 40 | element: nav.element, 41 | }); 42 | 43 | return arr; 44 | }; 45 | 46 | const addLink = (nav: TypeNavs, path: string) => { 47 | return nav.children ? ( 48 | capitalizeFirstLetter(nav.label as string) 49 | ) : ( 50 | {capitalizeFirstLetter(nav.label as string)} 51 | ); 52 | }; 53 | 54 | const getShowNavigation = ( 55 | nav: TypeNavs, 56 | basePath = '', 57 | ): TypeNavs | undefined => { 58 | if (!nav.label) return; 59 | if (nav.children) { 60 | const arr: TypeNavs[] = []; 61 | for (const n of nav.children) { 62 | const formatN = getShowNavigation(n, basePath + nav.key); 63 | if (formatN) arr.push(formatN); 64 | } 65 | 66 | nav.children = arr.length > 0 ? arr : undefined; 67 | } 68 | 69 | return { 70 | key: basePath + nav.key, 71 | label: addLink(nav, basePath + nav.key), 72 | children: nav.children, 73 | element: nav.element, 74 | }; 75 | }; 76 | 77 | const menuList: TypeNavs[] = []; 78 | const routeList: TypeRoutes[] = []; 79 | const navList: TypeNavs[] = navs.map((nav) => ({ 80 | key: nav.key, 81 | label: nav.label, 82 | })); 83 | 84 | for (const nav of navs) { 85 | const nav1 = cloneDeep(nav); 86 | const n = getShowNavigation(nav1); 87 | n && menuList.push(n); 88 | 89 | const nav2 = cloneDeep(nav); 90 | getRoutes(routeList, nav2); 91 | } 92 | 93 | export { routeList, menuList, navList }; 94 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | node: true, 4 | browser: true, 5 | es6: true, 6 | }, 7 | ignorePatterns: ['./tsconfig.json'], 8 | extends: [ 9 | 'eslint:recommended', 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:jsx-a11y/recommended', 12 | 'plugin:react/recommended', 13 | 'plugin:prettier/recommended', 14 | 'plugin:testing-library/react', 15 | 'plugin:react-hooks/recommended', 16 | 'plugin:prettier/recommended', 17 | 'plugin:@typescript-eslint/recommended-requiring-type-checking', 18 | ], 19 | overrides: [ 20 | { 21 | env: { 22 | node: true, 23 | }, 24 | files: ['.eslintrc.{js,cjs}'], 25 | parserOptions: { 26 | sourceType: 'script', 27 | }, 28 | }, 29 | ], 30 | parser: '@typescript-eslint/parser', 31 | parserOptions: { 32 | ecmaVersion: 'latest', 33 | sourceType: 'module', 34 | project: ['./tsconfig.json', './.eslintrc.cjs'], 35 | tsconfigRootDir: __dirname, 36 | }, 37 | plugins: ['@typescript-eslint', 'import', 'simple-import-sort'], 38 | rules: { 39 | 'no-console': 2, 40 | 'react-hooks/rules-of-hooks': 2, 41 | 'react-hooks/exhaustive-deps': 2, 42 | 'react/no-array-index-key': 2, 43 | 'react/react-in-jsx-scope': 'off', 44 | 'react/prop-types': 'off', 45 | '@typescript-eslint/no-non-null-assertion': 0, 46 | 'no-unused-vars': 'off', 47 | '@typescript-eslint/no-misused-promises': 'off', 48 | '@typescript-eslint/no-unused-vars': [ 49 | 2, 50 | { 51 | argsIgnorePattern: '^_', 52 | varsIgnorePattern: '^_', 53 | ignoreRestSiblings: true, 54 | }, 55 | ], 56 | 'prettier/prettier': ['off', { singleQuote: true }], 57 | 'no-restricted-imports': [ 58 | 2, 59 | { 60 | patterns: [ 61 | '@/features/*/*', 62 | '@/components/*', 63 | '@/hooks/*', 64 | '@/utils/*', 65 | '@/ts/*/*', 66 | ], 67 | }, 68 | ], 69 | 'import/order': [ 70 | 'error', 71 | { 72 | groups: ['builtin', 'external', 'internal'], 73 | pathGroups: [ 74 | { 75 | pattern: 'react', 76 | group: 'external', 77 | position: 'before', 78 | }, 79 | ], 80 | pathGroupsExcludedImportTypes: ['react'], 81 | 'newlines-between': 'always', 82 | alphabetize: { 83 | order: 'asc', 84 | caseInsensitive: true, 85 | }, 86 | }, 87 | ], 88 | 'no-implied-eval': 'off', 89 | 'require-await': 'off', 90 | }, 91 | settings: { 92 | react: { 93 | version: 'detect', 94 | }, 95 | }, 96 | }; 97 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-template-for-starter", 3 | "author": "NuiCoder", 4 | "private": true, 5 | "version": "0.0.0", 6 | "type": "module", 7 | "scripts": { 8 | "dev": "vite", 9 | "preinstall": "npx only-allow pnpm", 10 | "build": "tsc && vite build", 11 | "lint": "eslint . --ext js,ts,tsx", 12 | "format": "prettier --write **/*.{js,ts,tsx} && eslint . --ext js,ts,tsx --fix", 13 | "preview": "vite preview", 14 | "postinstall": "bash postinstall.sh", 15 | "prepare": "husky install" 16 | }, 17 | "husky": { 18 | "hooks": { 19 | "pre-commint": "lint-staged" 20 | } 21 | }, 22 | "lint-staged": { 23 | "src/**/*": [ 24 | "eslint --ext ./src --fix" 25 | ], 26 | "./src/**": [ 27 | "prettier --write ." 28 | ] 29 | }, 30 | "resolutions": { 31 | "styled-components": "^5" 32 | }, 33 | "dependencies": { 34 | "@emotion/babel-plugin": "^11.11.0", 35 | "@emotion/react": "^11.11.1", 36 | "@emotion/styled": "^11.11.0", 37 | "@hookform/resolvers": "^3.3.1", 38 | "@lukemorales/query-key-factory": "^1.3.2", 39 | "@mui/icons-material": "^5.14.18", 40 | "@mui/material": "^5.14.17", 41 | "@tanstack/react-query": "^4.35.3", 42 | "@types/styled-components": "^5.1.27", 43 | "axios": "^1.5.0", 44 | "class-variance-authority": "^0.7.0", 45 | "clsx": "^2.0.0", 46 | "lodash": "^4.17.21", 47 | "lucide-react": "^0.279.0", 48 | "react": "^18.2.0", 49 | "react-dom": "^18.2.0", 50 | "react-error-boundary": "^4.0.11", 51 | "react-hook-form": "^7.47.0", 52 | "react-hot-toast": "^2.4.1", 53 | "react-icons": "^4.11.0", 54 | "react-router-dom": "^6.16.0", 55 | "styled-components": "^6.0.8", 56 | "tailwind-merge": "^1.14.0", 57 | "tailwindcss-animate": "^1.0.7", 58 | "zod": "^3.22.2", 59 | "zustand": "^4.4.1" 60 | }, 61 | "devDependencies": { 62 | "@optimize-lodash/rollup-plugin": "^4.0.4", 63 | "@types/lodash": "^4.14.198", 64 | "@types/node": "^20.6.2", 65 | "@types/react": "^18.2.15", 66 | "@types/react-dom": "^18.2.7", 67 | "@typescript-eslint/eslint-plugin": "^6.7.2", 68 | "@typescript-eslint/parser": "^6.7.2", 69 | "@vitejs/plugin-react": "^4.0.3", 70 | "autoprefixer": "^10.4.15", 71 | "eslint": "^8.45.0", 72 | "eslint-config-prettier": "^9.0.0", 73 | "eslint-plugin-import": "^2.28.1", 74 | "eslint-plugin-jsx-a11y": "^6.7.1", 75 | "eslint-plugin-prettier": "^5.0.0", 76 | "eslint-plugin-react": "^7.33.2", 77 | "eslint-plugin-react-hooks": "^4.6.0", 78 | "eslint-plugin-react-refresh": "^0.4.3", 79 | "eslint-plugin-simple-import-sort": "^10.0.0", 80 | "eslint-plugin-testing-library": "^6.0.1", 81 | "husky": "^8.0.3", 82 | "lint-staged": "^14.0.1", 83 | "postcss": "^8.4.30", 84 | "prettier": "^3.0.3", 85 | "tailwindcss": "^3.3.3", 86 | "typescript": "^5.0.2", 87 | "vite": "^4.4.5" 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/components/common/button/button-theme.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from '@mui/material/styles'; 2 | import Switch, { SwitchProps } from '@mui/material/Switch'; 3 | 4 | import { useThemeStore } from '@/hooks'; 5 | 6 | const MaterialUISwitch = styled((props: SwitchProps) => )( 7 | ({ theme }) => ({ 8 | width: 62, 9 | height: 34, 10 | padding: 7, 11 | '& .MuiSwitch-switchBase': { 12 | margin: 1, 13 | padding: 0, 14 | transform: 'translateX(6px)', 15 | '&.Mui-checked': { 16 | color: '#fff', 17 | transform: 'translateX(22px)', 18 | '& .MuiSwitch-thumb:before': { 19 | backgroundImage: `url('data:image/svg+xml;utf8,')`, 22 | }, 23 | '& + .MuiSwitch-track': { 24 | opacity: 1, 25 | backgroundColor: 26 | theme.palette.mode === 'dark' ? '#8796A5' : '#aab4be', 27 | }, 28 | }, 29 | }, 30 | '& .MuiSwitch-thumb': { 31 | backgroundColor: theme.palette.mode === 'dark' ? '#003892' : '#001e3c', 32 | width: 32, 33 | height: 32, 34 | '&:before': { 35 | content: "''", 36 | position: 'absolute', 37 | width: '100%', 38 | height: '100%', 39 | left: 0, 40 | top: 0, 41 | backgroundRepeat: 'no-repeat', 42 | backgroundPosition: 'center', 43 | backgroundImage: `url('data:image/svg+xml;utf8,')`, 46 | }, 47 | }, 48 | '& .MuiSwitch-track': { 49 | opacity: 1, 50 | backgroundColor: theme.palette.mode === 'dark' ? '#8796A5' : '#aab4be', 51 | borderRadius: 20 / 2, 52 | }, 53 | }), 54 | ); 55 | 56 | export function ButtonTheme() { 57 | const setTheme = useThemeStore((state) => state.setTheme); 58 | return ( 59 | 63 | e.target.checked 64 | ? setTheme({ theme: 'dark' }) 65 | : setTheme({ theme: 'light' }) 66 | } 67 | /> 68 | ); 69 | } 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Boilerplate and Starter for React JS 18+, Material-UI, Tailwind CSS v3.3 and Typescript 2 | 3 |

4 | React js starter banner 5 |

6 | 7 | 🚀🚀🚀 Boilerplate and Starter for React.js, Material-UI, Tailwind CSS and TypeScript ⚡️ Made with developer experience first: React.js, TypeScript, Axios, ESLint, Prettier, Husky, Lint-Staged, VSCode, PostCSS, Tailwind CSS. 8 | 9 | Clone this project and use it to create your own [React.js](https://nextjs.org) project. 10 | 11 | ### Features 12 | 13 | - ⚡ [React.js](https://react.dev/) 14 | - ⚡ [Material-UI](https://mui.com/) 15 | - 🔥 Type checking [TypeScript](https://www.typescriptlang.org) 16 | - 💎 Integrate with [Tailwind CSS](https://tailwindcss.com) 17 | - ✅ Strict Mode for TypeScript and React 18 18 | - 📏 Linter with [ESLint](https://eslint.org) (default NextJS, NextJS Core Web Vitals, Tailwind CSS and Airbnb configuration) 19 | - 💖 Code Formatter with [Prettier](https://prettier.io) 20 | - 🦊 Husky for Git Hooks 21 | - 🚫 Lint-staged for running linters on Git staged files 22 | - 🗂 VSCode configuration: Debug, Settings, Tasks and extension for PostCSS, ESLint, Prettier, TypeScript, Jest 23 | 24 | ### Requirements 25 | 26 | - Node.js 16+ and pnpm 27 | 28 | ### Getting started 29 | 30 | Run the following command on your local environment: 31 | 32 | ```shell 33 | git clone --depth=1 https://github.com/sonht113/react-boilerplate-for-starter.git 34 | cd my-project-name 35 | pnpm install 36 | ``` 37 | 38 | Then, you can run locally in development mode with live reload: 39 | 40 | ```shell 41 | pnpm run dev 42 | ``` 43 | 44 | Open http://localhost:5173 with your favorite browser to see your project. 45 | 46 | ```shell 47 | . 48 | ├── README.md # README file 49 | ├── .github # GitHub folder 50 | ├── .husky # Husky configuration 51 | ├── public # Public assets folder 52 | ├── src 53 | │ ├── apis # Common apis folder 54 | │ ├── components # Component folder 55 | │ ├── data # Data constants JS Pages 56 | │ └── features # Features folder 57 | │ ├── hooks # Hooks customs folder 58 | │ ├── layout # Layout Pages 59 | │ └── pages # React JS Pages 60 | │ ├── provider # Provider folder 61 | │ └── routes # Routes folder 62 | │ ├── ts # Type and Enum folder 63 | │ ├── utils # Utility functions 64 | ├── tailwind.config.js # Tailwind CSS configuration 65 | └── tsconfig.json # TypeScript configuration 66 | ``` 67 | 68 | ### Customization 69 | 70 | - `src/index.css`: your CSS file using Tailwind CSS 71 | - `src/main.tsx`: default theme 72 | 73 | You have access to the whole code source if you need further customization. The provided code is only example for you to start your project. The sky is the limit 🚀. 74 | 75 | --- 76 | 77 | Made with ♥ by [TrongSon](https://www.facebook.com/profile.php?id=100032736788526&locale=vi_VN) 78 | -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pages/todos/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useMemo } from 'react'; 2 | 3 | import AddIcon from '@mui/icons-material/Add'; 4 | import { 5 | Backdrop, 6 | Box, 7 | Button, 8 | Card, 9 | CardContent, 10 | CardHeader, 11 | Checkbox, 12 | CircularProgress, 13 | FormControl, 14 | FormControlLabel, 15 | FormHelperText, 16 | IconButton, 17 | Modal, 18 | TextField, 19 | Typography, 20 | } from '@mui/material'; 21 | import { Controller, useForm } from 'react-hook-form'; 22 | import { FiTrash2 } from 'react-icons/fi'; 23 | 24 | import { 25 | TodoDataMutation, 26 | useAddTodoMutation, 27 | useDeleteTodoMutation, 28 | useTodoListQuery, 29 | useUpdateTodoMutation, 30 | } from '@/features/todo'; 31 | import { useModalStore } from '@/hooks'; 32 | 33 | type Input = { 34 | todoName: string; 35 | }; 36 | 37 | const Todos: FC = () => { 38 | const { 39 | control, 40 | handleSubmit, 41 | reset, 42 | formState: { errors }, 43 | } = useForm(); 44 | 45 | const isOpen = useModalStore((state) => state.isOpen); 46 | const open = useModalStore((state) => state.open); 47 | const close = useModalStore((state) => state.close); 48 | 49 | const { data: todos, refetch, isLoading: loadingFetch } = useTodoListQuery(); 50 | const { mutate: addTodo, isLoading: loadingCreate } = useAddTodoMutation(); 51 | const { mutate: updateTodo, isLoading: loadingUpdate } = 52 | useUpdateTodoMutation(); 53 | const { mutate: deleteTodo, isLoading: loadingDelete } = 54 | useDeleteTodoMutation(); 55 | 56 | const todoList = useMemo(() => { 57 | if(todos && todos.data) { 58 | return todos?.data.sort((a, b) => { 59 | if (a.isComplete && !b.isComplete) { 60 | return 1; // Move 'a' to the end 61 | } else if (!a.isComplete && b.isComplete) { 62 | return -1; // Keep 'a' before 'b' 63 | } else { 64 | return 0; // Maintain the order of other elements 65 | } 66 | }); 67 | } 68 | }, [todos]); 69 | 70 | const handleCreateTodo = (body: TodoDataMutation) => { 71 | return addTodo(body, { 72 | onSuccess: () => { 73 | void refetch(); 74 | void reset({ todoName: '' }); 75 | void close(); 76 | }, 77 | }); 78 | }; 79 | 80 | const handleUpdateTodo = (body: { id: string; data: TodoDataMutation }) => { 81 | return updateTodo(body, { 82 | onSuccess: () => { 83 | void refetch(); 84 | }, 85 | }); 86 | }; 87 | 88 | const handleDeleteTodo = (id: string) => { 89 | return deleteTodo(id, { 90 | onSuccess: () => { 91 | void refetch(); 92 | }, 93 | }); 94 | }; 95 | 96 | return ( 97 | 98 | theme.zIndex.drawer + 1 }} 100 | open={loadingDelete || loadingUpdate} 101 | > 102 | 103 | 104 | 111 | <> 112 | theme.zIndex.drawer + 1 }} 114 | open={loadingCreate} 115 | > 116 | 117 | 118 | 123 | ( 127 | 128 | 134 | {errors.todoName && ( 135 | 136 | {errors.todoName.message} 137 | 138 | )} 139 | 140 | )} 141 | name="todoName" 142 | /> 143 |
144 | 147 |
148 |
149 | 150 |
151 | 152 | 156 | 157 | 158 | } 159 | /> 160 | 161 | {loadingFetch && ( 162 |
163 | 164 |
165 | )} 166 | { 167 | !loadingFetch &&!todoList && ( 168 |
169 | No data 170 |
171 | ) 172 | } 173 | {todoList && 174 | todoList.map((todo) => ( 175 |
176 | 183 | {todo.todoName} 184 | 185 | } 186 | control={ 187 | 191 | handleUpdateTodo({ 192 | id: todo._id, 193 | data: { isComplete: true }, 194 | }) 195 | } 196 | /> 197 | } 198 | /> 199 | handleDeleteTodo(todo._id)}> 200 | 201 | 202 |
203 | ))} 204 |
205 |
206 |
207 | ); 208 | }; 209 | 210 | export default Todos; 211 | --------------------------------------------------------------------------------