├── .dockerignore ├── .env.development ├── .env.production ├── .gitattributes ├── .gitignore ├── .husky ├── pre-commit └── pre-push ├── .npmrc ├── .nvmrc ├── .prettierignore ├── Dockerfile ├── README.md ├── eslint.config.mjs ├── lint-staged.config.mjs ├── lint ├── general.eslint.mjs ├── import.eslint.mjs ├── javascript.eslint.mjs ├── prettier.eslint.mjs ├── react.eslint.mjs ├── typescript.eslint.mjs └── utils.eslint.mjs ├── next.config.ts ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── prettier.config.mjs ├── public ├── favicon.ico ├── manifest.json └── robots.txt ├── scripts └── lint.sh ├── src ├── api │ ├── appError.ts │ ├── axios.ts │ ├── catchAsync.ts │ ├── endpoints.ts │ ├── index.ts │ └── utils.ts ├── app │ ├── (auth) │ │ ├── layout.ts │ │ └── login │ │ │ └── page.tsx │ ├── (public) │ │ └── about │ │ │ └── page.tsx │ ├── api │ │ ├── (protected) │ │ │ ├── user │ │ │ │ └── profile │ │ │ │ │ ├── logout │ │ │ │ │ └── route.ts │ │ │ │ │ └── me │ │ │ │ │ └── route.ts │ │ │ └── users │ │ │ │ └── route.ts │ │ ├── _ │ │ │ ├── constants │ │ │ │ └── index.ts │ │ │ ├── functions │ │ │ │ ├── getBody.ts │ │ │ │ ├── getUserData.ts │ │ │ │ ├── sendError.ts │ │ │ │ └── sendRes.ts │ │ │ ├── mock │ │ │ │ └── users.ts │ │ │ └── types │ │ │ │ └── index.ts │ │ ├── auth │ │ │ └── login │ │ │ │ └── route.ts │ │ └── route.ts │ ├── app.tsx │ ├── app │ │ ├── layout.ts │ │ ├── logout │ │ │ └── page.tsx │ │ ├── page.tsx │ │ └── users │ │ │ └── page.tsx │ ├── error.tsx │ ├── global-error.tsx │ ├── layout.tsx │ ├── not-found.tsx │ ├── page.tsx │ └── providers.tsx ├── assets │ └── images │ │ └── next.svg ├── components │ ├── AppClient.tsx │ ├── dashboard │ │ ├── Dashboard.module.css │ │ └── Dashboard.tsx │ ├── defaults │ │ ├── error │ │ │ ├── Error.module.css │ │ │ └── Error.tsx │ │ └── notFound │ │ │ ├── NotFound.module.css │ │ │ └── NotFound.tsx │ ├── header │ │ ├── Header.module.css │ │ └── Header.tsx │ ├── login │ │ ├── Login.module.css │ │ └── Login.tsx │ ├── logout │ │ └── Logout.tsx │ └── users │ │ ├── Users.module.css │ │ └── Users.tsx ├── config │ └── index.ts ├── constants │ └── index.ts ├── features │ ├── auth │ │ ├── auth.api.ts │ │ ├── auth.type.ts │ │ └── useLogin.ts │ ├── profile │ │ ├── profile.api.ts │ │ ├── profile.type.ts │ │ ├── useLogout.ts │ │ └── useProfile.ts │ └── users │ │ ├── useUsers.ts │ │ ├── users.api.ts │ │ └── users.type.ts ├── hooks │ ├── useRouter.ts │ ├── useStore.ts │ └── useZustandState.ts ├── lib │ ├── cookieStore.ts │ ├── isAuthenticated.ts │ └── queryClient.ts ├── middleware.ts ├── providers │ └── ZustandProvider.tsx ├── shared │ └── loader │ │ ├── Loader.module.css │ │ ├── Loader.tsx │ │ ├── ProgressBar.tsx │ │ └── WebsiteLoader.tsx ├── store │ ├── index.ts │ └── slices │ │ ├── auth │ │ ├── auth.slice.ts │ │ └── auth.type.ts │ │ ├── loading │ │ ├── loading.slice.ts │ │ └── loading.type.ts │ │ └── theme │ │ ├── theme.slice.ts │ │ └── theme.type.ts ├── styles │ ├── font.ts │ └── globals.css ├── types │ ├── axios.type.ts │ ├── cookieStore.type.ts │ ├── index.ts │ ├── store.type.ts │ └── zustandState.type.ts └── utils │ └── index.ts └── tsconfig.json /.dockerignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules 3 | .pnp 4 | .pnp.js 5 | 6 | # testing 7 | coverage 8 | 9 | # production 10 | build 11 | dist 12 | 13 | # misc 14 | .DS_Store 15 | *.pem 16 | *.crt 17 | *.key 18 | 19 | # debug 20 | *.log 21 | 22 | # typescript 23 | *.tsbuildinfo 24 | 25 | # editor folders 26 | .idea 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # ignore folders 32 | .git 33 | .husky 34 | 35 | # npm files 36 | .npmrc 37 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | NEXT_TELEMETRY_DISABLED=1 2 | 3 | NEXT_PUBLIC_API_PATH=http://localhost:3000/api -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | NEXT_TELEMETRY_DISABLED=1 2 | 3 | NEXT_PUBLIC_API_PATH=https://nextjs-fullstack-typescript-boilerplate.vercel.app/api -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Project files 2 | .dockerignore text eol=lf 3 | .env* text eol=lf 4 | .gitattributes text eol=lf 5 | .gitignore text eol=lf 6 | .npmrc text eol=lf 7 | .nvmrc text eol=lf 8 | .prettierignore text eol=lf 9 | Dockerfile text eol=lf 10 | 11 | # Global text-based files 12 | *.{css,js,cjs,mjs,jsx,ts,cts,mts,tsx,json,yaml,yml,sh,md,txt,svg} text eol=lf 13 | 14 | # Binary files 15 | *.{ico} binary 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules 3 | .pnp 4 | .pnp.js 5 | 6 | # testing 7 | coverage 8 | 9 | # production 10 | build 11 | dist 12 | 13 | # misc 14 | .DS_Store 15 | *.pem 16 | *.crt 17 | *.key 18 | 19 | # debug 20 | *.log 21 | 22 | # vercel 23 | .vercel 24 | 25 | # next.js 26 | .next 27 | out 28 | next-env.d.ts 29 | 30 | # typescript 31 | *.tsbuildinfo 32 | 33 | # editor folders 34 | .idea 35 | 36 | # local env files 37 | .env*.local 38 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | pnpm lint-staged 2 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | pnpm --silent script:lint --for-check 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | script-shell=bash 2 | engine-strict=true 3 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 22 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | pnpm-lock.yaml 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Set Node.js version 2 | ARG NODE_VERSION=22 3 | 4 | # Base stage for dependency installation 5 | FROM node:${NODE_VERSION}-alpine AS base 6 | WORKDIR /app 7 | 8 | # Set ENVs 9 | ENV NODE_ENV=production 10 | 11 | # Copy only package manager files for caching 12 | COPY package.json pnpm-lock.yaml ./ 13 | 14 | # Install pnpm globally 15 | RUN npm install -g pnpm 16 | 17 | # Install only dependencies 18 | RUN pnpm install --prod --ignore-scripts --frozen-lockfile --prefer-offline 19 | 20 | # Build stage (Uses devDependencies) 21 | FROM base AS build 22 | 23 | # Install all dependencies, including devDependencies for build 24 | RUN pnpm install --prod=false --ignore-scripts --frozen-lockfile --prefer-offline 25 | 26 | # Copy all the files 27 | COPY . . 28 | 29 | # Execute build 30 | RUN pnpm build 31 | 32 | # Production stage image 33 | FROM node:${NODE_VERSION}-alpine AS final 34 | WORKDIR /app 35 | 36 | # Set ENVs 37 | ENV NODE_ENV=production 38 | 39 | # Copy required files from the images 40 | COPY --from=base /app/package.json package.json 41 | COPY --from=base /app/node_modules node_modules 42 | COPY --from=build /app/public public 43 | COPY --from=build /app/.next .next 44 | 45 | # Start script 46 | CMD ["npm", "start"] 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Project Name 2 | 3 | ## Prerequisites 4 | 5 | - Install `pnpm` globally. 6 | - Requires Node.js 22 or later. 7 | - Use only `pnpm`, no NPM or Yarn. 8 | - Keep ESLint & Prettier enabled. 9 | - Adhere to the coding rules as much as possible. 10 | 11 | ## Installation 12 | 13 | 1. Run `pnpm install`. 14 | 2. Start the project with `pnpm start` (use `pnpm start:dev` for development). 15 | 16 | ## Features 17 | 18 | - Fully typed with TypeScript. 19 | - Full-stack application. 20 | - React Query for server state and Zustand for client state. 21 | - Cookie based authentication & authorization flow. 22 | - Custom navigation loader. 23 | - Prettier & strict ESLint rules. 24 | - Husky and lint-staged for pre-commit validation. 25 | - Commits are blocked unless all rules pass (unless `--no-verify` is used). 26 | - Pre-configured Dockerfile & GitHub Actions workflow for DevOps. 27 | 28 | ## Credentials 29 | 30 | ``` 31 | Email: admin@example.com 32 | Password: admin 33 | ``` 34 | 35 | **Last updated by:** Nisharg Shah 36 | **Date:** 09/03/2025 37 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import customGeneralESLintConfig from './lint/general.eslint.mjs'; 2 | import customImportESLintConfig from './lint/import.eslint.mjs'; 3 | import customJSESLintConfig from './lint/javascript.eslint.mjs'; 4 | import customPrettierESLintConfig from './lint/prettier.eslint.mjs'; 5 | import customReactESLintConfig from './lint/react.eslint.mjs'; 6 | import customTSESLintConfig from './lint/typescript.eslint.mjs'; 7 | import { gitIgnoreFile } from './lint/utils.eslint.mjs'; 8 | 9 | export default [ 10 | gitIgnoreFile, 11 | ...customJSESLintConfig, 12 | ...customReactESLintConfig, 13 | ...customTSESLintConfig, 14 | ...customImportESLintConfig, 15 | ...customGeneralESLintConfig, 16 | ...customPrettierESLintConfig, 17 | ]; 18 | -------------------------------------------------------------------------------- /lint-staged.config.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('lint-staged').Configuration} 3 | */ 4 | export default { 5 | '*.{js,cjs,mjs,jsx,ts,cts,mts,tsx}': 'pnpm lint', 6 | }; 7 | -------------------------------------------------------------------------------- /lint/general.eslint.mjs: -------------------------------------------------------------------------------- 1 | const customGeneralESLintConfig = [ 2 | { 3 | name: 'x/general/rules', 4 | rules: { 5 | 'no-console': 'off', 6 | 'no-void': 'off', 7 | 'consistent-return': 'off', 8 | 'no-array-constructor': 'off', 9 | 'no-underscore-dangle': [ 10 | 'error', 11 | { 12 | allow: ['_id'], 13 | }, 14 | ], 15 | 'no-restricted-syntax': [ 16 | 'error', 17 | 'ForStatement', 18 | 'ContinueStatement', 19 | 'DoWhileStatement', 20 | 'WhileStatement', 21 | 'WithStatement', 22 | // React 23 | { 24 | selector: 'MemberExpression[object.name="React"]', 25 | message: 'Use of React.method is not allowed.', 26 | }, 27 | // React - TypeScript 28 | { 29 | selector: 'TSTypeReference[typeName.left.name="React"]', 30 | message: 'Use of React.type is not allowed.', 31 | }, 32 | ], 33 | }, 34 | }, 35 | { 36 | name: 'x/general/ts-only', 37 | files: ['**/*.{ts,cts,mts,tsx}'], 38 | rules: { 39 | 'no-restricted-imports': [ 40 | 'error', 41 | { 42 | patterns: [ 43 | { 44 | group: ['./*', '../*'], 45 | message: "Please use the absolute path '@/*' instead.", 46 | }, 47 | ], 48 | }, 49 | ], 50 | }, 51 | }, 52 | ]; 53 | 54 | export default customGeneralESLintConfig; 55 | -------------------------------------------------------------------------------- /lint/import.eslint.mjs: -------------------------------------------------------------------------------- 1 | import { rules } from 'eslint-config-airbnb-extended'; 2 | import unusedImportsPlugin from 'eslint-plugin-unused-imports'; 3 | 4 | const customImportESLintConfig = [ 5 | // Strict Import Rules 6 | rules.base.importsStrict, 7 | // Unused Import Config 8 | { 9 | name: 'unused-imports/config', 10 | plugins: { 11 | 'unused-imports': unusedImportsPlugin, 12 | }, 13 | rules: { 14 | 'unused-imports/no-unused-imports': 'error', 15 | }, 16 | }, 17 | // Disable Default Export for Features and Hooks 18 | { 19 | name: 'x/import-x/disable-default-export', 20 | files: ['**/features/**/**.api.ts', '**/use*.ts'], 21 | rules: { 22 | 'import-x/prefer-default-export': 'off', 23 | }, 24 | }, 25 | // Disable Extensions in Module Files 26 | { 27 | name: 'x/import-x/disable-extensions-in-module-files', 28 | files: ['**/*.mjs'], 29 | rules: { 30 | 'import-x/extensions': 'off', 31 | }, 32 | }, 33 | ]; 34 | 35 | export default customImportESLintConfig; 36 | -------------------------------------------------------------------------------- /lint/javascript.eslint.mjs: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js'; 2 | import { configs, plugins } from 'eslint-config-airbnb-extended'; 3 | import promisePlugin from 'eslint-plugin-promise'; 4 | import unicornPlugin from 'eslint-plugin-unicorn'; 5 | 6 | const customJSESLintConfig = [ 7 | // ESLint Recommended Rules 8 | { 9 | name: 'js/config', 10 | ...js.configs.recommended, 11 | }, 12 | // Stylistic Plugin 13 | plugins.stylistic, 14 | // Import X Plugin 15 | plugins.importX, 16 | // Airbnb Base Recommended Config 17 | ...configs.base.recommended, 18 | // Promise Config 19 | promisePlugin.configs['flat/recommended'], 20 | // Unicorn Config 21 | unicornPlugin.configs.recommended, 22 | // Unicorn Config Rules 23 | { 24 | name: 'x/unicorn/rules', 25 | rules: { 26 | 'unicorn/filename-case': [ 27 | 'error', 28 | { 29 | cases: { 30 | kebabCase: true, 31 | camelCase: true, 32 | pascalCase: true, 33 | }, 34 | multipleFileExtensions: false, 35 | }, 36 | ], 37 | 'unicorn/prevent-abbreviations': 'off', 38 | 'unicorn/no-null': 'off', 39 | 'unicorn/no-array-reduce': 'off', 40 | 'unicorn/consistent-function-scoping': 'off', 41 | }, 42 | }, 43 | ]; 44 | 45 | export default customJSESLintConfig; 46 | -------------------------------------------------------------------------------- /lint/prettier.eslint.mjs: -------------------------------------------------------------------------------- 1 | import { rules as prettierConfigRules } from 'eslint-config-prettier'; 2 | import prettierPlugin from 'eslint-plugin-prettier'; 3 | 4 | const customPrettierESLintConfig = [ 5 | // Prettier Plugin 6 | { 7 | name: 'prettier/plugin/config', 8 | plugins: { 9 | prettier: prettierPlugin, 10 | }, 11 | }, 12 | // Prettier Config 13 | { 14 | name: 'prettier/config', 15 | rules: { 16 | ...prettierConfigRules, 17 | 'prettier/prettier': 'error', 18 | }, 19 | }, 20 | ]; 21 | 22 | export default customPrettierESLintConfig; 23 | -------------------------------------------------------------------------------- /lint/react.eslint.mjs: -------------------------------------------------------------------------------- 1 | import tanstackQueryPlugin from '@tanstack/eslint-plugin-query'; 2 | import { configs, plugins, rules } from 'eslint-config-airbnb-extended'; 3 | 4 | const customReactESLintConfig = [ 5 | // React Plugin 6 | plugins.react, 7 | // React Hooks Plugin 8 | plugins.reactHooks, 9 | // React JSX A11y Plugin 10 | plugins.reactA11y, 11 | // Next Plugin 12 | plugins.next, 13 | // Airbnb Next Recommended Config 14 | ...configs.next.recommended, 15 | // Airbnb React Strict Rules 16 | rules.react.strict, 17 | // Tanstack Query Config 18 | ...tanstackQueryPlugin.configs['flat/recommended'], 19 | // JSX A11y Config Rules 20 | { 21 | name: 'x/jsx-a11y/rules', 22 | rules: { 23 | 'jsx-a11y/label-has-associated-control': 'off', 24 | }, 25 | }, 26 | ]; 27 | 28 | export default customReactESLintConfig; 29 | -------------------------------------------------------------------------------- /lint/typescript.eslint.mjs: -------------------------------------------------------------------------------- 1 | import { configs, plugins, rules } from 'eslint-config-airbnb-extended'; 2 | 3 | const customTSESLintConfig = [ 4 | // TypeScript ESLint Plugin 5 | plugins.typescriptEslint, 6 | // Airbnb Base TypeScript Config 7 | ...configs.base.typescript, 8 | // Airbnb Next TypeScript Config 9 | ...configs.next.typescript, 10 | // Airbnb TypeScript ESLint Strict Rules 11 | rules.typescript.typescriptEslintStrict, 12 | // Disable Return Type for Features Hook 13 | { 14 | name: 'x/typescript-eslint/features-hook-only', 15 | files: ['src/features/**/use*.ts'], 16 | rules: { 17 | '@typescript-eslint/explicit-module-boundary-types': 'off', 18 | }, 19 | }, 20 | ]; 21 | 22 | export default customTSESLintConfig; 23 | -------------------------------------------------------------------------------- /lint/utils.eslint.mjs: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | 3 | import { includeIgnoreFile } from '@eslint/compat'; 4 | 5 | export const projectRoot = path.resolve('.'); 6 | export const gitignorePath = path.resolve(projectRoot, '.gitignore'); 7 | 8 | export const gitIgnoreFile = includeIgnoreFile(gitignorePath); 9 | -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from 'next'; 2 | 3 | const nextConfig: NextConfig = { 4 | reactStrictMode: false, 5 | // FIXME: REMEMBER TO REMOVE IT, IF YOU NOT NEEDED 6 | images: { 7 | unoptimized: true, 8 | dangerouslyAllowSVG: true, 9 | remotePatterns: [ 10 | { 11 | protocol: 'https', 12 | hostname: '**', 13 | pathname: '**', 14 | }, 15 | ], 16 | }, 17 | }; 18 | 19 | export default nextConfig; 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "project-name", 3 | "version": "0.0.1", 4 | "private": true, 5 | "description": "project-description", 6 | "keywords": [ 7 | "project keywords" 8 | ], 9 | "license": "ISC", 10 | "author": "Nisharg Shah", 11 | "scripts": { 12 | "build": "next build", 13 | "dev": "next dev --turbopack", 14 | "format:check": "prettier . --check", 15 | "format:fix": "prettier . --write", 16 | "lint": "eslint .", 17 | "lint:fix": "pnpm --silent lint --fix", 18 | "prepare": "husky", 19 | "script:lint": "bash -e ./scripts/lint.sh", 20 | "start": "next start", 21 | "typecheck": "tsc --noEmit" 22 | }, 23 | "dependencies": { 24 | "@tanstack/react-query": "^5.79.0", 25 | "axios": "^1.9.0", 26 | "clsx": "^2.1.1", 27 | "cookies-next": "^5.1.0", 28 | "next": "^15.3.3", 29 | "next-nprogress-bar": "^2.4.7", 30 | "react": "^19.1.0", 31 | "react-dom": "^19.1.0", 32 | "react-hook-form": "^7.56.4", 33 | "react-toastify": "^11.0.5", 34 | "zustand": "^5.0.5" 35 | }, 36 | "devDependencies": { 37 | "@eslint/compat": "^1.2.9", 38 | "@eslint/js": "^9.28.0", 39 | "@next/eslint-plugin-next": "^15.3.3", 40 | "@stylistic/eslint-plugin": "^3.1.0", 41 | "@tanstack/eslint-plugin-query": "^5.78.0", 42 | "@tanstack/react-query-devtools": "^5.79.0", 43 | "@types/node": "^22.15.29", 44 | "@types/react": "^19.1.6", 45 | "@types/react-dom": "^19.1.5", 46 | "eslint": "^9.28.0", 47 | "eslint-config-airbnb-extended": "^1.0.11", 48 | "eslint-config-next": "^15.3.3", 49 | "eslint-config-prettier": "^10.1.5", 50 | "eslint-import-resolver-typescript": "^4.4.2", 51 | "eslint-plugin-import-x": "^4.15.0", 52 | "eslint-plugin-jsx-a11y": "^6.10.2", 53 | "eslint-plugin-prettier": "^5.4.1", 54 | "eslint-plugin-promise": "^7.2.1", 55 | "eslint-plugin-react": "^7.37.5", 56 | "eslint-plugin-react-hooks": "^5.2.0", 57 | "eslint-plugin-unicorn": "^59.0.1", 58 | "eslint-plugin-unused-imports": "^4.1.4", 59 | "husky": "^9.1.7", 60 | "lint-staged": "^16.1.0", 61 | "prettier": "^3.5.3", 62 | "prettier-plugin-packagejson": "^2.5.15", 63 | "type-fest": "^4.41.0", 64 | "typescript": "^5.8.3", 65 | "typescript-eslint": "^8.33.0" 66 | }, 67 | "packageManager": "pnpm@10.11.0", 68 | "engines": { 69 | "node": ">=22.0.0", 70 | "npm": "please-use-pnpm", 71 | "pnpm": ">=10.0.0", 72 | "yarn": "please-use-pnpm" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | onlyBuiltDependencies: 2 | - sharp 3 | - unrs-resolver 4 | -------------------------------------------------------------------------------- /prettier.config.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * @see https://prettier.io/docs/configuration 3 | * @type {import("prettier").Config} 4 | */ 5 | const config = { 6 | printWidth: 100, 7 | singleQuote: true, 8 | plugins: ['prettier-plugin-packagejson'], 9 | }; 10 | 11 | export default config; 12 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NishargShah/nextjs-fullstack-typescript-boilerplate/77562a7599c1f850750366100aa40bc780dbe95f/public/favicon.ico -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Project Name", 3 | "name": "Project Long Name", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /scripts/lint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | forCheck="--for-check" 4 | isForCheck="$([[ $1 == $forCheck ]] && echo 0 || echo 1)" 5 | 6 | echo "Started" 7 | 8 | if [ "$isForCheck" -eq 0 ]; then 9 | npm run --silent format:check -- --log-level silent 10 | echo "Prettier Checked" 11 | 12 | npm run --silent lint 13 | echo "ESLint Checked" 14 | 15 | npm run --silent typecheck 16 | echo "TypeScript Checked" 17 | else 18 | npm run --silent format:fix -- --log-level silent 19 | echo "Prettier Completed" 20 | 21 | npm run --silent lint:fix 22 | echo "ESLint Completed" 23 | 24 | npm run --silent typecheck 25 | echo "TypeScript Completed" 26 | fi 27 | 28 | echo "Done" -------------------------------------------------------------------------------- /src/api/appError.ts: -------------------------------------------------------------------------------- 1 | import type { RecursiveType } from '@/types'; 2 | 3 | export interface AppErrorType { 4 | message: string | string[] | (() => string | string[]); 5 | messages: string[] | undefined; 6 | statusCode: number; 7 | extraFields: RecursiveType; 8 | isOperational: boolean; 9 | } 10 | 11 | export class AppError extends Error { 12 | public messages: AppErrorType['messages']; 13 | 14 | public statusCode: AppErrorType['statusCode']; 15 | 16 | public status: AppErrorType['statusCode']; 17 | 18 | public extraFields: AppErrorType['extraFields']; 19 | 20 | public isOperational: AppErrorType['isOperational']; 21 | 22 | static getMessage = (message: AppErrorType['message']): string => { 23 | if (typeof message === 'function') return message() as string; 24 | return Array.isArray(message) ? message[0] : message; 25 | }; 26 | 27 | constructor( 28 | message: AppErrorType['message'], 29 | statusCode: AppErrorType['statusCode'], 30 | extraFields: AppErrorType['extraFields'] = {}, 31 | ) { 32 | super(); 33 | this.message = AppError.getMessage(message); 34 | this.messages = Array.isArray(message) ? message : undefined; 35 | this.statusCode = statusCode || 400; 36 | this.status = statusCode; 37 | this.extraFields = extraFields; 38 | this.isOperational = true; 39 | 40 | if ('captureStackTrace' in Error) Error.captureStackTrace(this, this.constructor); 41 | } 42 | } 43 | 44 | export const isAppError = (AppErrorArg: unknown): AppErrorArg is AppError => 45 | AppErrorArg instanceof AppError; 46 | -------------------------------------------------------------------------------- /src/api/axios.ts: -------------------------------------------------------------------------------- 1 | import axiosInstance from 'axios'; 2 | import { toast } from 'react-toastify'; 3 | 4 | import { showToast } from '@/api/utils'; 5 | import config from '@/config'; 6 | import cookieStore from '@/lib/cookieStore'; 7 | import { isServer } from '@/utils'; 8 | 9 | import type { AxiosError } from 'axios'; 10 | 11 | import type { AxiosErr } from '@/api/utils'; 12 | import type { InternalAxiosRequestConfigWithExtraProps } from '@/types/axios.type'; 13 | 14 | const axios = axiosInstance.create({ baseURL: config.NEXT_PUBLIC_API_PATH, withCredentials: true }); 15 | 16 | axios.interceptors.request.use( 17 | async (conf: InternalAxiosRequestConfigWithExtraProps) => { 18 | const myConfig = { ...conf }; 19 | 20 | const lang = myConfig.headers['Accept-Language']; 21 | if (!isServer && !lang) myConfig.headers['Accept-Language'] = 'en'; 22 | 23 | myConfig.url = Object.entries(conf.urlParams ?? {}).reduce((acc, [k, v]) => { 24 | let temp = acc; 25 | temp = temp.replace(`:${k}`, v.toString()); 26 | 27 | return temp; 28 | }, myConfig.url ?? ''); 29 | 30 | if (myConfig.data instanceof FormData) { 31 | myConfig.headers['Content-Type'] = 'multipart/form-data'; 32 | } 33 | 34 | if (isServer && myConfig.ssr !== false) { 35 | myConfig.headers.Cookie = await cookieStore.getAllSerialized(); 36 | } 37 | 38 | return myConfig; 39 | }, 40 | async (error: AxiosError) => { 41 | if (!isServer) console.debug('Request Error', error); 42 | throw error; 43 | }, 44 | ); 45 | 46 | axios.interceptors.response.use( 47 | (res) => { 48 | if (!isServer) showToast(res); 49 | return res; 50 | }, 51 | async (error: AxiosErr) => { 52 | if (!isServer) { 53 | if (error.code === 'ERR_NETWORK') { 54 | toast.error(error.message); 55 | throw error; 56 | } 57 | 58 | if ( 59 | error.response && 60 | [401, 403].includes(error.response.status) && 61 | globalThis.location.pathname !== '/app/logout' 62 | ) { 63 | globalThis.location.assign('/app/logout'); 64 | } 65 | 66 | showToast(error); 67 | console.debug('Response Error', error); 68 | } 69 | 70 | throw error; 71 | }, 72 | ); 73 | 74 | export default axios; 75 | -------------------------------------------------------------------------------- /src/api/catchAsync.ts: -------------------------------------------------------------------------------- 1 | import { throwAxiosError } from '@/api/utils'; 2 | 3 | import type { Promisable } from 'type-fest'; 4 | 5 | import type { AxiosSignal, PaginatedOutput } from '@/types/axios.type'; 6 | 7 | type DefaultParamsInput = AxiosSignal; 8 | 9 | interface CatchAsyncOptions { 10 | throwError?: boolean; 11 | } 12 | 13 | type CatchAsyncOutputCond = P extends true ? PaginatedOutput : P extends false ? T[] : T; 14 | 15 | type CatchAsyncInput = ( 16 | data: T, 17 | options?: CatchAsyncOptions, 18 | ) => Promise>; 19 | 20 | type CatchAsyncOutput = ( 21 | data: T, 22 | options?: CatchAsyncOptions, 23 | ) => Promise | E | null>; 24 | 25 | type ErrorCB = (error: unknown) => Promisable; 26 | 27 | type CatchAsync = ( 28 | fn: CatchAsyncInput, 29 | errorCB?: ErrorCB, 30 | ) => CatchAsyncOutput; 31 | 32 | const catchAsync: CatchAsync = 33 | (fn, errorCB?) => 34 | async (data, options = {}) => { 35 | const { throwError = true } = options; 36 | 37 | try { 38 | return await fn(data, options); 39 | } catch (error) { 40 | if (throwError) throwAxiosError(error); 41 | return errorCB ? errorCB(error) : null; 42 | } 43 | }; 44 | 45 | export default catchAsync; 46 | -------------------------------------------------------------------------------- /src/api/endpoints.ts: -------------------------------------------------------------------------------- 1 | import type { Endpoints } from '@/types/axios.type'; 2 | 3 | const endpoints = { 4 | login: { 5 | method: 'POST', 6 | url: '/auth/login', 7 | manageToast: (res) => !!res.message, 8 | }, 9 | getProfile: { 10 | method: 'GET', 11 | url: '/user/profile/me', 12 | }, 13 | logout: { 14 | method: 'POST', 15 | url: '/user/profile/logout', 16 | }, 17 | getAllUsers: { 18 | method: 'GET', 19 | url: '/users', 20 | }, 21 | } satisfies Endpoints; 22 | 23 | export default endpoints; 24 | -------------------------------------------------------------------------------- /src/api/index.ts: -------------------------------------------------------------------------------- 1 | import axios from '@/api/axios'; 2 | import endpoints from '@/api/endpoints'; 3 | 4 | import type { AxiosOutput, AxiosPaginatedOutput } from '@/types/axios.type'; 5 | 6 | const nonPaginatedApis = { 7 | login: async (data) => axios({ ...endpoints.login, ...data }), 8 | 9 | getProfile: async (data) => axios({ ...endpoints.getProfile, ...data }), 10 | 11 | logout: async (data) => axios({ ...endpoints.logout, ...data }), 12 | 13 | getAllUsers: async (data) => axios({ ...endpoints.getAllUsers, ...data }), 14 | } satisfies Record; 15 | 16 | const paginatedApis = {} satisfies Record; 17 | 18 | const api = { ...nonPaginatedApis, paginatedApis } as const; 19 | 20 | export default api; 21 | -------------------------------------------------------------------------------- /src/api/utils.ts: -------------------------------------------------------------------------------- 1 | import { AxiosError } from 'axios'; 2 | import { toast } from 'react-toastify'; 3 | 4 | import { AppError } from '@/api/appError'; 5 | 6 | import type { AxiosErrConfig, ShowToast, SuccessOutput, ThrowAxiosError } from '@/types/axios.type'; 7 | 8 | export class AxiosErr extends AxiosError { 9 | config?: AxiosErrConfig; 10 | } 11 | 12 | export const throwAxiosError: ThrowAxiosError = (error) => { 13 | const STATUS_CODE = 400; 14 | 15 | const message = error instanceof AxiosError ? error.response?.data.message : error; 16 | const statusCode = 17 | error instanceof AxiosError ? (error.response?.status ?? STATUS_CODE) : STATUS_CODE; 18 | 19 | throw new AppError(message, statusCode, { 20 | error, 21 | ...(error instanceof AxiosError ? { res: error.response?.data ?? null } : null), 22 | }); 23 | }; 24 | 25 | export const showToast: ShowToast = (res) => { 26 | const { config } = res; 27 | if (!config) return; 28 | 29 | const { manageToast } = config; 30 | 31 | const isError = res instanceof AxiosError; 32 | 33 | const responseData = (() => { 34 | if (isError) return res.response?.data ?? null; 35 | return 'data' in res ? res.data : null; 36 | })() as SuccessOutput | null; 37 | 38 | const shouldShowToast = 39 | typeof manageToast === 'function' && responseData ? manageToast(responseData) : !!manageToast; 40 | 41 | if (!shouldShowToast) return; 42 | 43 | const toastMessage = responseData?.message ?? 'Something went wrong'; 44 | const call = isError ? 'error' : 'success'; 45 | toast[call](toastMessage); 46 | }; 47 | -------------------------------------------------------------------------------- /src/app/(auth)/layout.ts: -------------------------------------------------------------------------------- 1 | import { redirect } from 'next/navigation'; 2 | 3 | import isAuthenticated from '@/lib/isAuthenticated'; 4 | 5 | import type { Layout } from '@/types'; 6 | 7 | const AuthLayout: Layout = async ({ children }) => { 8 | const isLoggedIn = await isAuthenticated(); 9 | 10 | return isLoggedIn ? redirect('/app') : children; 11 | }; 12 | 13 | export default AuthLayout; 14 | -------------------------------------------------------------------------------- /src/app/(auth)/login/page.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | 3 | import Login from '@/components/login/Login'; 4 | import constants from '@/constants'; 5 | 6 | import type { Metadata } from 'next'; 7 | 8 | import type { Component } from '@/types'; 9 | 10 | export const metadata = { 11 | title: `Login | ${constants.APP_NAME}`, 12 | } satisfies Metadata; 13 | 14 | const LoginPage: Component = () => ( 15 | 16 |

