├── .env.example ├── .github ├── FUNDING.yml └── dependabot.yml ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── eslint.config.mjs ├── headers.config.js ├── next-env.d.ts ├── next.config.ts ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── public ├── file.svg ├── globe.svg ├── next.svg ├── vercel.svg └── window.svg ├── src ├── @types │ ├── declaration.d.ts │ └── global.ts ├── app │ ├── api │ │ └── quotes │ │ │ └── random │ │ │ └── route.ts │ ├── favicon.ico │ ├── layout.tsx │ └── page.ts ├── configs │ ├── envs.ts │ └── sites.ts ├── designs │ ├── styles │ │ ├── custom-utilities.css │ │ └── globals.css │ └── utils │ │ └── cn.ts ├── modules │ ├── Common │ │ └── middlewares │ │ │ └── withVerifyAppkey.ts │ ├── Home │ │ └── Home.page.tsx │ └── Quotes │ │ └── services │ │ ├── Quote.controller.ts │ │ ├── Quote.model.ts │ │ └── routes │ │ └── random.ts └── packages │ ├── hooks │ ├── useClipboard.ts │ ├── useCountdown.ts │ ├── useMounted.ts │ ├── useOutsideClick.ts │ ├── usePooling.ts │ ├── useQueryParams.ts │ ├── useScrollListener.ts │ ├── useToggler.ts │ └── useUpdated.ts │ ├── libs │ ├── Asynchronous │ │ ├── withPromise.ts │ │ └── withRequest.ts │ ├── BaseHttp │ │ ├── index.ts │ │ └── interfaces.ts │ ├── File │ │ └── download.ts │ ├── Performance │ │ ├── debounce.ts │ │ └── throttle.ts │ └── URL │ │ └── queryParamsModifier.ts │ ├── server │ └── base │ │ ├── Controller.ts │ │ ├── HttpError.ts │ │ └── Middleware.ts │ └── utils │ └── metadata.ts ├── tsconfig.json └── webpack.config.js /.env.example: -------------------------------------------------------------------------------- 1 | # Server Environment 2 | SECRET_APP_KEY = xxxxxx 3 | 4 | # Frontend Environment (prefix: `NEXT_PUBLIC_`) 5 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: gadingnst 2 | ko_fi: gadingnst 3 | custom: ['https://trakteer.id/gadingnst', 'https://karyakarsa.com/gadingnst'] 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: npm 5 | directory: '/' 6 | target-branch: 'main' 7 | schedule: 8 | interval: monthly 9 | time: '00:00' 10 | open-pull-requests-limit: 10 11 | reviewers: 12 | - gadingnst 13 | assignees: 14 | - gadingnst 15 | commit-message: 16 | prefix: fix 17 | prefix-development: chore 18 | include: scope 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | !.env.example 36 | 37 | # vercel 38 | .vercel 39 | 40 | # typescript 41 | *.tsbuildinfo 42 | next-env.d.ts 43 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2, 3 | "typescript.preferences.importModuleSpecifier": "non-relative", 4 | "javascript.preferences.importModuleSpecifier": "non-relative", 5 | "files.insertFinalNewline": true, 6 | "files.trimTrailingWhitespace": true, 7 | "typescript.tsdk": "./node_modules/typescript/lib", 8 | "editor.codeActionsOnSave": { 9 | "source.fixAll.eslint": "explicit" 10 | }, 11 | "emmet.includeLanguages":{ 12 | "postcss": "css" 13 | }, 14 | "emmet.syntaxProfiles": { 15 | "postcss": "css" 16 | }, 17 | "files.associations": { 18 | "*.css": "postcss", 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Gading Nasution. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Features 4 | This `starter-template` is packed with: 5 | 6 | - 🎉 Next.js (with [App Directory](https://nextjs.org/docs/app)). 7 | - ⚛️ React. 8 | - ✨ TypeScript. 9 | - 💨 Tailwind CSS - Pre-setup with PostCSS Nesting and Import. 10 | - 👀 SVGR - Pre-Configured import SVG directly transform to React Component with type definitions 11 | - 📈 Path Alias - Import your module in `src` using `@/` prefix, and in `public` using `#/`. 12 | - 📏 ESLint - Find and fix problems in your code. 13 | - 🧩 Pre-built ***components*** to handle dynamic Lazyload, Image and SVG in `packages/components/base`. 14 | - ⚡️ Pre-setup ***backend things*** in `packages/server/` folders. 15 | - 🪄 Pre-built ***utilities*** to handle common things in backend and frontend. 16 | - 🔥 Minimal dependencies & full of customization 17 | 18 | ## Getting Started 19 | 20 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 21 | 22 | ## Getting Started 23 | 24 | First, run the development server: 25 | 26 | ```bash 27 | npm run dev 28 | # or 29 | yarn dev 30 | # or 31 | pnpm dev 32 | ``` 33 | 34 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 35 | 36 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 37 | 38 | `API routes` with [Route handlers](https://nextjs.org/docs/app/building-your-application/routing/router-handlers) can be accessed on [http://localhost:3000/api/jokes](http://localhost:3000/api/jokes). This endpoint can be edited in `app/api/jokes/route.ts`. 39 | 40 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 41 | 42 | ## Recomendation for better development 43 | - [Install Tailwind CSS intellisense](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss) 44 | - [Enable CSS module auto-completion](https://github.com/mrmckeb/typescript-plugin-css-modules#visual-studio-code) 45 | 46 | ## Learn More 47 | 48 | To learn more about Next.js, take a look at the following resources: 49 | 50 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 51 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 52 | 53 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 54 | 55 | ## Deploy on Vercel 56 | 57 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 58 | 59 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 60 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { dirname } from 'path'; 2 | import { fileURLToPath } from 'url'; 3 | import { FlatCompat } from '@eslint/eslintrc'; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname 10 | }); 11 | 12 | const eslintConfig = [ 13 | ...compat.extends('next/core-web-vitals', 'next/typescript'), 14 | { 15 | rules: { 16 | 'semi': ['error', 'always'], // Enforce semicolons at the end of statements 17 | 'indent': ['error', 2, { 'SwitchCase': 1 }], // Enforce 2-space indentation, 1 for switch cases 18 | 'comma-dangle': ['error', 'never'], // Disallow trailing commas 19 | 'comma-spacing': ['error', { 'before': false, 'after': true }], // Require a space after, but not before, commas 20 | 'space-before-blocks': 'error', // Require space before blocks 21 | 'no-multiple-empty-lines': ['error', { 'max': 1 }], // Disallow multiple empty lines 22 | 'object-curly-spacing': ['error', 'always'], // Require spacing inside curly braces 23 | 'array-bracket-spacing': 'error', // Enforce consistent spacing inside array brackets 24 | 'keyword-spacing': 'error', // Enforce consistent spacing before and after keywords 25 | 'arrow-spacing': 'error', // Enforce consistent spacing before and after the arrow in arrow functions 26 | 'space-infix-ops': 'error', // Require spacing around infix operators 27 | 'no-console': 'warn', // Warn when using console.log or similar methods 28 | 'no-useless-catch': 'off', // Allow catch blocks that only rethrow 29 | 'jsx-quotes': ['error', 'prefer-double'], // Enforce double quotes for JSX attributes 30 | 'space-in-parens': ['error', 'never'], // Disallow spaces inside parentheses 31 | 'space-before-function-paren': ['error', 'never'], // Disallow space before function parenthesis 32 | '@/semi': 'error', // Enforce semicolons at the end of statements (custom rule) 33 | '@/space-before-blocks': 'error', // Require space before blocks (custom rule) 34 | '@/explicit-module-boundary-types': 'off', // Disable requiring explicit return types on functions and class methods (custom rule) 35 | '@/no-explicit-any': 'off', // Allow the use of the `any` type (custom rule) 36 | '@/quotes': [ 37 | 'error', 38 | 'single', 39 | { 40 | 'allowTemplateLiterals': true // Allow template literals even when enforcing single quotes (custom rule) 41 | } 42 | ], 43 | 'quotes': ['error', 'single'], // Enforce single quotes for strings 44 | 'eqeqeq': ['error', 'always'], // Enforce strict equality (===) instead of == and != 45 | 'no-unused-vars': 'warn', // Warn about variables that are declared but never used 46 | 'react-hooks/rules-of-hooks': 'error', // Ensure hooks are only called inside functional components or custom hooks 47 | 'react-hooks/exhaustive-deps': 'warn', // Ensure dependencies in useEffect, useCallback, and useMemo are properly defined 48 | 'react/react-in-jsx-scope': 'off', // Disable the rule requiring React to be in scope when using JSX 49 | 'react/jsx-wrap-multilines': ['error', { 50 | 'declaration': 'parens-new-line', // Wrap multiline JSX in parentheses for declarations 51 | 'assignment': 'parens-new-line', // Wrap multiline JSX in parentheses for assignments 52 | 'return': 'parens-new-line', // Wrap multiline JSX in parentheses for returns 53 | 'arrow': 'parens-new-line', // Wrap multiline JSX in parentheses for arrow functions 54 | 'condition': 'parens-new-line', // Wrap multiline JSX in parentheses for conditions 55 | 'logical': 'ignore', // Ignore logical expressions 56 | 'prop': 'ignore' // Ignore props 57 | }] 58 | // '@/type-annotation-spacing': 'error', 59 | // '@/member-delimiter-style': ['error', { 60 | // 'multiline': { 61 | // 'delimiter': 'semi', 62 | // 'requireLast': true 63 | // }, 64 | // 'singleline': { 65 | // 'delimiter': 'semi', 66 | // 'requireLast': true 67 | // }, 68 | // 'multilineDetection': 'brackets' 69 | // }], 70 | } 71 | } 72 | ]; 73 | 74 | export default eslintConfig; 75 | -------------------------------------------------------------------------------- /headers.config.js: -------------------------------------------------------------------------------- 1 | /** @see https://nextjs.org/docs/api-reference/next.config.js/headers */ 2 | function headers() { 3 | return [ 4 | { 5 | // Enable CORS 6 | source: '/api/(.*)', 7 | headers: [ 8 | { key: 'Access-Control-Allow-Credentials', value: 'true' }, 9 | /** disable configuration below if you want same origin */ 10 | { key: 'Access-Control-Allow-Origin', value: '*' }, 11 | { key: 'Access-Control-Allow-Methods', value: 'GET,OPTIONS,PATCH,DELETE,POST,PUT' }, 12 | { key: 'Access-Control-Allow-Headers', value: 'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version, Authorization' } 13 | ] 14 | }, 15 | { 16 | source: '/(.*)', 17 | headers: [ 18 | { 19 | key: 'X-XSS-Protection', 20 | value: '1; mode=block' 21 | }, 22 | { 23 | key: 'X-Content-Type-Options', 24 | value: 'nosniff' 25 | } 26 | ] 27 | } 28 | ]; 29 | } 30 | 31 | module.exports = headers; 32 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. 6 | -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | }; 6 | 7 | export default nextConfig; 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fullstack-next-template", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "next": "15.2.4", 13 | "react": "^19.0.0", 14 | "react-dom": "^19.0.0" 15 | }, 16 | "devDependencies": { 17 | "@eslint/eslintrc": "^3", 18 | "@svgr/webpack": "^8.1.0", 19 | "@tailwindcss/postcss": "^4", 20 | "@types/node": "^20", 21 | "@types/react": "^19", 22 | "@types/react-dom": "^19", 23 | "clsx": "^2.1.1", 24 | "eslint": "^9", 25 | "eslint-config-next": "15.2.4", 26 | "tailwind-merge": "^3.0.2", 27 | "tailwindcss": "^4", 28 | "typescript": "^5" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: ["@tailwindcss/postcss"], 3 | }; 4 | 5 | export default config; 6 | -------------------------------------------------------------------------------- /public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/@types/declaration.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' { 2 | const content: React.FunctionComponent>; 3 | export default content; 4 | } 5 | 6 | declare module '*.svg?url' { 7 | const content: string; 8 | export default content; 9 | } 10 | -------------------------------------------------------------------------------- /src/@types/global.ts: -------------------------------------------------------------------------------- 1 | import { type NextPage } from 'next'; 2 | 3 | export type NextPageComponent = NextPage; 4 | 5 | export interface NextPageProps> { 6 | params: T; 7 | searchParams: { 8 | [key: string]: string|string[]|undefined; 9 | }; 10 | } 11 | 12 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 13 | export interface HttpResponseJson { 14 | code: number; 15 | message?: string; 16 | payload?: T; 17 | errors?: E; 18 | } 19 | -------------------------------------------------------------------------------- /src/app/api/quotes/random/route.ts: -------------------------------------------------------------------------------- 1 | export * from '@/modules/Quotes/services/routes/random'; 2 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gadingnst/fullstack-next-template/04712515c68d60d8048fc8f5e09bc49ad2a4ca37/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { PropsWithChildren } from 'react'; 2 | import type { Metadata } from 'next'; 3 | import { Geist, Geist_Mono } from 'next/font/google'; 4 | import '@/designs/styles/globals.css'; 5 | 6 | const geistSans = Geist({ 7 | variable: '--font-geist-sans', 8 | subsets: ['latin'] 9 | }); 10 | 11 | const geistMono = Geist_Mono({ 12 | variable: '--font-geist-mono', 13 | subsets: ['latin'] 14 | }); 15 | 16 | export const metadata: Metadata = { 17 | title: 'Create Next App', 18 | description: 'Generated by create next app' 19 | }; 20 | 21 | export default function RootLayout({ children }: PropsWithChildren) { 22 | return ( 23 | 24 | 27 | {children} 28 | 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /src/app/page.ts: -------------------------------------------------------------------------------- 1 | import HomePage from '@/modules/Home/Home.page'; 2 | 3 | export default HomePage; 4 | -------------------------------------------------------------------------------- /src/configs/envs.ts: -------------------------------------------------------------------------------- 1 | /** @see https://nextjs.org/docs/basic-features/environment-variables#loading-environment-variables */ 2 | 3 | /** Process ENV */ 4 | export const NODE_ENV = process.env.NODE_ENV || 'production'; 5 | export const DB_HOST = process.env.DB_HOST; 6 | export const DB_USER = process.env.DB_USER; 7 | export const DB_PASSWORD = process.env.DB_PASSWORD; 8 | export const DB_NAME = process.env.DB_NAME; 9 | export const SECRET_APP_KEY = process.env.SECRET_APP_KEY; 10 | -------------------------------------------------------------------------------- /src/configs/sites.ts: -------------------------------------------------------------------------------- 1 | import { NODE_ENV } from './envs'; 2 | 3 | export const IS_DEV = NODE_ENV !== 'production'; 4 | export const SITE_NAME = 'Fullstack Next.js Template'; 5 | -------------------------------------------------------------------------------- /src/designs/styles/custom-utilities.css: -------------------------------------------------------------------------------- 1 | @utility base-container { 2 | max-width: 1600px; 3 | @apply w-full mx-auto px-6; 4 | } 5 | -------------------------------------------------------------------------------- /src/designs/styles/globals.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | @import "./custom-utilities"; 3 | 4 | :root { 5 | --background: #ffffff; 6 | --foreground: #171717; 7 | } 8 | 9 | @theme inline { 10 | --color-background: var(--background); 11 | --color-foreground: var(--foreground); 12 | --font-sans: var(--font-geist-sans); 13 | --font-mono: var(--font-geist-mono); 14 | } 15 | 16 | @media (prefers-color-scheme: dark) { 17 | :root { 18 | --background: #0a0a0a; 19 | --foreground: #ededed; 20 | } 21 | } 22 | 23 | body { 24 | background: var(--background); 25 | color: var(--foreground); 26 | font-family: Arial, Helvetica, sans-serif; 27 | } 28 | -------------------------------------------------------------------------------- /src/designs/utils/cn.ts: -------------------------------------------------------------------------------- 1 | import clsx, { type ClassValue } from 'clsx'; 2 | import { twMerge } from 'tailwind-merge'; 3 | 4 | /** 5 | * clsx + tailwind class merge 6 | * @param classes - class names 7 | * @returns {string} - class names joined by space 8 | */ 9 | function cn(...classes: ClassValue[]): string { 10 | return twMerge(clsx(...classes)); 11 | } 12 | 13 | export default cn; 14 | -------------------------------------------------------------------------------- /src/modules/Common/middlewares/withVerifyAppkey.ts: -------------------------------------------------------------------------------- 1 | import { createMiddleware } from '@/packages/server/base/Middleware'; 2 | import { SECRET_APP_KEY } from '@/configs/envs'; 3 | import Controller from '@/packages/server/base/Controller'; 4 | 5 | /** 6 | * example to screate custom middleware with `createMiddleware HoF` 7 | */ 8 | const withVerifyAppKey = createMiddleware((req, next) => { 9 | const query = new URL(req.url).searchParams; 10 | const key = query.get('key'); 11 | if (key === SECRET_APP_KEY) return next(); 12 | return Controller.sendJSON({ 13 | code: 400, 14 | message: 'Bad request.', 15 | errors: ['Secret key invalid.'] 16 | }); 17 | }); 18 | 19 | export default withVerifyAppKey; 20 | -------------------------------------------------------------------------------- /src/modules/Home/Home.page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Image from 'next/image'; 4 | import Link from 'next/link'; 5 | 6 | export default function HomePage() { 7 | return ( 8 |
9 | {/* Hero */} 10 |
11 | Next.js Logo 19 |