Hello, Non Protected Route

17 | 18 |
19 | ); 20 | 21 | export default LoginPage; 22 | -------------------------------------------------------------------------------- /src/app/(public)/about/page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import constants from '@/constants'; 4 | 5 | import type { Metadata } from 'next'; 6 | 7 | import type { Component } from '@/types'; 8 | 9 | export const metadata = { 10 | title: `About | ${constants.APP_NAME}`, 11 | } satisfies Metadata; 12 | 13 | const AboutPage: Component = () =>

Hello, Public route

; 14 | 15 | export default AboutPage; 16 | -------------------------------------------------------------------------------- /src/app/api/(protected)/user/profile/logout/route.ts: -------------------------------------------------------------------------------- 1 | import cookieStore from '@/lib/cookieStore'; 2 | import constants from '@server/_/constants'; 3 | import sendRes from '@server/_/functions/sendRes'; 4 | 5 | import type { Route } from '@server/_/types'; 6 | 7 | export const POST: Route = async () => { 8 | await cookieStore.deleteAsync(constants.COOKIES.TOKEN_NAME); 9 | 10 | return sendRes(undefined, constants.SUCCESS, { 11 | message: constants.LOGOUT_MESSAGE, 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /src/app/api/(protected)/user/profile/me/route.ts: -------------------------------------------------------------------------------- 1 | import constants from '@server/_/constants'; 2 | import getUserData from '@server/_/functions/getUserData'; 3 | import sendError from '@server/_/functions/sendError'; 4 | import sendRes from '@server/_/functions/sendRes'; 5 | import users, { transformUser } from '@server/_/mock/users'; 6 | 7 | import type { Route } from '@server/_/types'; 8 | 9 | export const GET: Route = async (request) => { 10 | const user = getUserData(request); 11 | 12 | const userData = users.find((cur) => cur.id === user.id); 13 | if (!userData) return sendError(constants.TECHNICAL_ERROR, constants.SERVER_ERROR); 14 | 15 | const data = transformUser(userData); 16 | return sendRes(data, constants.SUCCESS, { 17 | message: constants.PROFILE_RETRIEVED, 18 | }); 19 | }; 20 | -------------------------------------------------------------------------------- /src/app/api/(protected)/users/route.ts: -------------------------------------------------------------------------------- 1 | import constants from '@server/_/constants'; 2 | import sendRes from '@server/_/functions/sendRes'; 3 | import users, { transformUser } from '@server/_/mock/users'; 4 | 5 | import type { Route } from '@server/_/types'; 6 | 7 | export const GET: Route = async () => 8 | sendRes( 9 | users.map((cur) => transformUser(cur)), 10 | constants.SUCCESS, 11 | { 12 | message: constants.USERS_RETRIEVED, 13 | }, 14 | ); 15 | -------------------------------------------------------------------------------- /src/app/api/_/constants/index.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import-x/no-rename-default 2 | import clientConstants from '@/constants'; 3 | 4 | export const HTTP_STATUSES = { 5 | SUCCESS: 200, 6 | CREATED: 201, 7 | ACCEPTED: 202, 8 | NO_CONTENT: 204, 9 | RESET_CONTENT: 205, 10 | PARTIAL_CONTENT: 206, 11 | BAD_REQUEST: 400, 12 | UNAUTHORIZED: 401, 13 | FORBIDDEN: 403, 14 | NOT_FOUND: 404, 15 | NOT_ACCEPTABLE: 406, 16 | PAYLOAD_LARGE: 413, 17 | TOO_MANY_REQUESTS: 429, 18 | SERVER_ERROR: 500, 19 | SERVICE_UNAVAILABLE: 503, 20 | } as const; 21 | 22 | const ERRORS = { 23 | UNKNOWN_ERROR: 'Something went wrong, please try again later!', 24 | TECHNICAL_ERROR: 'It’s not you. It’s us. Give it another try, please!', 25 | FORBIDDEN_ERROR: 'You do not have permission to perform this action', 26 | UNAUTHORIZED_ERROR: 'You are not logged in! please log in to get access', 27 | TOKEN_USER_NOT_EXIST_ERROR: 'The user belonging to this token does no longer exist', 28 | INVALID_USER_TOKEN_ERROR: 'Invalid user token. Please log in again!', 29 | USER_TOKEN_EXPIRED_ERROR: 'Your token has expired. Please log in again!', 30 | RATE_LIMIT_ERROR: 'Too many requests from this IP, please try again in an hour!', 31 | MAINTENANCE_ERROR: 32 | 'This site is currently unable to handle the HTTP request due to a maintenance of the server. Please try after some time', 33 | JSON_PARSE_ERROR: 'Invalid JSON', 34 | CORS_ERROR: 'Cors not enabled for this site, please contact your admin.', 35 | CONNECTION_ERROR: 36 | "We're experiencing some issues with connecting to the database at the moment. Please try again later.", 37 | } as const; 38 | 39 | const MESSAGES = { 40 | API_HANDSHAKE: 'Hi, I am an API Home Page', 41 | INVALID_DATA: 'Provided data is invalid', 42 | NO_DATA_FOUND: 'No Data Found', 43 | ID_NOT_FOUND: 'No data found with this id', 44 | ALREADY_EXIST: 'Already Exist', 45 | LOGIN_MESSAGE: 'Logged in Successfully', 46 | LOGOUT_MESSAGE: 'Logout Successful', 47 | PROFILE_RETRIEVED: 'Profile Fetched Successfully', 48 | INCORRECT_LOGIN: 'Incorrect email or password', 49 | USERS_RETRIEVED: 'Users retrieved successfully.', 50 | } as const; 51 | 52 | const constants = { 53 | ...clientConstants, 54 | ...HTTP_STATUSES, 55 | ...ERRORS, 56 | ...MESSAGES, 57 | } as const; 58 | 59 | export default constants; 60 | -------------------------------------------------------------------------------- /src/app/api/_/functions/getBody.ts: -------------------------------------------------------------------------------- 1 | import type { NextRequest } from 'next/server'; 2 | 3 | const getBody = async (request: NextRequest): Promise => { 4 | try { 5 | return (await request.json()) as T; 6 | } catch { 7 | return null; 8 | } 9 | }; 10 | 11 | export default getBody; 12 | -------------------------------------------------------------------------------- /src/app/api/_/functions/getUserData.ts: -------------------------------------------------------------------------------- 1 | interface GetUserDataOutput { 2 | id: number; 3 | } 4 | 5 | type GetUserData = (request: Request) => GetUserDataOutput; 6 | 7 | const getUserData: GetUserData = (request) => { 8 | const userId = request.headers.get('user') as string; 9 | 10 | return { id: +userId }; 11 | }; 12 | 13 | export default getUserData; 14 | -------------------------------------------------------------------------------- /src/app/api/_/functions/sendError.ts: -------------------------------------------------------------------------------- 1 | import sendRes from '@server/_/functions/sendRes'; 2 | 3 | import type { NextResponse } from 'next/server'; 4 | import type { ValueOf } from 'type-fest'; 5 | 6 | import type { HTTP_STATUSES } from '@server/_/constants'; 7 | 8 | export type SendError = ( 9 | message: string, 10 | statusCode: ValueOf, 11 | ) => NextResponse; 12 | 13 | const sendError: SendError = (message, statusCode) => sendRes(undefined, statusCode, { message }); 14 | 15 | export default sendError; 16 | -------------------------------------------------------------------------------- /src/app/api/_/functions/sendRes.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | 3 | import type { ValueOf } from 'type-fest'; 4 | 5 | import type { RecursiveType } from '@/types'; 6 | import type { HTTP_STATUSES } from '@server/_/constants'; 7 | 8 | export interface ResOptions { 9 | message?: string; 10 | extraFields?: RecursiveType; 11 | } 12 | 13 | export type SendRes = ( 14 | data: T, 15 | statusCode: ValueOf, 16 | options: ResOptions, 17 | ) => NextResponse; 18 | 19 | const sendRes: SendRes = (data, statusCode, options) => { 20 | const { message, extraFields = {} } = options; 21 | 22 | return NextResponse.json( 23 | { 24 | status: statusCode, 25 | message, 26 | data, 27 | ...extraFields, 28 | }, 29 | { status: statusCode }, 30 | ); 31 | }; 32 | 33 | export default sendRes; 34 | -------------------------------------------------------------------------------- /src/app/api/_/mock/users.ts: -------------------------------------------------------------------------------- 1 | interface User { 2 | id: number; 3 | email: string; 4 | password: string; 5 | firstName: string; 6 | lastName: string; 7 | } 8 | 9 | const users: User[] = [ 10 | { 11 | id: 1, 12 | email: 'nishargshah3101@gmail.com', 13 | password: 'admin', 14 | firstName: 'Nisharg', 15 | lastName: 'Shah', 16 | }, 17 | { 18 | id: 2, 19 | email: 'nisharg.shah@openxcell.com', 20 | password: 'admin@ox', 21 | firstName: 'Nisharg', 22 | lastName: 'OpenXcell', 23 | }, 24 | { 25 | id: 3, 26 | email: 'nshah1@codal.com', 27 | password: 'admin@codal', 28 | firstName: 'Nisharg', 29 | lastName: 'Codal', 30 | }, 31 | { 32 | id: 4, 33 | email: 'admin@example.com', 34 | password: 'admin', 35 | firstName: 'Admin', 36 | lastName: '', 37 | }, 38 | ]; 39 | 40 | type TransformUser = (user: User) => Omit & { 41 | fullName: string; 42 | }; 43 | 44 | export const transformUser: TransformUser = (user) => ({ 45 | id: user.id, 46 | email: user.email, 47 | firstName: user.firstName, 48 | lastName: user.lastName, 49 | fullName: `${user.firstName} ${user.lastName}`, 50 | }); 51 | 52 | export default users; 53 | -------------------------------------------------------------------------------- /src/app/api/_/types/index.ts: -------------------------------------------------------------------------------- 1 | import type { NextRequest, NextResponse } from 'next/server'; 2 | 3 | import type { Obj } from '@/types'; 4 | 5 | interface RouteContext { 6 | params: Promise>; 7 | } 8 | 9 | export type Route = (request: NextRequest, context: RouteContext) => Promise; 10 | -------------------------------------------------------------------------------- /src/app/api/auth/login/route.ts: -------------------------------------------------------------------------------- 1 | import cookieStore from '@/lib/cookieStore'; 2 | import constants from '@server/_/constants'; 3 | import getBody from '@server/_/functions/getBody'; 4 | import sendError from '@server/_/functions/sendError'; 5 | import sendRes from '@server/_/functions/sendRes'; 6 | import users from '@server/_/mock/users'; 7 | 8 | import type { Route } from '@server/_/types'; 9 | 10 | interface LoginRequestDTO { 11 | email: string; 12 | password: string; 13 | } 14 | 15 | export const POST: Route = async (request) => { 16 | const body = await getBody(request); 17 | if (!body) return sendError(constants.INVALID_DATA, constants.BAD_REQUEST); 18 | 19 | const user = users.find( 20 | (cur) => cur.email === body.email.toLowerCase() && cur.password === body.password, 21 | ); 22 | 23 | if (!user) return sendError(constants.INCORRECT_LOGIN, constants.BAD_REQUEST); 24 | 25 | await cookieStore.setAsync(constants.COOKIES.TOKEN_NAME, user.id.toString(), { 26 | httpOnly: true, 27 | sameSite: 'strict', 28 | }); 29 | 30 | return sendRes(undefined, constants.SUCCESS, { 31 | message: constants.LOGIN_MESSAGE, 32 | }); 33 | }; 34 | -------------------------------------------------------------------------------- /src/app/api/route.ts: -------------------------------------------------------------------------------- 1 | import constants from '@server/_/constants'; 2 | import sendRes from '@server/_/functions/sendRes'; 3 | 4 | import type { Route } from '@server/_/types'; 5 | 6 | export const GET: Route = async () => 7 | sendRes(undefined, constants.SUCCESS, { message: constants.API_HANDSHAKE }); 8 | -------------------------------------------------------------------------------- /src/app/app.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Providers from '@/app/providers'; 4 | import AppClient from '@/components/AppClient'; 5 | import Header from '@/components/header/Header'; 6 | 7 | import type { Layout } from '@/types'; 8 | import type { RootLayoutAppProps } from '@/types/zustandState.type'; 9 | 10 | const App: Layout = ({ children, ...props }) => ( 11 | 12 | 13 |
14 |
15 | {children} 16 |
17 |
18 | ); 19 | 20 | export default App; 21 | -------------------------------------------------------------------------------- /src/app/app/layout.ts: -------------------------------------------------------------------------------- 1 | import { redirect } from 'next/navigation'; 2 | 3 | import isAuthenticated from '@/lib/isAuthenticated'; 4 | 5 | import type { Layout } from '@/types'; 6 | 7 | const AppLayout: Layout = async ({ children }) => { 8 | const isLoggedIn = await isAuthenticated(); 9 | 10 | return isLoggedIn ? children : redirect('/login'); 11 | }; 12 | 13 | export default AppLayout; 14 | -------------------------------------------------------------------------------- /src/app/app/logout/page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Logout from '@/components/logout/Logout'; 4 | import constants from '@/constants'; 5 | 6 | import type { Metadata } from 'next'; 7 | 8 | import type { Component } from '@/types'; 9 | 10 | export const metadata = { 11 | title: `Logout | ${constants.APP_NAME}`, 12 | } satisfies Metadata; 13 | 14 | const LogoutPage: Component = () => ; 15 | 16 | export default LogoutPage; 17 | -------------------------------------------------------------------------------- /src/app/app/page.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | 3 | import Dashboard from '@/components/dashboard/Dashboard'; 4 | import constants from '@/constants'; 5 | 6 | import type { Metadata } from 'next'; 7 | 8 | import type { Component } from '@/types'; 9 | 10 | export const metadata = { 11 | title: `Dashboard | ${constants.APP_NAME}`, 12 | } satisfies Metadata; 13 | 14 | const AppPage: Component = () => ( 15 | 16 |

Hello, Protected Route

17 | 18 |
19 | ); 20 | 21 | export default AppPage; 22 | -------------------------------------------------------------------------------- /src/app/app/users/page.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | 3 | import Users from '@/components/users/Users'; 4 | import constants from '@/constants'; 5 | 6 | import type { Metadata } from 'next'; 7 | 8 | import type { Component } from '@/types'; 9 | 10 | export const metadata = { 11 | title: `Users | ${constants.APP_NAME}`, 12 | } satisfies Metadata; 13 | 14 | const UsersPage: Component = () => ( 15 | 16 |

Hello, Protected Route

17 | 18 |
19 | ); 20 | 21 | export default UsersPage; 22 | -------------------------------------------------------------------------------- /src/app/error.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | // eslint-disable-next-line no-restricted-exports 4 | export { default } from '@/components/defaults/error/Error'; 5 | -------------------------------------------------------------------------------- /src/app/global-error.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React from 'react'; 4 | 5 | import Error from '@/components/defaults/error/Error'; 6 | import { interFont } from '@/styles/font'; 7 | 8 | import type { Component, NextErrorType } from '@/types'; 9 | 10 | const GlobalError: Component = (props) => ( 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | 18 | export default GlobalError; 19 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import React, { Suspense } from 'react'; 2 | 3 | import App from '@/app/app'; 4 | import constants from '@/constants'; 5 | import { getProfileApi } from '@/features/profile/profile.api'; 6 | import cookieStore from '@/lib/cookieStore'; 7 | import Loader from '@/shared/loader/Loader'; 8 | import { getMode, getPreferredMode } from '@/store/slices/theme/theme.slice'; 9 | import { interFont } from '@/styles/font'; 10 | import '@/styles/globals.css'; 11 | 12 | import type { Metadata } from 'next'; 13 | 14 | import type { Layout } from '@/types'; 15 | 16 | export const metadata: Metadata = { 17 | title: constants.APP_NAME, 18 | description: `${constants.APP_NAME} Description`, 19 | }; 20 | 21 | const RootLayout: Layout = async ({ children }) => { 22 | const { mode, preferredMode, theme } = await (async () => { 23 | const [modeString, preferredModeString] = await Promise.all([ 24 | cookieStore.getAsync(constants.COOKIES.THEME_NAME), 25 | cookieStore.getAsync(constants.COOKIES.SYSTEM_THEME), 26 | ]); 27 | 28 | const themeMode = getMode(modeString); 29 | const preferredThemeMode = getPreferredMode(preferredModeString); 30 | const dataTheme = themeMode === constants.THEME.SYSTEM ? preferredThemeMode : themeMode; 31 | 32 | return { mode: themeMode, preferredMode: preferredThemeMode, theme: dataTheme }; 33 | })(); 34 | 35 | const user = await (async () => { 36 | const hasToken = !!(await cookieStore.getAsync(constants.COOKIES.TOKEN_NAME)); 37 | if (!hasToken) return; 38 | 39 | return getProfileApi({}, { throwError: false }); 40 | })(); 41 | 42 | return ( 43 | 44 | 45 | }> 46 | 47 | {children} 48 | 49 | 50 | 51 | 52 | ); 53 | }; 54 | 55 | export default RootLayout; 56 | -------------------------------------------------------------------------------- /src/app/not-found.tsx: -------------------------------------------------------------------------------- 1 | import constants from '@/constants'; 2 | 3 | import type { Metadata } from 'next'; 4 | 5 | export const metadata = { 6 | title: `404 | ${constants.APP_NAME}`, 7 | } satisfies Metadata; 8 | 9 | // eslint-disable-next-line no-restricted-exports 10 | export { default } from '@/components/defaults/notFound/NotFound'; 11 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import constants from '@/constants'; 4 | 5 | import type { Metadata } from 'next'; 6 | 7 | import type { Component } from '@/types'; 8 | 9 | export const metadata = { 10 | title: `Home | ${constants.APP_NAME}`, 11 | } satisfies Metadata; 12 | 13 | const HomePage: Component = () =>

Hello, Homepage

; 14 | 15 | export default HomePage; 16 | -------------------------------------------------------------------------------- /src/app/providers.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React from 'react'; 4 | 5 | import { QueryClientProvider } from '@tanstack/react-query'; 6 | // eslint-disable-next-line import-x/no-extraneous-dependencies 7 | import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; 8 | import { ToastContainer } from 'react-toastify'; 9 | 10 | import { useZustandState } from '@/hooks/useZustandState'; 11 | import queryClient from '@/lib/queryClient'; 12 | import ZustandProvider from '@/providers/ZustandProvider'; 13 | import ProgressBar from '@/shared/loader/ProgressBar'; 14 | 15 | import type { Layout } from '@/types'; 16 | import type { RootLayoutAppProps } from '@/types/zustandState.type'; 17 | 18 | type ProvidersProps = RootLayoutAppProps; 19 | 20 | const Providers: Layout = ({ children, ...props }) => { 21 | const zustandState = useZustandState(props); 22 | 23 | return ( 24 | 25 | 26 | {children} 27 | 28 | 29 | 30 | 31 | 32 | ); 33 | }; 34 | 35 | export default Providers; 36 | -------------------------------------------------------------------------------- /src/assets/images/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/AppClient.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React, { useCallback, useEffect } from 'react'; 4 | 5 | import constants from '@/constants'; 6 | import { useStore } from '@/hooks/useStore'; 7 | import WebsiteLoader from '@/shared/loader/WebsiteLoader'; 8 | import { sleep } from '@/utils'; 9 | 10 | import type { Component } from '@/types'; 11 | 12 | const AppClient: Component = () => { 13 | const isLoading = useStore((state) => state.isLoading); 14 | const setLoading = useStore((state) => state.setLoading); 15 | const setPreferredMode = useStore((state) => state.setPreferredMode); 16 | 17 | const handleColorSchema = useCallback( 18 | (event: MediaQueryListEvent) => { 19 | const { LIGHT, DARK } = constants.THEME; 20 | const newTheme = event.matches ? DARK : LIGHT; 21 | setPreferredMode(newTheme); 22 | }, 23 | [setPreferredMode], 24 | ); 25 | 26 | useEffect(() => { 27 | (async () => { 28 | await sleep(constants.STARTUP_PROGRESS_BAR_TIMEOUT); 29 | setLoading(false); 30 | })(); 31 | }, [setLoading]); 32 | 33 | useEffect(() => { 34 | const matchMedia = globalThis.matchMedia('(prefers-color-scheme: dark)'); 35 | 36 | matchMedia.addEventListener('change', handleColorSchema); 37 | return () => matchMedia.removeEventListener('change', handleColorSchema); 38 | }, [handleColorSchema]); 39 | 40 | return isLoading ? : null; 41 | }; 42 | 43 | export default AppClient; 44 | -------------------------------------------------------------------------------- /src/components/dashboard/Dashboard.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | text-align: center; 3 | margin-top: 1rem; 4 | } 5 | -------------------------------------------------------------------------------- /src/components/dashboard/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React from 'react'; 4 | 5 | import styles from '@/components/dashboard/Dashboard.module.css'; 6 | import { useStore } from '@/hooks/useStore'; 7 | 8 | import type { Component } from '@/types'; 9 | 10 | const Dashboard: Component = () => { 11 | const user = useStore((state) => state.user); 12 | 13 | if (!user) return null; 14 | 15 | return ( 16 |
17 |

ID: {user.id}

18 |

Full Name: {user.fullName}

19 |

Email: {user.email}

20 |
21 | ); 22 | }; 23 | 24 | export default Dashboard; 25 | -------------------------------------------------------------------------------- /src/components/defaults/error/Error.module.css: -------------------------------------------------------------------------------- 1 | .errorContainer { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | } 6 | 7 | .errorContainer h2 { 8 | padding: 1rem; 9 | } 10 | 11 | .errorContainer p { 12 | margin-bottom: 1rem; 13 | } 14 | 15 | .errorContainer button { 16 | width: fit-content; 17 | margin: 0 auto; 18 | padding: 0.5rem 1rem; 19 | } 20 | -------------------------------------------------------------------------------- /src/components/defaults/error/Error.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { useQueryErrorResetBoundary } from '@tanstack/react-query'; 4 | 5 | import styles from '@/components/defaults/error/Error.module.css'; 6 | 7 | import type { Component, NextErrorType } from '@/types'; 8 | 9 | const Error: Component = ({ error, reset }) => { 10 | const { reset: queryReset } = useQueryErrorResetBoundary(); 11 | 12 | const handleReset = () => { 13 | reset(); 14 | queryReset(); 15 | }; 16 | 17 | return ( 18 |
19 |

It’s not you. It’s us. Give it another try, please!

20 |

{error.message ?? ''}

21 | 24 |
25 | ); 26 | }; 27 | 28 | export default Error; 29 | -------------------------------------------------------------------------------- /src/components/defaults/notFound/NotFound.module.css: -------------------------------------------------------------------------------- 1 | .notFound { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | } 6 | 7 | .notFound h2 { 8 | padding: 1rem; 9 | } 10 | 11 | .notFound p { 12 | margin-bottom: 1rem; 13 | } 14 | 15 | .notFound button { 16 | width: fit-content; 17 | margin: 0 auto; 18 | padding: 0.5rem 1rem; 19 | cursor: pointer; 20 | } 21 | -------------------------------------------------------------------------------- /src/components/defaults/notFound/NotFound.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React from 'react'; 4 | 5 | import Link from 'next/link'; 6 | import { usePathname } from 'next/navigation'; 7 | 8 | import styles from '@/components/defaults/notFound/NotFound.module.css'; 9 | 10 | import type { Component } from '@/types'; 11 | 12 | const NotFound: Component = () => { 13 | const pathname = usePathname(); 14 | 15 | return ( 16 |
17 |

Not Found

18 |

Could not find requested resource "{pathname}"

19 | 20 | 21 | 22 |
23 | ); 24 | }; 25 | 26 | export default NotFound; 27 | -------------------------------------------------------------------------------- /src/components/header/Header.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | justify-content: center; 4 | align-items: baseline; 5 | padding: 1rem; 6 | border: 1px solid; 7 | font-size: 20px; 8 | margin-bottom: 1rem; 9 | } 10 | 11 | .link { 12 | margin-right: 10px; 13 | text-decoration: none; 14 | } 15 | 16 | .btnWrapper { 17 | position: absolute; 18 | right: 1rem; 19 | } 20 | 21 | .btn { 22 | cursor: pointer; 23 | margin-left: 1rem; 24 | } 25 | 26 | .capitalize { 27 | text-transform: capitalize; 28 | } 29 | -------------------------------------------------------------------------------- /src/components/header/Header.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React from 'react'; 4 | 5 | import Link from 'next/link'; 6 | 7 | import styles from '@/components/header/Header.module.css'; 8 | import { useRouter } from '@/hooks/useRouter'; 9 | import { useStore } from '@/hooks/useStore'; 10 | 11 | import type { Component } from '@/types'; 12 | 13 | const Header: Component = () => { 14 | const router = useRouter(); 15 | 16 | const mode = useStore((state) => state.mode); 17 | const setMode = useStore((state) => state.setMode); 18 | const isAuthenticated = useStore((state) => state.isAuthenticated); 19 | 20 | return ( 21 |
22 |
23 | 24 | Home 25 | 26 | 27 | About 28 | 29 | {!isAuthenticated && ( 30 | 31 | Login 32 | 33 | )} 34 | {isAuthenticated ? ( 35 | 36 | Dashboard 37 | 38 | ) : null} 39 | {isAuthenticated ? ( 40 | 41 | Users 42 | 43 | ) : null} 44 |
45 |
46 | 49 | {isAuthenticated ? ( 50 | 53 | ) : null} 54 |
55 |
56 | ); 57 | }; 58 | 59 | export default Header; 60 | -------------------------------------------------------------------------------- /src/components/login/Login.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 1rem; 3 | margin: 1rem auto; 4 | max-width: 50%; 5 | } 6 | 7 | .wrapper { 8 | margin-bottom: 1rem; 9 | } 10 | 11 | .label { 12 | display: block; 13 | margin-bottom: 4px; 14 | font-size: 16px; 15 | } 16 | 17 | .input { 18 | padding: 4px; 19 | width: 100%; 20 | } 21 | 22 | .error { 23 | margin-top: 0.5rem; 24 | } 25 | 26 | .btn { 27 | padding: 0.5rem 1rem; 28 | cursor: pointer; 29 | } 30 | -------------------------------------------------------------------------------- /src/components/login/Login.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React from 'react'; 4 | 5 | import { useForm } from 'react-hook-form'; 6 | 7 | import styles from '@/components/login/Login.module.css'; 8 | import { useLogin } from '@/features/auth/useLogin'; 9 | import { useProfile } from '@/features/profile/useProfile'; 10 | import { useRouter } from '@/hooks/useRouter'; 11 | 12 | import type { SubmitHandler } from 'react-hook-form'; 13 | 14 | import type { Component, Layout } from '@/types'; 15 | 16 | interface FormControlProps { 17 | label: string; 18 | labelFor: string; 19 | error?: string; 20 | } 21 | 22 | const FormControl: Layout = ({ children, label, labelFor, error }) => ( 23 |
24 | 27 | {children} 28 | {error ?

{error}

: null} 29 |
30 | ); 31 | 32 | interface FormData { 33 | email: string; 34 | password: string; 35 | } 36 | 37 | const Login: Component = () => { 38 | const router = useRouter(); 39 | 40 | const { mutateAsync, isPending: isLoginPending } = useLogin(); 41 | const { mutateAsync: fetchProfile, isPending: isProfilePending } = useProfile(); 42 | const isLoading = isLoginPending || isProfilePending; 43 | 44 | const { formState, register, handleSubmit: onSubmit } = useForm(); 45 | const { errors } = formState; 46 | 47 | const handleSubmit: SubmitHandler = async (data) => { 48 | try { 49 | await mutateAsync(data); 50 | await fetchProfile({}); 51 | router.push('/app'); 52 | } catch (error) { 53 | console.error(error); 54 | } 55 | }; 56 | 57 | return ( 58 |
59 | 60 | 66 | 67 | 68 | 74 | 75 |
76 | 79 |
80 |
81 | ); 82 | }; 83 | 84 | export default Login; 85 | -------------------------------------------------------------------------------- /src/components/logout/Logout.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React, { useEffect } from 'react'; 4 | 5 | import { useLogout } from '@/features/profile/useLogout'; 6 | import { useRouter } from '@/hooks/useRouter'; 7 | 8 | import type { Component } from '@/types'; 9 | 10 | const Logout: Component = () => { 11 | const router = useRouter(); 12 | const { mutateAsync: logout } = useLogout(); 13 | 14 | useEffect(() => { 15 | (async () => { 16 | try { 17 | await logout({}); 18 | } catch { 19 | // empty 20 | } 21 | 22 | router.push('/'); 23 | })(); 24 | }, [logout, router]); 25 | 26 | return

Logging you out...

; 27 | }; 28 | 29 | export default Logout; 30 | -------------------------------------------------------------------------------- /src/components/users/Users.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | margin-top: 1rem; 6 | } 7 | 8 | .table, 9 | .tableHead, 10 | .tableData { 11 | border: 1px solid; 12 | border-collapse: collapse; 13 | } 14 | 15 | .tableHead { 16 | padding: 1rem; 17 | } 18 | 19 | .tableData { 20 | padding: 1rem; 21 | width: 200px; 22 | text-align: center; 23 | } 24 | -------------------------------------------------------------------------------- /src/components/users/Users.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React from 'react'; 4 | 5 | import styles from '@/components/users/Users.module.css'; 6 | import { useUsers } from '@/features/users/useUsers'; 7 | 8 | import type { Component } from '@/types'; 9 | 10 | const Users: Component = () => { 11 | const { data: users } = useUsers(); 12 | 13 | if (!users) return null; 14 | 15 | return ( 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | {users.slice(0, 10).map((user) => ( 27 | 28 | 29 | 30 | 31 | 32 | ))} 33 | 34 |
IDFull NameEmail
{user.id}{user.fullName}{user.email}
35 |
36 | ); 37 | }; 38 | 39 | export default Users; 40 | -------------------------------------------------------------------------------- /src/config/index.ts: -------------------------------------------------------------------------------- 1 | type Environment = 'development' | 'production'; 2 | 3 | const config = { 4 | NODE_ENV: process.env.NODE_ENV as Environment, 5 | ENV_TYPE: process.env.NODE_ENV as Environment, 6 | NEXT_PUBLIC_API_PATH: process.env.NEXT_PUBLIC_API_PATH as string, 7 | } as const; 8 | 9 | export default config; 10 | -------------------------------------------------------------------------------- /src/constants/index.ts: -------------------------------------------------------------------------------- 1 | import { isLive } from '@/utils'; 2 | 3 | const constants = { 4 | APP_NAME: 'Next.js Template', 5 | COOKIES: { 6 | TOKEN_NAME: 'token', 7 | THEME_NAME: 'theme', 8 | SYSTEM_THEME: 'system_theme', 9 | }, 10 | THEME: { 11 | LIGHT: 'light', 12 | DARK: 'dark', 13 | SYSTEM: 'system', 14 | }, 15 | PROGRESS_BAR_DELAY: isLive ? 200 : 500, 16 | STARTUP_PROGRESS_BAR_TIMEOUT: 200, 17 | } as const; 18 | 19 | export default constants; 20 | -------------------------------------------------------------------------------- /src/features/auth/auth.api.ts: -------------------------------------------------------------------------------- 1 | import api from '@/api'; 2 | import catchAsync from '@/api/catchAsync'; 3 | 4 | import type { LoginInput, LoginOutput } from '@/features/auth/auth.type'; 5 | 6 | export const loginApi = catchAsync(async (data) => { 7 | const res = await api.login({ data }); 8 | return res.data.data; 9 | }); 10 | -------------------------------------------------------------------------------- /src/features/auth/auth.type.ts: -------------------------------------------------------------------------------- 1 | // Login 2 | 3 | export interface LoginInput { 4 | email: string; 5 | password: string; 6 | } 7 | 8 | export type LoginOutput = unknown; 9 | -------------------------------------------------------------------------------- /src/features/auth/useLogin.ts: -------------------------------------------------------------------------------- 1 | import { useMutation } from '@tanstack/react-query'; 2 | 3 | import { loginApi } from '@/features/auth/auth.api'; 4 | 5 | export const useLogin = () => useMutation({ mutationFn: loginApi }); 6 | -------------------------------------------------------------------------------- /src/features/profile/profile.api.ts: -------------------------------------------------------------------------------- 1 | import api from '@/api'; 2 | import catchAsync from '@/api/catchAsync'; 3 | 4 | import type { 5 | GetProfileInput, 6 | GetProfileOutput, 7 | LogoutInput, 8 | LogoutOutput, 9 | } from '@/features/profile/profile.type'; 10 | 11 | export const getProfileApi = catchAsync(async ({ signal }) => { 12 | const res = await api.getProfile({ signal }); 13 | return res.data.data; 14 | }); 15 | 16 | export const logoutApi = catchAsync(async (data) => { 17 | const res = await api.logout(data); 18 | return res.data.data; 19 | }); 20 | -------------------------------------------------------------------------------- /src/features/profile/profile.type.ts: -------------------------------------------------------------------------------- 1 | // Get Profile 2 | 3 | export type GetProfileInput = unknown; 4 | 5 | export interface GetProfileOutput { 6 | id: number; 7 | email: string; 8 | firstName: string; 9 | lastName: string; 10 | fullName: string; 11 | } 12 | 13 | // Logout 14 | 15 | export type LogoutInput = unknown; 16 | 17 | export type LogoutOutput = unknown; 18 | -------------------------------------------------------------------------------- /src/features/profile/useLogout.ts: -------------------------------------------------------------------------------- 1 | import { useMutation } from '@tanstack/react-query'; 2 | 3 | import { logoutApi } from '@/features/profile/profile.api'; 4 | import { useStore } from '@/hooks/useStore'; 5 | 6 | export const useLogout = () => { 7 | const logout = useStore((state) => state.logout); 8 | 9 | return useMutation({ 10 | mutationFn: logoutApi, 11 | onSuccess: logout, 12 | onError: logout, 13 | }); 14 | }; 15 | -------------------------------------------------------------------------------- /src/features/profile/useProfile.ts: -------------------------------------------------------------------------------- 1 | import { useMutation } from '@tanstack/react-query'; 2 | 3 | import { getProfileApi } from '@/features/profile/profile.api'; 4 | import { useStore } from '@/hooks/useStore'; 5 | 6 | export const useProfile = () => { 7 | const setUser = useStore((state) => state.setUser); 8 | 9 | return useMutation({ 10 | mutationFn: getProfileApi, 11 | onSuccess: (data) => { 12 | if (data) setUser(data); 13 | }, 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /src/features/users/useUsers.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | 3 | import { useQuery } from '@tanstack/react-query'; 4 | 5 | import { getAllUsersApi } from '@/features/users/users.api'; 6 | import { useStore } from '@/hooks/useStore'; 7 | 8 | export const useUsers = () => { 9 | const setLoading = useStore((state) => state.setLoading); 10 | 11 | const queryData = useQuery({ queryKey: ['users'], queryFn: getAllUsersApi }); 12 | const { data } = queryData; 13 | 14 | useEffect(() => { 15 | setLoading(!data); 16 | }, [data, setLoading]); 17 | 18 | return queryData; 19 | }; 20 | -------------------------------------------------------------------------------- /src/features/users/users.api.ts: -------------------------------------------------------------------------------- 1 | import api from '@/api'; 2 | import catchAsync from '@/api/catchAsync'; 3 | 4 | import type { GetAllUsersInput, GetAllUsersOutput } from '@/features/users/users.type'; 5 | 6 | export const getAllUsersApi = catchAsync( 7 | async ({ signal }) => { 8 | const res = await api.getAllUsers({ signal }); 9 | return res.data.data; 10 | }, 11 | ); 12 | -------------------------------------------------------------------------------- /src/features/users/users.type.ts: -------------------------------------------------------------------------------- 1 | import type { GetProfileOutput } from '@/features/profile/profile.type'; 2 | 3 | // Get All Users 4 | 5 | export type GetAllUsersInput = unknown; 6 | 7 | export type GetAllUsersOutput = GetProfileOutput[]; 8 | -------------------------------------------------------------------------------- /src/hooks/useRouter.ts: -------------------------------------------------------------------------------- 1 | export { useRouter } from 'next-nprogress-bar'; 2 | -------------------------------------------------------------------------------- /src/hooks/useStore.ts: -------------------------------------------------------------------------------- 1 | import { useStore as useZustandStore } from 'zustand'; 2 | 3 | import { useZustand } from '@/providers/ZustandProvider'; 4 | 5 | import type { UseStore } from '@/types/store.type'; 6 | 7 | export const useStore: UseStore = (selector) => { 8 | const store = useZustand(); 9 | return useZustandStore(store, selector); 10 | }; 11 | -------------------------------------------------------------------------------- /src/hooks/useZustandState.ts: -------------------------------------------------------------------------------- 1 | import { getUser } from '@/store/slices/auth/auth.slice'; 2 | 3 | import type { RootLayoutAppProps, ZustandState } from '@/types/zustandState.type'; 4 | 5 | type ZustandStateHook = (props: RootLayoutAppProps) => ZustandState; 6 | 7 | export const useZustandState: ZustandStateHook = (props) => { 8 | const { user, mode, preferredMode } = props; 9 | 10 | return { 11 | mode, 12 | preferredMode, 13 | ...(user ? getUser(user) : null), 14 | }; 15 | }; 16 | -------------------------------------------------------------------------------- /src/lib/cookieStore.ts: -------------------------------------------------------------------------------- 1 | import { deleteCookie, getCookie, getCookies, hasCookie, setCookie } from 'cookies-next'; 2 | 3 | import { isServer } from '@/utils'; 4 | 5 | import type { Obj } from '@/types'; 6 | import type { CookieStoreType, DefaultSetOptions, GetOptions } from '@/types/cookieStore.type'; 7 | 8 | const cookies = async () => { 9 | const { cookies: serverCookies } = await import('next/headers'); 10 | return serverCookies(); 11 | }; 12 | 13 | const getOptions: GetOptions = (options = {}) => { 14 | const defaultSetOptions: DefaultSetOptions = { 15 | secure: true, 16 | sameSite: 'lax', 17 | expires: new Date('9999-12-31'), 18 | }; 19 | 20 | return { ...defaultSetOptions, ...options }; 21 | }; 22 | 23 | const cookieStore: CookieStoreType = { 24 | get(key) { 25 | const token = hasCookie(key) ? getCookie(key) : null; 26 | return (token as Awaited) ?? null; 27 | }, 28 | async getAsync(key) { 29 | if (isServer) { 30 | const serverCookies = await cookies(); 31 | 32 | const token = serverCookies.has(key) ? serverCookies.get(key) : null; 33 | return token ? token.value : null; 34 | } 35 | 36 | return this.get(key); 37 | }, 38 | getAll() { 39 | return getCookies() as Obj; 40 | }, 41 | async getAllAsync() { 42 | if (isServer) { 43 | const serverCookies = await cookies(); 44 | 45 | const allCookies = serverCookies.getAll(); 46 | return allCookies.reduce>((acc, val) => { 47 | const { name, value } = val; 48 | 49 | acc[name] = value; 50 | return acc; 51 | }, {}); 52 | } 53 | 54 | return this.getAll(); 55 | }, 56 | async getAllSerialized() { 57 | if (isServer) { 58 | const serverCookies = await cookies(); 59 | return serverCookies.toString(); 60 | } 61 | 62 | return ''; 63 | }, 64 | set(key, value, options = {}) { 65 | setCookie(key, value, getOptions(options)); 66 | return true; 67 | }, 68 | async setAsync(key, value, options = {}) { 69 | if (isServer) { 70 | const serverCookies = await cookies(); 71 | 72 | serverCookies.set(key, value, getOptions(options)); 73 | return true; 74 | } 75 | 76 | return this.set(key, value); 77 | }, 78 | delete(key) { 79 | const isExist = hasCookie(key); 80 | if (!isExist) return false; 81 | 82 | deleteCookie(key); 83 | return true; 84 | }, 85 | async deleteAsync(key) { 86 | if (isServer) { 87 | const serverCookies = await cookies(); 88 | const isExist = serverCookies.has(key); 89 | if (!isExist) return false; 90 | 91 | serverCookies.delete(key); 92 | return true; 93 | } 94 | 95 | return this.delete(key); 96 | }, 97 | }; 98 | 99 | export default cookieStore; 100 | -------------------------------------------------------------------------------- /src/lib/isAuthenticated.ts: -------------------------------------------------------------------------------- 1 | import constants from '@/constants'; 2 | import cookieStore from '@/lib/cookieStore'; 3 | 4 | type IsAuthenticated = () => Promise; 5 | 6 | const isAuthenticated: IsAuthenticated = async () => { 7 | const hasCookie = await cookieStore.getAsync(constants.COOKIES.TOKEN_NAME); 8 | return !!hasCookie; 9 | }; 10 | 11 | export default isAuthenticated; 12 | -------------------------------------------------------------------------------- /src/lib/queryClient.ts: -------------------------------------------------------------------------------- 1 | import { QueryClient } from '@tanstack/react-query'; 2 | 3 | import { isServer } from '@/utils'; 4 | 5 | const queryClient = new QueryClient({ 6 | defaultOptions: { 7 | queries: { 8 | retry: isServer ? 0 : 1, 9 | staleTime: 10_000, 10 | gcTime: isServer ? Infinity : 1000 * 60 * 5, 11 | }, 12 | }, 13 | }); 14 | 15 | export default queryClient; 16 | -------------------------------------------------------------------------------- /src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | 3 | import constants from '@/constants'; 4 | import cookieStore from '@/lib/cookieStore'; 5 | 6 | import type { NextMiddleware } from 'next/server'; 7 | 8 | export const middleware: NextMiddleware = async (request) => { 9 | const { headers, nextUrl } = request; 10 | const { pathname } = nextUrl; 11 | 12 | if ( 13 | pathname.startsWith('/_next') || 14 | ['/favicon.ico', 'robots.txt'].includes(pathname) || 15 | pathname === '/api' 16 | ) { 17 | return; 18 | } 19 | 20 | // API ONLY 21 | if (pathname.startsWith('/api')) { 22 | const isProtected = !pathname.startsWith('/api/auth'); 23 | const token = await cookieStore.getAsync(constants.COOKIES.TOKEN_NAME); 24 | 25 | if (isProtected) { 26 | if (!token) { 27 | return Response.json( 28 | { status: 401, message: 'You are not logged in! please log in to get access.' }, 29 | { status: 401 }, 30 | ); 31 | } 32 | 33 | const requestHeaders = new Headers(headers); 34 | requestHeaders.set('user', token); 35 | 36 | return NextResponse.next({ 37 | request: { 38 | headers: requestHeaders, 39 | }, 40 | }); 41 | } 42 | } 43 | 44 | return null; 45 | }; 46 | -------------------------------------------------------------------------------- /src/providers/ZustandProvider.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React, { createContext, useContext, useRef } from 'react'; 4 | 5 | import createStore from '@/store'; 6 | 7 | import type { Layout } from '@/types'; 8 | import type { ZustandContextValue } from '@/types/store.type'; 9 | import type { ZustandProviderProps } from '@/types/zustandState.type'; 10 | 11 | const ZustandContext = createContext(undefined); 12 | 13 | const ZustandProvider: Layout = ({ children, ...state }) => { 14 | const storeRef = useRef(undefined); 15 | storeRef.current ??= createStore(state); 16 | 17 | return {children}; 18 | }; 19 | 20 | export const useZustand = (): ZustandContextValue => { 21 | const context = useContext(ZustandContext); 22 | if (!context) throw new Error('Missing ZustandProvider.Provider in the tree'); 23 | 24 | return context; 25 | }; 26 | 27 | export default ZustandProvider; 28 | -------------------------------------------------------------------------------- /src/shared/loader/Loader.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | position: absolute; 3 | width: 100%; 4 | height: 100%; 5 | top: 0; 6 | left: 0; 7 | display: flex; 8 | align-items: center; 9 | justify-content: center; 10 | } 11 | 12 | .loader { 13 | width: 50px; 14 | height: 50px; 15 | border-radius: 50%; 16 | background: 17 | radial-gradient(farthest-side, var(--theme-black) 94%, #0000) top/8px 8px no-repeat, 18 | conic-gradient(#0000 30%, var(--theme-black)); 19 | -webkit-mask: radial-gradient(farthest-side, #0000 calc(100% - 8px), #000 0); 20 | animation: loader 1s infinite linear; 21 | } 22 | 23 | @keyframes loader { 24 | 100% { 25 | transform: rotate(1turn); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/shared/loader/Loader.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import styles from '@/shared/loader/Loader.module.css'; 4 | 5 | import type { Component } from '@/types'; 6 | 7 | const Loader: Component = () => ( 8 |
9 |
10 |
11 | ); 12 | 13 | export default Loader; 14 | -------------------------------------------------------------------------------- /src/shared/loader/ProgressBar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { AppProgressBar } from 'next-nprogress-bar'; 4 | import ReactDOMServer from 'react-dom/server'; 5 | 6 | import constants from '@/constants'; 7 | import WebsiteLoader from '@/shared/loader/WebsiteLoader'; 8 | 9 | import type { Component } from '@/types'; 10 | 11 | const ProgressBar: Component = () => { 12 | const loader = ReactDOMServer.renderToString(); 13 | 14 | const template = ` 15 |
16 |
17 |
18 | ${loader} 19 | `; 20 | 21 | return ( 22 | 27 | ); 28 | }; 29 | 30 | export default ProgressBar; 31 | -------------------------------------------------------------------------------- /src/shared/loader/WebsiteLoader.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import clsx from 'clsx'; 4 | import Image from 'next/image'; 5 | 6 | import logo from '@/assets/images/next.svg'; 7 | 8 | import type { Component } from '@/types'; 9 | 10 | interface WebsiteLoaderType { 11 | isTransparent?: boolean; 12 | } 13 | 14 | const WebsiteLoader: Component = ({ isTransparent = true }) => ( 15 |
20 | logo-img 21 |
22 | ); 23 | 24 | export default WebsiteLoader; 25 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { createStore as createZustandStore } from 'zustand'; 2 | import { devtools } from 'zustand/middleware'; 3 | 4 | import createAuthSlice from '@/store/slices/auth/auth.slice'; 5 | import createLoadingSlice from '@/store/slices/loading/loading.slice'; 6 | import createThemeSlice from '@/store/slices/theme/theme.slice'; 7 | 8 | import type { CreateStore, StateFromFunctions } from '@/types/store.type'; 9 | 10 | export type StoreState = StateFromFunctions< 11 | [typeof createAuthSlice, typeof createLoadingSlice, typeof createThemeSlice] 12 | >; 13 | 14 | const createStore: CreateStore = (state) => 15 | createZustandStore()( 16 | devtools((...a) => ({ 17 | ...createAuthSlice(...a), 18 | ...createLoadingSlice(...a), 19 | ...createThemeSlice(...a), 20 | ...state, 21 | })), 22 | ); 23 | 24 | export default createStore; 25 | -------------------------------------------------------------------------------- /src/store/slices/auth/auth.slice.ts: -------------------------------------------------------------------------------- 1 | import queryClient from '@/lib/queryClient'; 2 | 3 | import type { 4 | AuthSliceGetUser, 5 | AuthSliceInitialState, 6 | CreateAuthSlice, 7 | } from '@/store/slices/auth/auth.type'; 8 | 9 | const initialState: AuthSliceInitialState = { 10 | isAuthenticated: false, 11 | user: null, 12 | }; 13 | 14 | export const getUser: AuthSliceGetUser = (payload) => { 15 | queryClient.setQueryData(['user'], payload); 16 | 17 | return { 18 | isAuthenticated: true, 19 | user: payload, 20 | }; 21 | }; 22 | 23 | const createAuthSlice: CreateAuthSlice = (set) => ({ 24 | ...initialState, 25 | setUser: (payload) => { 26 | set(getUser(payload), false, 'auth/setUser'); 27 | }, 28 | logout: () => { 29 | set(initialState, false, 'auth/logout'); 30 | queryClient.removeQueries(); 31 | }, 32 | }); 33 | 34 | export default createAuthSlice; 35 | -------------------------------------------------------------------------------- /src/store/slices/auth/auth.type.ts: -------------------------------------------------------------------------------- 1 | import type { GetProfileOutput } from '@/features/profile/profile.type'; 2 | import type { RemoveFnType } from '@/types'; 3 | import type { SliceCreator } from '@/types/store.type'; 4 | 5 | interface AuthSlice { 6 | isAuthenticated: boolean; 7 | user?: GetProfileOutput | null; 8 | setUser: (payload: GetProfileOutput) => void; 9 | logout: () => void; 10 | } 11 | 12 | export type AuthSliceInitialState = RemoveFnType; 13 | 14 | export type CreateAuthSlice = SliceCreator; 15 | 16 | // OUTER FUNCTIONS 17 | 18 | interface AuthSliceGetUserOutput { 19 | isAuthenticated: true; 20 | user: GetProfileOutput; 21 | } 22 | 23 | export type AuthSliceGetUser = (payload: GetProfileOutput) => AuthSliceGetUserOutput; 24 | -------------------------------------------------------------------------------- /src/store/slices/loading/loading.slice.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | CreateLoadingSlice, 3 | LoadingSliceInitialState, 4 | } from '@/store/slices/loading/loading.type'; 5 | 6 | const initialState: LoadingSliceInitialState = { 7 | isLoading: true, 8 | }; 9 | 10 | const createLoadingSlice: CreateLoadingSlice = (set) => ({ 11 | ...initialState, 12 | setLoading: (payload) => { 13 | set({ isLoading: payload }, false, 'loading/setLoading'); 14 | }, 15 | }); 16 | 17 | export default createLoadingSlice; 18 | -------------------------------------------------------------------------------- /src/store/slices/loading/loading.type.ts: -------------------------------------------------------------------------------- 1 | import type { RemoveFnType } from '@/types'; 2 | import type { SliceCreator } from '@/types/store.type'; 3 | 4 | interface LoadingSlice { 5 | isLoading: boolean; 6 | setLoading: (payload: boolean) => void; 7 | } 8 | 9 | export type LoadingSliceInitialState = RemoveFnType; 10 | 11 | export type CreateLoadingSlice = SliceCreator; 12 | -------------------------------------------------------------------------------- /src/store/slices/theme/theme.slice.ts: -------------------------------------------------------------------------------- 1 | import constants from '@/constants'; 2 | import cookieStore from '@/lib/cookieStore'; 3 | 4 | import type { 5 | CreateThemeSlice, 6 | ThemeSliceDetectMode, 7 | ThemeSliceGetMode, 8 | ThemeSliceGetPreferredMode, 9 | ThemeSliceInitialState, 10 | ThemeSliceSetDetectedMode, 11 | ThemeSliceSetModeClient, 12 | } from '@/store/slices/theme/theme.type'; 13 | 14 | const { LIGHT, DARK, SYSTEM } = constants.THEME; 15 | 16 | const initialState: ThemeSliceInitialState = { 17 | mode: LIGHT, 18 | preferredMode: LIGHT, 19 | }; 20 | 21 | export const detectMode: ThemeSliceDetectMode = () => { 22 | const darkThemeMq = globalThis.matchMedia(`(prefers-color-scheme: ${DARK})`); 23 | return darkThemeMq.matches ? DARK : LIGHT; 24 | }; 25 | 26 | export const getMode: ThemeSliceGetMode = (mode) => { 27 | if (mode === DARK) return DARK; 28 | if (mode === SYSTEM) return SYSTEM; 29 | return LIGHT; 30 | }; 31 | 32 | export const getPreferredMode: ThemeSliceGetPreferredMode = (mode) => 33 | mode === DARK ? DARK : LIGHT; 34 | 35 | export const setModeClient: ThemeSliceSetModeClient = (mode) => { 36 | const isSystem = mode === SYSTEM; 37 | const actualMode = isSystem ? detectMode() : mode; 38 | 39 | document.documentElement.dataset.theme = actualMode; 40 | cookieStore.set(constants.COOKIES.THEME_NAME, mode); 41 | if (isSystem) cookieStore.set(constants.COOKIES.SYSTEM_THEME, actualMode); 42 | 43 | return actualMode; 44 | }; 45 | 46 | export const setDetectedMode: ThemeSliceSetDetectedMode = (mode) => { 47 | document.documentElement.dataset.theme = mode; 48 | cookieStore.set(constants.COOKIES.THEME_NAME, SYSTEM); 49 | cookieStore.set(constants.COOKIES.SYSTEM_THEME, mode); 50 | }; 51 | 52 | const createThemeSlice: CreateThemeSlice = (set, get) => ({ 53 | ...initialState, 54 | setMode: (mode) => { 55 | const themeMode = (() => { 56 | if (get().mode === LIGHT) return DARK; 57 | if (get().mode === DARK) return SYSTEM; 58 | return LIGHT; 59 | })(); 60 | 61 | const actualMode = mode ?? themeMode; 62 | const preferredMode = setModeClient(actualMode); 63 | 64 | set({ mode: actualMode, preferredMode }, false, 'theme/setMode'); 65 | }, 66 | setPreferredMode: (preferredMode) => { 67 | if (get().mode !== SYSTEM) return; 68 | 69 | setDetectedMode(preferredMode); 70 | 71 | set({ mode: SYSTEM, preferredMode }, false, 'theme/setDetectedMode'); 72 | }, 73 | }); 74 | 75 | export default createThemeSlice; 76 | -------------------------------------------------------------------------------- /src/store/slices/theme/theme.type.ts: -------------------------------------------------------------------------------- 1 | import type { ValueOf } from 'type-fest'; 2 | 3 | import type constants from '@/constants'; 4 | import type { RemoveFnType } from '@/types'; 5 | import type { SliceCreator } from '@/types/store.type'; 6 | 7 | type ThemeMode = ValueOf; 8 | type PreferredMode = Exclude; 9 | 10 | interface ThemeSlice { 11 | mode: ThemeMode; 12 | preferredMode: PreferredMode; 13 | setMode: (mode?: ThemeMode) => void; 14 | setPreferredMode: (preferredMode: PreferredMode) => void; 15 | } 16 | 17 | export type ThemeSliceInitialState = RemoveFnType; 18 | 19 | export type CreateThemeSlice = SliceCreator; 20 | 21 | // OUTER FUNCTIONS 22 | 23 | export type ThemeSliceDetectMode = () => PreferredMode; 24 | 25 | export type ThemeSliceGetMode = (modeString: string | null) => ThemeMode; 26 | 27 | export type ThemeSliceGetPreferredMode = (modeString: string | null) => PreferredMode; 28 | 29 | export type ThemeSliceSetModeClient = (mode: ThemeMode) => PreferredMode; 30 | 31 | export type ThemeSliceSetDetectedMode = (preferredMode: PreferredMode) => void; 32 | -------------------------------------------------------------------------------- /src/styles/font.ts: -------------------------------------------------------------------------------- 1 | import { Inter } from 'next/font/google'; 2 | 3 | // eslint-disable-next-line import-x/prefer-default-export 4 | export const interFont = Inter({ subsets: ['latin'] }); 5 | -------------------------------------------------------------------------------- /src/styles/globals.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | padding: 0; 4 | margin: 0; 5 | } 6 | 7 | :root { 8 | --toastify-z-index: 99999; 9 | } 10 | 11 | [data-theme='light'] { 12 | --theme-white: #fff; 13 | --theme-black: #000; 14 | } 15 | 16 | [data-theme='dark'] { 17 | --theme-white: #000; 18 | --theme-black: #fff; 19 | } 20 | 21 | body { 22 | background-color: var(--theme-white); 23 | color: var(--theme-black); 24 | } 25 | 26 | /* Utils */ 27 | 28 | .text-center { 29 | text-align: center; 30 | } 31 | 32 | /* Website Loader */ 33 | 34 | .website-loader { 35 | display: flex; 36 | justify-content: center; 37 | align-items: center; 38 | background: var(--theme-white); 39 | position: fixed; 40 | top: 0; 41 | left: 0; 42 | height: 100vh; 43 | width: 100lvw !important; 44 | z-index: 99998; 45 | } 46 | 47 | .website-loader img { 48 | width: 100px; 49 | height: 100px; 50 | object-fit: contain; 51 | } 52 | 53 | .website-loader-transparent { 54 | opacity: 0.75; 55 | } 56 | 57 | .dark-mode .website-loader img { 58 | filter: invert(1); 59 | } 60 | -------------------------------------------------------------------------------- /src/types/axios.type.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios'; 2 | 3 | import type { AxiosErr } from '@/api/utils'; 4 | 5 | export interface AxiosSSR { 6 | ssr?: boolean; 7 | } 8 | 9 | type UrlParams = Record; 10 | 11 | interface AxiosExtraProps extends AxiosSSR { 12 | urlParams?: UrlParams; 13 | noAuth?: boolean; 14 | } 15 | 16 | interface ManageToast { 17 | manageToast?: ((res: SuccessOutput) => boolean) | boolean; 18 | } 19 | 20 | export type AxiosRequestConfigWithExtraProps = AxiosRequestConfig & AxiosExtraProps; 21 | 22 | type AxiosRequestInput = Pick & ManageToast; 23 | 24 | export type Endpoints = Record; 25 | 26 | export type InternalAxiosRequestConfigWithExtraProps = InternalAxiosRequestConfig & AxiosExtraProps; 27 | 28 | export type AxiosRes = Omit, 'config'> & { 29 | config: InternalAxiosRequestConfig & ManageToast; 30 | }; 31 | 32 | export type AxiosErrConfig = InternalAxiosRequestConfig & ManageToast; 33 | 34 | export interface SuccessOutput { 35 | status: number; 36 | message: string; 37 | results: T extends unknown[] ? number : number | undefined; 38 | data: T; 39 | } 40 | 41 | export interface PaginatedInputParamsDefaults { 42 | page?: number; 43 | limit?: number; 44 | } 45 | 46 | export interface PaginatedInputDataDefaults { 47 | search?: string; 48 | fields?: string; 49 | sort?: number; 50 | sortBy?: string; 51 | dateField?: string; 52 | startDate?: string; 53 | endDate?: string; 54 | pagination?: boolean; 55 | } 56 | 57 | export type PaginatedInputDefaults = PaginatedInputParamsDefaults & PaginatedInputDataDefaults; 58 | 59 | export interface PaginatedOutput { 60 | docs: T[]; 61 | limit: number; 62 | page: number; 63 | totalDocs: number; 64 | totalPages: number; 65 | hasPrevPage: boolean; 66 | hasNextPage: boolean; 67 | prevPage: number; 68 | nextPage: number; 69 | } 70 | 71 | export type SuccessPaginatedOutput = SuccessOutput>; 72 | 73 | export interface ErrorResponse { 74 | status: number; 75 | message: string; 76 | messages?: string[]; 77 | } 78 | 79 | export type AxiosOutput = ( 80 | data: AxiosRequestConfigWithExtraProps, 81 | ) => Promise & E : M>>; 82 | 83 | export type AxiosPaginatedOutput = ( 84 | data: AxiosRequestConfigWithExtraProps, 85 | ) => Promise & E : M>>; 86 | 87 | export interface AxiosSignal { 88 | signal?: AbortSignal; 89 | } 90 | 91 | export type ThrowAxiosError = (error: unknown) => void; 92 | 93 | export type ShowToast = (res: AxiosRes | AxiosErr) => void; 94 | -------------------------------------------------------------------------------- /src/types/cookieStore.type.ts: -------------------------------------------------------------------------------- 1 | import type { ResponseCookie } from 'next/dist/compiled/@edge-runtime/cookies'; 2 | import type { ValueOf } from 'type-fest'; 3 | 4 | import type constants from '@/constants'; 5 | import type { Obj } from '@/types/index'; 6 | 7 | export type DefaultSetOptions = Partial & { 8 | expires?: Exclude; 9 | }; 10 | 11 | export type GetOptions = (options: DefaultSetOptions) => DefaultSetOptions; 12 | 13 | type CookieKey = ValueOf; 14 | 15 | export type GetCookie

= ( 16 | key: CookieKey, 17 | ) => P extends true ? Promise : T; 18 | 19 | export type GetAllCookies

> = () => P extends true 20 | ? Promise 21 | : T; 22 | 23 | export type GetAllSerialized = () => Promise; 24 | 25 | export type SetCookie

= ( 26 | key: CookieKey, 27 | value: string, 28 | options?: DefaultSetOptions, 29 | ) => P extends true ? Promise : T; 30 | 31 | export type DeleteCookie

= ( 32 | key: CookieKey, 33 | ) => P extends true ? Promise : T; 34 | 35 | export interface CookieStoreType { 36 | get: GetCookie; 37 | getAsync: GetCookie; 38 | getAll: GetAllCookies; 39 | getAllAsync: GetAllCookies; 40 | getAllSerialized: GetAllSerialized; 41 | set: SetCookie; 42 | setAsync: SetCookie; 43 | delete: DeleteCookie; 44 | deleteAsync: DeleteCookie; 45 | } 46 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | import type { FC, ReactNode } from 'react'; 2 | import type { ConditionalExcept } from 'type-fest'; 3 | 4 | export type PrimitiveType = string | number | boolean; 5 | 6 | export type Obj = Record; 7 | 8 | export interface RecursiveType { 9 | [key: string]: T | RecursiveType; 10 | } 11 | 12 | interface Children { 13 | children: ReactNode; 14 | } 15 | 16 | export type Component = FC; 17 | 18 | export type Layout = FC; 19 | 20 | export interface NextErrorType { 21 | error: Error & { digest?: string }; 22 | reset: () => void; 23 | } 24 | 25 | // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type 26 | export type RemoveFnType = ConditionalExcept; 27 | -------------------------------------------------------------------------------- /src/types/store.type.ts: -------------------------------------------------------------------------------- 1 | import type { StateCreator, StoreApi } from 'zustand'; 2 | 3 | import type { StoreState } from '@/store'; 4 | import type { ZustandState } from '@/types/zustandState.type'; 5 | 6 | export type StateFromFunctions object)[]> = T extends [ 7 | infer F, 8 | ...infer R, 9 | ] 10 | ? F extends (...args: never[]) => object 11 | ? StateFromFunctions object)[] ? R : []> & ReturnType 12 | : unknown 13 | : unknown; 14 | 15 | export type ZustandStore = StoreApi; 16 | 17 | export type ZustandContextValue = ZustandStore; 18 | 19 | export type CreateStore = (state: ZustandState) => ZustandStore; 20 | 21 | export type UseStore = (selector: (state: StoreState) => T) => T; 22 | 23 | export type SliceCreator = StateCreator; 24 | -------------------------------------------------------------------------------- /src/types/zustandState.type.ts: -------------------------------------------------------------------------------- 1 | import type { OverrideProperties } from 'type-fest'; 2 | 3 | import type { GetProfileOutput } from '@/features/profile/profile.type'; 4 | import type { ThemeSliceInitialState } from '@/store/slices/theme/theme.type'; 5 | 6 | export interface RootLayoutAppProps extends Pick { 7 | user: GetProfileOutput | null | undefined; 8 | } 9 | 10 | export type ZustandState = OverrideProperties; 11 | 12 | export type ZustandProviderProps = ZustandState; 13 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import config from '@/config'; 2 | 3 | // eslint-disable-next-line unicorn/prefer-global-this 4 | export const isServer = typeof window === 'undefined'; 5 | 6 | export const isProduction = config.NODE_ENV === 'production'; 7 | 8 | export const isLive = config.ENV_TYPE === 'production'; 9 | 10 | export const sleep = async (ms: number): Promise => 11 | new Promise((resolve) => { 12 | setTimeout(resolve, ms); 13 | }); 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "noImplicitReturns": true, 12 | "module": "esnext", 13 | "moduleResolution": "bundler", 14 | "resolveJsonModule": true, 15 | "useUnknownInCatchVariables": true, 16 | "isolatedModules": true, 17 | "jsx": "preserve", 18 | "incremental": true, 19 | "plugins": [ 20 | { 21 | "name": "next" 22 | } 23 | ], 24 | "paths": { 25 | "@/*": ["./src/*"], 26 | "@server/*": ["./src/app/api/*"] 27 | } 28 | }, 29 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.mjs", ".next/types/**/*.ts"], 30 | "exclude": ["node_modules"] 31 | } 32 | --------------------------------------------------------------------------------