20 | Fullstack Next Template 21 |

22 |

23 | Boilerplate Next.js + TypeScript + TailwindCSS dengan module alias, 24 | SVGR, ESLint, Husky, dan workflow Vercel siap pakai. 25 |

26 | 27 | {/* Credit */} 28 |

29 | by{' '} 30 | 36 | Gading Nasution 37 | 38 |

39 | 40 |
41 | 46 | ⭐ Star on GitHub 47 | 48 | 53 | 🚀 Deploy 54 | 55 |
56 |
57 | 58 | {/* Features */} 59 |
60 |

Key Features

61 |
    62 |
  • 63 | ⚛️ Next.js App Dir + TypeScript 64 |
  • 65 |
  • 66 | 🎨 TailwindCSS pre-setup 67 |
  • 68 |
  • 69 | 🛠️ SVGR for SVG → React 70 |
  • 71 |
  • 72 | 🚦 ESLint preset & rules 73 |
  • 74 |
  • 75 | 🔗 @/ Path Alias 76 |
  • 77 |
78 |
79 | 80 | {/* Getting Started */} 81 |
82 |

83 | Getting Started 84 |

85 |
 86 |           git clone https://github.com/gadingnst/fullstack-next-template.git
87 | cd fullstack-next-template
88 | npm install
89 | npm run dev 90 |
91 |
92 | 93 | {/* Footer */} 94 |
95 | 96 | 📚 Next.js Docs 97 | 98 |
99 |
100 | ); 101 | } 102 | -------------------------------------------------------------------------------- /src/modules/Quotes/services/Quote.controller.ts: -------------------------------------------------------------------------------- 1 | import QuoteModel from '@/modules/Quotes/services/Quote.model'; 2 | import Controller from '@/packages/server/base/Controller'; 3 | 4 | class CQuote extends Controller { 5 | /** 6 | * Use arrow function to create Controller method. 7 | * @see https://www.geeksforgeeks.org/arrow-functions-in-javascript/ 8 | * @param req Request 9 | */ 10 | public random = async() => { 11 | try { 12 | const payload = await QuoteModel.random(); 13 | return this.sendJSON({ 14 | code: 200, 15 | message: 'Success get random quote.', 16 | payload 17 | }); 18 | } catch (err) { 19 | return this.handleError(err); 20 | } 21 | }; 22 | } 23 | 24 | const QuoteController = new CQuote(); 25 | export default QuoteController; 26 | -------------------------------------------------------------------------------- /src/modules/Quotes/services/Quote.model.ts: -------------------------------------------------------------------------------- 1 | import { Http } from '@/packages/libs/BaseHttp'; 2 | 3 | export interface IQuote { 4 | quote: string; 5 | } 6 | 7 | async function random() { 8 | const response = await Http.request('GET', 'https://quotes-api-self.vercel.app/quote'); 9 | return Http.getResponseJson(response); 10 | } 11 | 12 | const QuoteModel = { 13 | random 14 | }; 15 | 16 | export default QuoteModel; 17 | -------------------------------------------------------------------------------- /src/modules/Quotes/services/routes/random.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @route `/api/quotes/random` 3 | * @dir `app/api/quotes/random/route.ts` 4 | */ 5 | 6 | import withVerifyAppKey from '@/modules/Common/middlewares/withVerifyAppkey'; 7 | import QuoteController from '@/modules/Quotes/services/Quote.controller'; 8 | 9 | export const GET = withVerifyAppKey(QuoteController.random); 10 | -------------------------------------------------------------------------------- /src/packages/hooks/useClipboard.ts: -------------------------------------------------------------------------------- 1 | import { useState, useCallback } from 'react'; 2 | 3 | /** 4 | * custom hooks to create Copy Clipboard 5 | * @param value - value to copy 6 | */ 7 | function useClipboard(value: string, delay = 1500) { 8 | const [isCopied, setCopied] = useState(false); 9 | 10 | const copyHandler = useCallback(() => { 11 | if (isCopied) return; 12 | navigator.clipboard.writeText(value); 13 | setCopied(true); 14 | setTimeout(() => { 15 | setCopied(false); 16 | }, delay); 17 | }, [isCopied, value, delay]); 18 | 19 | return { 20 | isCopied, 21 | copyHandler 22 | }; 23 | } 24 | 25 | export default useClipboard; 26 | -------------------------------------------------------------------------------- /src/packages/hooks/useCountdown.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | function useCountdown(initial: number, delayInMs = 1000) { 4 | const [countdown, setCountdown] = useState(initial); 5 | 6 | const resetCountdown = (count: number = initial) => setCountdown(count); 7 | 8 | useEffect(() => { 9 | if (!countdown) return; 10 | const interval = setInterval(() => { 11 | setCountdown(prev => prev - 1); 12 | }, delayInMs); 13 | return () => { 14 | clearInterval(interval); 15 | }; 16 | }, [countdown, delayInMs]); 17 | 18 | return [countdown, resetCountdown] as const; 19 | } 20 | 21 | export default useCountdown; 22 | -------------------------------------------------------------------------------- /src/packages/hooks/useMounted.ts: -------------------------------------------------------------------------------- 1 | import { EffectCallback, useEffect } from 'react'; 2 | 3 | /** 4 | * 5 | * @param callback - The callback to run when the component is mounted 6 | * @returns {void} - void 7 | */ 8 | function useMounted(callback: EffectCallback): void { 9 | // eslint-disable-next-line react-hooks/exhaustive-deps 10 | useEffect(callback, []); 11 | } 12 | 13 | export default useMounted; 14 | -------------------------------------------------------------------------------- /src/packages/hooks/useOutsideClick.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | import { RefObject } from 'react'; 4 | 5 | import useMounted from './useMounted'; 6 | 7 | /** 8 | * React hook that listens for clicks outside of a given refs. 9 | * @param callback - The callback to run when user clicks outside of the elements 10 | * @param refs - The array of ref element to listen to 11 | * @returns {void} - void 12 | */ 13 | function useOutsideClick(callback: (target: HTMLElement) => void, refs: RefObject[]): void { 14 | const handleOutsideClick = (event: MouseEvent) => { 15 | const isOutsideRefs = refs.every(ref => { 16 | const refElement = ref?.current; 17 | const isOutside = refElement && !refElement?.contains(event?.target as Node); 18 | return isOutside; 19 | }); 20 | if (isOutsideRefs) callback(event.target as HTMLElement); 21 | }; 22 | useMounted(() => { 23 | document.addEventListener('mousedown', handleOutsideClick); 24 | return () => { 25 | document.removeEventListener('mousedown', handleOutsideClick); 26 | }; 27 | }); 28 | } 29 | 30 | export default useOutsideClick; 31 | -------------------------------------------------------------------------------- /src/packages/hooks/usePooling.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | import { DependencyList, useCallback, useEffect, useRef } from 'react'; 3 | 4 | function usePooling(callback: (stop: () => void) => void, deps: DependencyList, interval = 5000) { 5 | const intervalRef = useRef(null); 6 | 7 | const stop = useCallback(() => { 8 | if (intervalRef.current) { 9 | clearInterval(intervalRef.current); 10 | } 11 | }, []); 12 | 13 | useEffect(() => { 14 | intervalRef.current = setInterval(() => { 15 | callback(stop); 16 | }, interval); 17 | 18 | return () => { 19 | stop(); 20 | }; 21 | // eslint-disable-next-line react-hooks/exhaustive-deps 22 | }, [callback, interval, stop, ...deps]); 23 | } 24 | 25 | export default usePooling; 26 | -------------------------------------------------------------------------------- /src/packages/hooks/useQueryParams.ts: -------------------------------------------------------------------------------- 1 | import { useSearchParams } from 'next/navigation'; 2 | import { useMemo } from 'react'; 3 | 4 | function useQueryParams() { 5 | const queryReader = useSearchParams(); 6 | 7 | const queryModifier = useMemo(() => { 8 | return new URLSearchParams(queryReader); 9 | }, [queryReader]); 10 | 11 | return { queryReader, queryModifier }; 12 | } 13 | 14 | export default useQueryParams; 15 | -------------------------------------------------------------------------------- /src/packages/hooks/useScrollListener.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | import { RefObject, useCallback, useEffect, useRef } from 'react'; 3 | 4 | type Reference = RefObject | 'window'; 5 | 6 | interface ScrollListenerParams { 7 | scrollY: number; 8 | scrollX: number; 9 | element: T | null; 10 | } 11 | 12 | export type ScrollListenerCallback = (scrollPosition: ScrollListenerParams) => void; 13 | 14 | function getReference(reference: Reference) { 15 | const isReactRef = reference !== 'window'; 16 | const element = (reference as RefObject)?.current; 17 | return { isReactRef, element }; 18 | } 19 | 20 | export function isScrollAtEndX(scrollX: number, element: HTMLElement | 'window', tolerance = 50) { 21 | if (element === 'window') { 22 | return window.innerWidth + window.scrollX >= document.documentElement.scrollWidth - tolerance; 23 | } 24 | return scrollX + element.clientWidth >= element.scrollWidth - tolerance; 25 | } 26 | 27 | export function isScrollAtEndY(scrollY: number, element: HTMLElement|'window', tolerance = 50) { 28 | if (element === 'window') { 29 | return window.innerHeight + window.scrollY >= document.documentElement.scrollHeight - tolerance; 30 | } 31 | return scrollY + element.clientHeight >= element.scrollHeight - tolerance; 32 | } 33 | 34 | /** 35 | * Hooks for handling scroll events on a given reference. 36 | * @param callback - Event handler for scroll events. 37 | * @param reference - The ref of the element to listen to. 38 | */ 39 | function useScrollListener( 40 | callback: ScrollListenerCallback, 41 | reference: Reference 42 | ) { 43 | const callbackRef = useRef(callback); 44 | 45 | useEffect(() => { 46 | callbackRef.current = callback; 47 | }, [callback]); 48 | const handleScroll = useCallback(() => { 49 | const { isReactRef, element } = getReference(reference); 50 | const scrollY = (isReactRef ? element?.scrollTop : window.scrollY) ?? 0; 51 | const scrollX = (isReactRef ? element?.scrollLeft : window.scrollX) ?? 0; 52 | callbackRef.current({ scrollX, scrollY, element }); 53 | }, [reference]); 54 | 55 | useEffect(() => { 56 | const { isReactRef, element } = getReference(reference); 57 | const target = isReactRef ? element : window; 58 | target?.addEventListener('scroll', handleScroll); 59 | return () => { 60 | target?.removeEventListener('scroll', handleScroll); 61 | }; 62 | }, [handleScroll, reference]); 63 | } 64 | 65 | export default useScrollListener; 66 | -------------------------------------------------------------------------------- /src/packages/hooks/useToggler.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from 'react'; 2 | 3 | function useToggler() { 4 | const [isShow, setShow] = useState(false); 5 | 6 | const toggler = useCallback(() => { 7 | setShow(_current => !_current); 8 | }, []); 9 | 10 | return [isShow, toggler, setShow] as const; 11 | } 12 | 13 | export default useToggler; 14 | -------------------------------------------------------------------------------- /src/packages/hooks/useUpdated.ts: -------------------------------------------------------------------------------- 1 | import { EffectCallback, DependencyList, useEffect, useRef } from 'react'; 2 | 3 | /** 4 | * React hooks that run useEffect() hooks only when the dependency changes, 5 | * not when the component is in initial mounted. 6 | * @param callback - The callback to run only when the dependency changes 7 | * @param deps - The dependencies to listen to 8 | * @returns {void} - void 9 | */ 10 | function useUpdated(callback: EffectCallback, deps: DependencyList): void { 11 | const mounted = useRef(false); 12 | useEffect(() => { 13 | if (mounted.current) return callback(); 14 | else mounted.current = true; 15 | // eslint-disable-next-line react-hooks/exhaustive-deps 16 | }, [callback, ...deps]); 17 | } 18 | 19 | export default useUpdated; 20 | -------------------------------------------------------------------------------- /src/packages/libs/Asynchronous/withPromise.ts: -------------------------------------------------------------------------------- 1 | export type IPromiseResult = readonly [E|null, T]; 2 | 3 | export function isPromiseError(error: unknown): error is E { 4 | return error !== null; 5 | } 6 | 7 | async function withPromise(func: () => Promise): Promise> { 8 | try { 9 | const response = await func(); 10 | return [null, response] as const; 11 | } catch (err: unknown) { 12 | return [err as E, null as Awaited] as const; 13 | } 14 | } 15 | 16 | withPromise.isError = isPromiseError; 17 | 18 | export default withPromise; 19 | -------------------------------------------------------------------------------- /src/packages/libs/Asynchronous/withRequest.ts: -------------------------------------------------------------------------------- 1 | import withPromise, { type IPromiseResult, isPromiseError } from './withPromise'; 2 | import type { BaseHttpError } from '../BaseHttp/interfaces'; 3 | 4 | type IRequestResult = IPromiseResult; 5 | 6 | export function isRequestError(error: unknown): error is BaseHttpError { 7 | return isPromiseError(error); 8 | } 9 | 10 | export function is404(error: BaseHttpError|null) { 11 | const { response } = error || {}; 12 | return response?.status === 404; 13 | } 14 | 15 | async function withRequest(func: () => Promise): Promise> { 16 | const [err, result] = await withPromise(func); 17 | if (isRequestError(err)) { 18 | const error = err ?? { 19 | message: 'Internal Server Error', 20 | response: new Response(null, { status: 500, statusText: 'Internal Server Error' }) 21 | } as BaseHttpError; 22 | return [error, null as Awaited] as const; 23 | } 24 | return [null, result] as const; 25 | } 26 | 27 | withRequest.isError = isRequestError; 28 | withRequest.is404 = is404; 29 | 30 | export default withRequest; 31 | -------------------------------------------------------------------------------- /src/packages/libs/BaseHttp/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BaseHttpConfig, 3 | BaseHttpMethod, 4 | BaseHttpError, 5 | BaseHttpResponseJson 6 | } from './interfaces'; 7 | 8 | export const DEFAULT_ERROR_STATUS = 'Unexpected Error.'; 9 | export const DEFAULT_HTTP_ERROR_MESSAGE = 'An unknown error occurred.'; 10 | 11 | export const defaultHttpArgs: BaseHttpConfig = { 12 | baseURL: '' 13 | }; 14 | 15 | /** 16 | * BaseHttp class for making HTTP requests. 17 | * it inherits from the native fetch API 18 | * -- 19 | * usage: 20 | * const Http = new BaseHttp({ baseURL: API_BASE_URL }); 21 | * try { 22 | * Http.request('POST', '/api/v1/users', { body: JSON.stringify({ name: 'John Doe' }) }); 23 | * } catch (error) { 24 | * const { status, statusText, message } = await Http.getErrorResponse(error); 25 | * console.error(status, statusText, message); 26 | * } 27 | */ 28 | class BaseHttp { 29 | public baseURL: string; 30 | public requestInit: RequestInit; 31 | 32 | constructor(args: BaseHttpConfig = defaultHttpArgs) { 33 | const { baseURL, ...reqInit } = args; 34 | this.baseURL = baseURL; 35 | this.requestInit = reqInit; 36 | } 37 | 38 | public get(url: string, args?: Omit) { 39 | return this.request('GET', url, args); 40 | } 41 | 42 | public post(url: string, args?: Omit) { 43 | return this.request('POST', url, args); 44 | } 45 | 46 | public put(url: string, args?: Omit) { 47 | return this.request('PUT', url, args); 48 | } 49 | 50 | public patch(url: string, args?: Omit) { 51 | return this.request('PATCH', url, args); 52 | } 53 | 54 | public delete(url: string, args?: Omit) { 55 | return this.request('DELETE', url, args); 56 | } 57 | 58 | public async request(method: BaseHttpMethod, url: string, args?: Omit) { 59 | const endpoint = `${this.baseURL}${encodeURI(url).trim()}`; 60 | const response = await fetch(endpoint, { 61 | ...this.requestInit, 62 | ...args, 63 | headers: { 64 | 'Content-Type': 'application/json', 65 | ...this.requestInit.headers, 66 | ...args?.headers, 67 | }, 68 | method, 69 | }); 70 | 71 | if (!response.ok) { 72 | const error = new Error( 73 | `HTTP (${method}) ${response.status} error. On: "${endpoint}"` 74 | ) as BaseHttpError; 75 | error.response = response; 76 | throw error; 77 | } 78 | return response; 79 | } 80 | 81 | public async getResponseJson(res: Response): Promise { 82 | return res.json(); 83 | } 84 | 85 | private static getHttpStatusMessage(status: number): string { 86 | switch (status) { 87 | case 400: 88 | return 'The request was invalid or cannot be processed'; 89 | case 401: 90 | return 'Authentication is required to access this resource'; 91 | case 403: 92 | return 'You do not have permission to access this resource'; 93 | case 404: 94 | return 'The requested resource was not found'; 95 | case 429: 96 | return 'Too many requests. Please try again later'; 97 | default: 98 | return status >= 500 99 | ? 'The server encountered an error. Please try again later' 100 | : `Server returned an unexpected error (${status})`; 101 | } 102 | } 103 | 104 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 105 | private static extractErrorMessage(res: any, status: number): string { 106 | if (!res) return BaseHttp.getHttpStatusMessage(status); 107 | return ( 108 | res['error']?.['message'] || 109 | res['error'] || 110 | res['data'] || 111 | res['message'] || 112 | res['msg'] || 113 | BaseHttp.getHttpStatusMessage(status) 114 | ); 115 | } 116 | 117 | public static async getErrorResponse(err: unknown) { 118 | const error = err as BaseHttpError; 119 | if (!error.response) { 120 | return { 121 | status: 0, 122 | statusText: DEFAULT_ERROR_STATUS, 123 | message: error.message 124 | }; 125 | } 126 | 127 | const { response } = error; 128 | let message = DEFAULT_HTTP_ERROR_MESSAGE; 129 | 130 | try { 131 | const res = await response.json(); 132 | message = 133 | typeof res === 'string' && res 134 | ? res 135 | : BaseHttp.extractErrorMessage(res, response.status); 136 | } catch { 137 | try { 138 | const textResponse = await response.text(); 139 | message = 140 | textResponse.includes('') || 141 | textResponse.includes(' = T; 4 | 5 | export interface BaseHttpConfig extends RequestInit { 6 | baseURL: string; 7 | } 8 | 9 | export interface BaseHttpError extends Error { 10 | response?: Response; 11 | } 12 | -------------------------------------------------------------------------------- /src/packages/libs/File/download.ts: -------------------------------------------------------------------------------- 1 | type DownLoadFileParams = { 2 | data: string | Blob; 3 | fileName: string; 4 | fileType: string; 5 | }; 6 | 7 | export async function downloadFile({ data, fileName, fileType }: DownLoadFileParams) { 8 | let blobOrFile: Blob | File; 9 | 10 | if (typeof data === 'string' && (data.startsWith('http://') || data.startsWith('https://'))) { 11 | try { 12 | // Fetch the data if the input is a URL 13 | const response = await fetch(data); 14 | if (!response.ok) { 15 | throw new Error(`Failed to fetch the file: ${response.statusText}`); 16 | } 17 | blobOrFile = await response.blob(); 18 | } catch (error) { 19 | window.open(data, '_blank'); 20 | throw error; 21 | } 22 | } else if (data instanceof Blob) { 23 | blobOrFile = data; 24 | } else { 25 | blobOrFile = new Blob([data], { type: fileType }); 26 | } 27 | 28 | // Create a temporary URL for the blob 29 | const url = URL.createObjectURL(blobOrFile); 30 | 31 | // Create an anchor element to trigger download 32 | const a = document.createElement('a'); 33 | a.href = url; 34 | a.download = fileName; 35 | 36 | // Dispatch a click event to download 37 | const clickEvt = new MouseEvent('click', { 38 | view: window, 39 | bubbles: true, 40 | cancelable: true 41 | }); 42 | a.dispatchEvent(clickEvt); 43 | 44 | // Clean up 45 | a.remove(); 46 | URL.revokeObjectURL(url); 47 | } 48 | -------------------------------------------------------------------------------- /src/packages/libs/Performance/debounce.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | /* eslint-disable @typescript-eslint/no-explicit-any */ 3 | 'use client'; 4 | 5 | import { useCallback, useRef } from 'react'; 6 | 7 | export function withDebounce(func: (...args: T) => R, delay: number) { 8 | let timeout: NodeJS.Timeout; 9 | return (...args: T): R => { 10 | clearTimeout(timeout); 11 | timeout = setTimeout(() => func(...args), delay); 12 | return func(...args); 13 | }; 14 | } 15 | 16 | export function useDebounce(func: (...args: T) => void, delay: number) { 17 | const timeoutRef = useRef(null); 18 | return useCallback((...args: T) => { 19 | if (timeoutRef.current) clearTimeout(timeoutRef.current); 20 | timeoutRef.current = setTimeout(() => { 21 | func(...args); 22 | }, delay); 23 | }, [func, delay]); 24 | } 25 | -------------------------------------------------------------------------------- /src/packages/libs/Performance/throttle.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | /* eslint-disable @typescript-eslint/no-explicit-any */ 3 | 'use client'; 4 | 5 | import { useCallback, useRef } from 'react'; 6 | 7 | export function withThrottle(func: (...args: T) => R, delay: number) { 8 | let lastCall = 0; 9 | return (...args: T): R | void => { 10 | const now = Date.now(); 11 | if (now - lastCall < delay) return; 12 | lastCall = now; 13 | return func(...args); 14 | }; 15 | } 16 | 17 | export function useThrottle(func: (...args: T) => void, delay: number) { 18 | const lastCallRef = useRef(0); 19 | return useCallback((...args: T) => { 20 | const now = Date.now(); 21 | if (now - lastCallRef.current < delay) return; 22 | lastCallRef.current = now; 23 | func(...args); 24 | }, [func, delay]); 25 | } 26 | -------------------------------------------------------------------------------- /src/packages/libs/URL/queryParamsModifier.ts: -------------------------------------------------------------------------------- 1 | export const parseQuery = (url: string): Record => { 2 | const query = url.split('?')[1]; 3 | return query ? query.split('&').reduce((acc, item) => { 4 | const [key, value] = item.split('='); 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 6 | (acc as any)[key] = value; 7 | return acc; 8 | }, {}) : {}; 9 | }; 10 | 11 | export const stringifyQuery = (query: Record): string => { 12 | return Object.entries(query).reduce((acc, [key, value]) => { 13 | const val = Array.isArray(value) ? value.join(',') : value; 14 | if (val) acc += `${key}=${val}&`; 15 | return acc; 16 | }, '').slice(0, -1); 17 | }; 18 | -------------------------------------------------------------------------------- /src/packages/server/base/Controller.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | 3 | import type { HttpResponseJson } from '@/@types/global'; 4 | import HttpError from '@/packages/server/base/HttpError'; 5 | 6 | abstract class Controller { 7 | protected static response = NextResponse; 8 | 9 | sendJSON(data: HttpResponseJson) { 10 | return Controller.sendJSON(data); 11 | } 12 | 13 | sendResponse(stream: BodyInit, options: ResponseInit) { 14 | return Controller.sendResponse(stream, options); 15 | } 16 | 17 | setError(code: number, errors: E, message?: string) { 18 | return Controller.setError(code, errors, message); 19 | } 20 | 21 | handleError(error: Error | unknown) { 22 | return Controller.handleError(error); 23 | } 24 | 25 | logError(error: Error | unknown) { 26 | return Controller.logError(error); 27 | } 28 | 29 | /** static methods */ 30 | static sendJSON(data: HttpResponseJson) { 31 | return NextResponse.json(data, { 32 | status: data.code 33 | }); 34 | } 35 | 36 | static sendResponse(stream: BodyInit, options: ResponseInit) { 37 | return new NextResponse(stream, options); 38 | } 39 | 40 | static setError(code: number, errors: E, message?: string) { 41 | throw new HttpError(code, errors, message ?? 'HTTP errors has occured.'); 42 | } 43 | 44 | static handleError(error: Error | unknown) { 45 | return HttpError.handle(error as Error); 46 | } 47 | 48 | static logError(error: Error | unknown) { 49 | return HttpError.logError(error as Error); 50 | } 51 | } 52 | 53 | export default Controller; 54 | -------------------------------------------------------------------------------- /src/packages/server/base/HttpError.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import { NextResponse } from 'next/server'; 3 | 4 | class HttpError extends Error { 5 | constructor(code: number, errors: E, message: string, payload?: T) { 6 | const responseError = { code, errors, message, payload }; 7 | super(JSON.stringify(responseError)); 8 | Object.setPrototypeOf(this, this.constructor.prototype); 9 | } 10 | 11 | public static logError(err: Error) { 12 | console.error(err); 13 | } 14 | 15 | public static handle(err: Error) { 16 | HttpError.logError(err); 17 | 18 | if (err instanceof this) { 19 | const error = JSON.parse(err.message); 20 | return NextResponse.json(error, { 21 | status: error.code 22 | }); 23 | } 24 | 25 | if (err?.message?.includes('Unexpected end of JSON input')) { 26 | const statusCode = 400; 27 | return NextResponse.json({ 28 | code: statusCode, 29 | message: 'Bad Request.', 30 | errors: ['Request body is required'] 31 | }, { 32 | status: statusCode 33 | }); 34 | } 35 | 36 | const statusCode = 500; 37 | return NextResponse.json({ 38 | code: statusCode, 39 | message: 'Internal server error.', 40 | errors: ['An unknown error in server has occured.'] 41 | }, { 42 | status: statusCode 43 | }); 44 | } 45 | } 46 | 47 | export default HttpError; 48 | -------------------------------------------------------------------------------- /src/packages/server/base/Middleware.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | export type NextFunction = () => void; 4 | export type Handler = (req: Request) => void; 5 | export type MiddlewareHandler = (req: Request, next: NextFunction) => void; 6 | 7 | export const createMiddleware = (handler: MiddlewareHandler) => (next: Handler) => (req: Request) => { 8 | const nextHandler = () => next(req); 9 | return handler(req, nextHandler); 10 | }; 11 | -------------------------------------------------------------------------------- /src/packages/utils/metadata.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | import type { Metadata, ResolvingMetadata } from 'next'; 3 | 4 | import type { NextPageProps } from '@/@types/global'; 5 | 6 | type ICallbackGenerateMetadata = (props: NextPageProps, parent: ResolvingMetadata) => Promise; 7 | 8 | /** 9 | * @usage 10 | * export const metadata = withMetadata({ 11 | * title: 'My Title' 12 | * }); 13 | */ 14 | export function withMetadata(metadata: Metadata): Metadata { 15 | return metadata; 16 | } 17 | 18 | /** 19 | * @usage 20 | * type Props = { 21 | * params: { productId: string; } 22 | * } 23 | * 24 | * export const generateMetadata = withGenerateMetadata(async(props, parent) => { 25 | * const productName = await getProductName(props.params.productId); 26 | * return { 27 | * title: productName 28 | * }; 29 | * }) 30 | */ 31 | export function withGenerateMetadata(cb: ICallbackGenerateMetadata) { 32 | return (props: NextPageProps, parent: ResolvingMetadata) => cb(props, parent); 33 | } 34 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 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 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "typeRoots": [ 23 | "./node_modules/@types", 24 | "./src/@types" 25 | ], 26 | "paths": { 27 | "@/*": [ 28 | "./src/*" 29 | ], 30 | "#/*": [ 31 | "./public/*" 32 | ] 33 | } 34 | }, 35 | "include": [ 36 | "src/@types/declaration.d.ts", 37 | "next-env.d.ts", 38 | "next.config.js", 39 | "src/**/*.module.css", 40 | "src/**/*.css", 41 | "src/**/*.ts", 42 | "src/**/*.tsx", 43 | ".next/types/**/*.ts" 44 | ], 45 | "exclude": [ 46 | "node_modules", 47 | "dist", 48 | ".next" 49 | ], 50 | "ts-node": { 51 | "compilerOptions": { 52 | "module": "commonjs" 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /** @see https://nextjs.org/docs/api-reference/next.config.js/custom-webpack-config */ 2 | function webpack(config) { 3 | /** 4 | * handle SVGR module 5 | * Configures webpack to handle SVG files with SVGR. SVGR optimizes and transforms SVG files into React components. 6 | * @see https://react-svgr.com/docs/next/ 7 | * @see https://react-svgr.com/docs/webpack/ 8 | */ 9 | 10 | // Grab the existing rule that handles SVG imports 11 | const fileLoaderRule = config.module.rules.find((rule) => rule.test?.test?.('.svg')); 12 | 13 | config.module.rules.push( 14 | // Reapply the existing rule, but only for svg imports ending in ?url 15 | { 16 | ...fileLoaderRule, 17 | test: /\.svg$/i, 18 | resourceQuery: /url/ // *.svg?url 19 | }, 20 | // Convert all other *.svg imports to React components 21 | { 22 | test: /\.svg$/i, 23 | 24 | issuer: fileLoaderRule.issuer, 25 | resourceQuery: { not: [...fileLoaderRule.resourceQuery.not, /url/] }, // exclude if *.svg?url 26 | use: [ 27 | { 28 | loader: '@svgr/webpack', 29 | options: { 30 | typescript: true, 31 | ext: 'tsx' 32 | } 33 | } 34 | ] 35 | } 36 | ); 37 | 38 | // Modify the file loader rule to ignore *.svg, since we have it handled now. 39 | fileLoaderRule.exclude = /\.svg$/i; 40 | 41 | /** End Config SVGR */ 42 | 43 | return config; 44 | } 45 | 46 | module.exports = webpack; 47 | --------------------------------------------------------------------------------