├── .env.example ├── .eslintrc.js ├── .github ├── FUNDING.yml ├── issue-branch.yml ├── pull_request_template.md └── workflows │ ├── create-branch.yml │ ├── issue-autolink.yml │ ├── lint.yml │ └── release-please.yml ├── .gitignore ├── .husky ├── commit-msg ├── post-merge └── pre-commit ├── .npmrc ├── .nvmrc ├── .prettierignore ├── .prettierrc.js ├── .vscode ├── css.code-snippets ├── extensions.json ├── settings.json └── typescriptreact.code-snippets ├── CHANGELOG.md ├── README.md ├── commitlint.config.js ├── jest.config.js ├── jest.setup.js ├── next-env.d.ts ├── next-sitemap.config.js ├── next.config.js ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── public ├── favicon.ico ├── favicon │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ └── site.webmanifest ├── fonts │ └── inter-var-latin.woff2 ├── images │ ├── new-tab.png │ └── og.jpg └── svg │ ├── Logo.svg │ └── Vercel.svg ├── src ├── __mocks__ │ └── svg.tsx ├── __tests__ │ └── pages │ │ └── HomePage.test.tsx ├── app │ ├── api │ │ └── hello │ │ │ └── route.ts │ ├── components │ │ ├── layout.tsx │ │ └── page.tsx │ ├── error.tsx │ ├── layout.tsx │ ├── not-found.tsx │ └── page.tsx ├── components │ ├── NextImage.tsx │ ├── Skeleton.tsx │ ├── buttons │ │ ├── Button.tsx │ │ ├── IconButton.tsx │ │ └── TextButton.tsx │ └── links │ │ ├── ArrowLink.tsx │ │ ├── ButtonLink.tsx │ │ ├── IconLink.tsx │ │ ├── PrimaryLink.tsx │ │ ├── UnderlineLink.tsx │ │ └── UnstyledLink.tsx ├── constant │ ├── config.ts │ └── env.ts ├── lib │ ├── __tests__ │ │ └── og.test.ts │ ├── env.ts │ ├── helper.ts │ ├── logger.ts │ ├── og.ts │ └── utils.ts └── styles │ ├── colors.css │ └── globals.css ├── tailwind.config.ts ├── tsconfig.json └── vercel.json /.env.example: -------------------------------------------------------------------------------- 1 | # !STARTERCONF Duplicate this to .env.local 2 | 3 | # DEVELOPMENT TOOLS 4 | # Ideally, don't add them to production deployment envs 5 | # !STARTERCONF Change to true if you want to log data 6 | NEXT_PUBLIC_SHOW_LOGGER="false" -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | node: true, 6 | }, 7 | plugins: ['@typescript-eslint', 'simple-import-sort', 'unused-imports'], 8 | extends: [ 9 | 'eslint:recommended', 10 | 'next', 11 | 'next/core-web-vitals', 12 | 'plugin:@typescript-eslint/recommended', 13 | 'prettier', 14 | ], 15 | rules: { 16 | 'no-unused-vars': 'off', 17 | 'no-console': 'warn', 18 | '@typescript-eslint/explicit-module-boundary-types': 'off', 19 | 'react/no-unescaped-entities': 'off', 20 | 21 | 'react/display-name': 'off', 22 | 'react/jsx-curly-brace-presence': [ 23 | 'warn', 24 | { props: 'never', children: 'never' }, 25 | ], 26 | 27 | //#region //*=========== Unused Import =========== 28 | '@typescript-eslint/no-unused-vars': 'off', 29 | 'unused-imports/no-unused-imports': 'warn', 30 | 'unused-imports/no-unused-vars': [ 31 | 'warn', 32 | { 33 | vars: 'all', 34 | varsIgnorePattern: '^_', 35 | args: 'after-used', 36 | argsIgnorePattern: '^_', 37 | }, 38 | ], 39 | //#endregion //*======== Unused Import =========== 40 | 41 | //#region //*=========== Import Sort =========== 42 | 'simple-import-sort/exports': 'warn', 43 | 'simple-import-sort/imports': [ 44 | 'warn', 45 | { 46 | groups: [ 47 | // ext library & side effect imports 48 | ['^@?\\w', '^\\u0000'], 49 | // {s}css files 50 | ['^.+\\.s?css$'], 51 | // Lib and hooks 52 | ['^@/lib', '^@/hooks'], 53 | // static data 54 | ['^@/data'], 55 | // components 56 | ['^@/components', '^@/container'], 57 | // zustand store 58 | ['^@/store'], 59 | // Other imports 60 | ['^@/'], 61 | // relative paths up until 3 level 62 | [ 63 | '^\\./?$', 64 | '^\\.(?!/?$)', 65 | '^\\.\\./?$', 66 | '^\\.\\.(?!/?$)', 67 | '^\\.\\./\\.\\./?$', 68 | '^\\.\\./\\.\\.(?!/?$)', 69 | '^\\.\\./\\.\\./\\.\\./?$', 70 | '^\\.\\./\\.\\./\\.\\.(?!/?$)', 71 | ], 72 | ['^@/types'], 73 | // other that didnt fit in 74 | ['^'], 75 | ], 76 | }, 77 | ], 78 | //#endregion //*======== Import Sort =========== 79 | }, 80 | globals: { 81 | React: true, 82 | JSX: true, 83 | }, 84 | }; 85 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # !STARTERCONF You can delete this file :) Your support is much appreciated! 2 | # These are supported funding model platforms 3 | 4 | github: theodorusclarence 5 | patreon: # Replace with a single Patreon username 6 | open_collective: # Replace with a single Open Collective username 7 | ko_fi: # Replace with a single Ko-fi username 8 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 9 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 10 | liberapay: # Replace with a single Liberapay username 11 | issuehunt: # Replace with a single IssueHunt username 12 | otechie: # Replace with a single Otechie username 13 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 14 | custom: ['https://saweria.co/theodorusclarence'] 15 | -------------------------------------------------------------------------------- /.github/issue-branch.yml: -------------------------------------------------------------------------------- 1 | # https://github.com/robvanderleek/create-issue-branch#option-2-configure-github-action 2 | 3 | # ex: i4-lower_camel_upper 4 | branchName: 'i${issue.number}-${issue.title,}' 5 | branches: 6 | - label: epic 7 | skip: true 8 | - label: debt 9 | skip: true 10 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Description & Technical Solution 2 | 3 | Describe problems, if any, clearly and concisely. 4 | Summarize the impact to the system. 5 | Please also include relevant motivation and context. 6 | Please include a summary of the technical solution and how it solves the problem. 7 | 8 | # Checklist 9 | 10 | - [ ] I have commented my code, particularly in hard-to-understand areas. 11 | - [ ] Already rebased against main branch. 12 | 13 | # Screenshots 14 | 15 | Provide screenshots or videos of the changes made if any. 16 | -------------------------------------------------------------------------------- /.github/workflows/create-branch.yml: -------------------------------------------------------------------------------- 1 | name: Create Branch from Issue 2 | 3 | on: 4 | issues: 5 | types: [assigned] 6 | 7 | jobs: 8 | create_issue_branch_job: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Create Issue Branch 12 | uses: robvanderleek/create-issue-branch@main 13 | env: 14 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 15 | -------------------------------------------------------------------------------- /.github/workflows/issue-autolink.yml: -------------------------------------------------------------------------------- 1 | name: 'Issue Autolink' 2 | on: 3 | pull_request: 4 | types: [opened] 5 | 6 | jobs: 7 | issue-links: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | pull-requests: write 11 | steps: 12 | - uses: tkt-actions/add-issue-links@v1.8.1 13 | with: 14 | repo-token: '${{ secrets.GITHUB_TOKEN }}' 15 | branch-prefix: 'i' 16 | resolve: 'true' 17 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Code Check 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: {} 7 | 8 | concurrency: 9 | group: ${{ github.job }}-${{ github.ref }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | lint: 14 | name: ⬣ ESLint, ʦ TypeScript, 💅 Prettier, and 🃏 Test 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: ⬇️ Checkout repo 18 | uses: actions/checkout@v4 19 | 20 | - name: 🤌 Setup pnpm 21 | uses: pnpm/action-setup@v4 22 | with: 23 | version: 9 24 | 25 | - name: ⎔ Setup node 26 | uses: actions/setup-node@v4 27 | with: 28 | node-version: 20 29 | cache: 'pnpm' 30 | 31 | - name: 📥 Download deps 32 | run: pnpm install --frozen-lockfile 33 | 34 | - name: 🔬 Lint 35 | run: pnpm run lint:strict 36 | 37 | - name: 🔎 Type check 38 | run: pnpm run typecheck 39 | 40 | - name: 💅 Prettier check 41 | run: pnpm run format:check 42 | 43 | - name: 🃏 Run jest 44 | run: pnpm run test 45 | -------------------------------------------------------------------------------- /.github/workflows/release-please.yml: -------------------------------------------------------------------------------- 1 | name: release-please 2 | on: 3 | # !STARTERCONF Choose your preferred event 4 | # !Option 1: Manual Trigger from GitHub 5 | workflow_dispatch: 6 | # !Option 2: Release on every push on main branch 7 | # push: 8 | # branches: 9 | # - main 10 | jobs: 11 | release-please: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: google-github-actions/release-please-action@v3 15 | with: 16 | release-type: node 17 | package-name: release-please-action 18 | -------------------------------------------------------------------------------- /.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.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env 30 | .env*.local 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | 39 | # next-sitemap 40 | robots.txt 41 | sitemap.xml 42 | sitemap-*.xml -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit "$1" 5 | -------------------------------------------------------------------------------- /.husky/post-merge: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | pnpm install 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | enable-pre-post-scripts=true -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v20.10.0 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | .next 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | .pnpm-debug.log* 28 | 29 | # local env files 30 | .env.local 31 | .env.development.local 32 | .env.test.local 33 | .env.production.local 34 | 35 | # vercel 36 | .vercel 37 | 38 | # changelog 39 | CHANGELOG.md 40 | 41 | pnpm-lock.yaml 42 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | arrowParens: 'always', 3 | singleQuote: true, 4 | jsxSingleQuote: true, 5 | tabWidth: 2, 6 | semi: true, 7 | }; 8 | -------------------------------------------------------------------------------- /.vscode/css.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | "Region CSS": { 3 | "prefix": "regc", 4 | "body": [ 5 | "/* #region /**=========== ${1} =========== */", 6 | "$0", 7 | "/* #endregion /**======== ${1} =========== */" 8 | ] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | // Tailwind CSS Intellisense 4 | "bradlc.vscode-tailwindcss", 5 | "esbenp.prettier-vscode", 6 | "dbaeumer.vscode-eslint", 7 | "aaron-bond.better-comments" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "css.validate": false, 3 | "editor.formatOnSave": true, 4 | "editor.tabSize": 2, 5 | "editor.codeActionsOnSave": { 6 | "source.fixAll": "explicit" 7 | }, 8 | "[css]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, 9 | // Tailwind CSS Autocomplete, add more if used in projects 10 | "tailwindCSS.classAttributes": [ 11 | "class", 12 | "className", 13 | "classNames", 14 | "containerClassName" 15 | ], 16 | "typescript.preferences.importModuleSpecifier": "non-relative" 17 | } 18 | -------------------------------------------------------------------------------- /.vscode/typescriptreact.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | //#region //*=========== React =========== 3 | "import React": { 4 | "prefix": "ir", 5 | "body": ["import * as React from 'react';"] 6 | }, 7 | "React.useState": { 8 | "prefix": "us", 9 | "body": [ 10 | "const [${1}, set${1/(^[a-zA-Z])(.*)/${1:/upcase}${2}/}] = React.useState<$3>(${2:initial${1/(^[a-zA-Z])(.*)/${1:/upcase}${2}/}})$0" 11 | ] 12 | }, 13 | "React.useEffect": { 14 | "prefix": "uf", 15 | "body": ["React.useEffect(() => {", " $0", "}, []);"] 16 | }, 17 | "React.useReducer": { 18 | "prefix": "ur", 19 | "body": [ 20 | "const [state, dispatch] = React.useReducer(${0:someReducer}, {", 21 | " ", 22 | "})" 23 | ] 24 | }, 25 | "React.useRef": { 26 | "prefix": "urf", 27 | "body": ["const ${1:someRef} = React.useRef($0)"] 28 | }, 29 | "React Functional Component": { 30 | "prefix": "rc", 31 | "body": [ 32 | "import * as React from 'react';\n", 33 | "export default function ${1:${TM_FILENAME_BASE}}() {", 34 | " return (", 35 | "
", 36 | " $0", 37 | "
", 38 | " )", 39 | "}" 40 | ] 41 | }, 42 | "React Functional Component with Props": { 43 | "prefix": "rcp", 44 | "body": [ 45 | "import * as React from 'react';\n", 46 | "import clsxm from '@/lib/clsxm';\n", 47 | "type ${1:${TM_FILENAME_BASE}}Props= {\n", 48 | "} & React.ComponentPropsWithoutRef<'div'>\n", 49 | "export default function ${1:${TM_FILENAME_BASE}}({className, ...rest}: ${1:${TM_FILENAME_BASE}}Props) {", 50 | " return (", 51 | "
", 52 | " $0", 53 | "
", 54 | " )", 55 | "}" 56 | ] 57 | }, 58 | //#endregion //*======== React =========== 59 | 60 | //#region //*=========== Commons =========== 61 | "Region": { 62 | "prefix": "reg", 63 | "scope": "javascript, typescript, javascriptreact, typescriptreact", 64 | "body": [ 65 | "//#region //*=========== ${1} ===========", 66 | "${TM_SELECTED_TEXT}$0", 67 | "//#endregion //*======== ${1} ===========" 68 | ] 69 | }, 70 | "Region CSS": { 71 | "prefix": "regc", 72 | "scope": "css, scss", 73 | "body": [ 74 | "/* #region /**=========== ${1} =========== */", 75 | "${TM_SELECTED_TEXT}$0", 76 | "/* #endregion /**======== ${1} =========== */" 77 | ] 78 | }, 79 | //#endregion //*======== Commons =========== 80 | 81 | //#region //*=========== Next.js =========== 82 | "Next Pages": { 83 | "prefix": "np", 84 | "body": [ 85 | "import * as React from 'react';\n", 86 | "import Layout from '@/components/layout/Layout';", 87 | "import Seo from '@/components/Seo';\n", 88 | "export default function ${1:${TM_FILENAME_BASE/(^[a-zA-Z])(.*)/${1:/upcase}${2}/}}Page() {", 89 | " return (", 90 | " ", 91 | " \n", 92 | "
\n", 93 | "
", 94 | "
", 95 | " $0", 96 | "
", 97 | "
", 98 | "
", 99 | "
", 100 | " )", 101 | "}" 102 | ] 103 | }, 104 | "Next API": { 105 | "prefix": "napi", 106 | "body": [ 107 | "import { NextApiRequest, NextApiResponse } from 'next';\n", 108 | "export default async function handler(req: NextApiRequest, res: NextApiResponse) {", 109 | " if (req.method === 'GET') {", 110 | " res.status(200).json({ name: 'Bambang' });", 111 | " } else {", 112 | " res.status(405).json({ message: 'Method Not Allowed' });", 113 | " }", 114 | "}" 115 | ] 116 | }, 117 | "Get Static Props": { 118 | "prefix": "gsp", 119 | "body": [ 120 | "export const getStaticProps = async (context: GetStaticPropsContext) => {", 121 | " return {", 122 | " props: {}", 123 | " };", 124 | "}" 125 | ] 126 | }, 127 | "Get Static Paths": { 128 | "prefix": "gspa", 129 | "body": [ 130 | "export const getStaticPaths: GetStaticPaths = async () => {", 131 | " return {", 132 | " paths: [", 133 | " { params: { $1 }}", 134 | " ],", 135 | " fallback: ", 136 | " };", 137 | "}" 138 | ] 139 | }, 140 | "Get Server Side Props": { 141 | "prefix": "gssp", 142 | "body": [ 143 | "export const getServerSideProps = async (context: GetServerSidePropsContext) => {", 144 | " return {", 145 | " props: {}", 146 | " };", 147 | "}" 148 | ] 149 | }, 150 | "Infer Get Static Props": { 151 | "prefix": "igsp", 152 | "body": "InferGetStaticPropsType" 153 | }, 154 | "Infer Get Server Side Props": { 155 | "prefix": "igssp", 156 | "body": "InferGetServerSidePropsType" 157 | }, 158 | "Import useRouter": { 159 | "prefix": "imust", 160 | "body": ["import { useRouter } from 'next/router';"] 161 | }, 162 | "Import Next Image": { 163 | "prefix": "imimg", 164 | "body": ["import Image from 'next/image';"] 165 | }, 166 | "Import Next Link": { 167 | "prefix": "iml", 168 | "body": ["import Link from 'next/link';"] 169 | }, 170 | //#endregion //*======== Next.js =========== 171 | 172 | //#region //*=========== Snippet Wrap =========== 173 | "Wrap with Fragment": { 174 | "prefix": "ff", 175 | "body": ["<>", "\t${TM_SELECTED_TEXT}", ""] 176 | }, 177 | "Wrap with clsx": { 178 | "prefix": "cx", 179 | "body": ["{clsx([${TM_SELECTED_TEXT}$0])}"] 180 | }, 181 | "Wrap with clsxm": { 182 | "prefix": "cxm", 183 | "body": ["{clsxm([${TM_SELECTED_TEXT}$0, className])}"] 184 | }, 185 | //#endregion //*======== Snippet Wrap =========== 186 | 187 | "Logger": { 188 | "prefix": "lg", 189 | "body": [ 190 | "logger({ ${1:${CLIPBOARD}} }, '${TM_FILENAME} line ${TM_LINE_NUMBER}')" 191 | ] 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # ts-nextjs-tailwind-starter changelog 4 | 5 | This changelog is manually generated and not accurate with the package.json, only to show the changes since the last release. 6 | 7 | ## 1.0.0 - 2023-07-17 8 | 9 | ### New Features 10 | 11 | - #### Next.js App Router 12 | 13 | Now uses the new app directory structure. 14 | 15 | ### Improvements & Bug Fixes 16 | 17 | - #### Rename `clsxm` to `cn` 18 | 19 | For better support with shadcn/ui 20 | 21 | - #### Faster Lint Actions 22 | 23 | Lint jobs is now merged into one for faster performance, also updated the concurrency rule 24 | 25 | ## 0.5.4 - 2022-07-22 26 | 27 | ### New Features 28 | 29 | - #### Release Please 30 | 31 | Standard Version is now deprecated, and ts-nextjs-tailwind-starter is now using release please. Activate them on `.github/workflows/release-please` 32 | 33 | ### Improvements & Bug Fixes 34 | 35 | - #### More Efficient Lint Actions 36 | 37 | Lint workflow is now cached and will cancel previous run if there are 2 concurrent runs. 38 | 39 | ## 0.5.3 - 2022-02-27 40 | 41 | ### New Features 42 | 43 | - #### Shimmer for NextImage and Skeleton 44 | 45 | Addition of shimmer & blur placeholder for NextImage, and new Skeleton Component with shimmer effect. 46 | 47 | https://user-images.githubusercontent.com/55318172/155867729-8c3176ad-ede4-4443-b42b-780517615e5a.mp4 48 | 49 | 50 | - #### Support for SVGR 51 | 52 | You can directly import SVG like 53 | 54 | ```tsx 55 | import Vercel from '~/svg/Vercel.svg'; 56 | 57 | 58 | ``` 59 | 60 | - #### Public Folder Path Mapping 61 | 62 | Easily access public folder with `~/` prefix. 63 | 64 | - #### Tailwind CSS Prettier Sorter 65 | 66 | ts-nextjs-tailwind-starter now use first-party plugin `prettier-plugin-tailwindcss` 67 | 68 | ### Improvements & Bug Fixes 69 | 70 | - #### Layout Declared Twice 71 | 72 | Fix issue where adding elements to Layout ends up rendering them twice 73 | 74 | - #### ESLint Curly Brace Rule 75 | 76 | New autofixable rule 77 | 78 | ```tsx 79 | props={'hi'} 80 | 81 | will become 82 | 83 | props='hi' 84 | ``` 85 | 86 | ## 0.5.2 - 2021-12-30 87 | 88 | ### New Features 89 | 90 | - #### New Component: PrimaryLink 91 | 92 | Add a link component with accent color on top of UnstyledLink. 93 | 94 | ### Improvements & Bug Fixes 95 | 96 | - #### Unused Import ESlint Autofix 97 | 98 | Unused import will automatically be removed by the ESlint autofix. 99 | 100 | - #### Renamed CustomLink to UnderlineLink 101 | 102 | This is to compensate the new PrimaryLink component 103 | 104 | - #### Primary Button & ButtonLink Shade 105 | 106 | The shade of the button is now using the `500` instead of `400`. 107 | 108 | 109 | ## 0.5.1 - 2021-12-26 110 | 111 | ### New Features 112 | 113 | - #### New Snippets Wrap: clsx and fragment `<>` 114 | 115 | You can select text then wrap it with clsx or React.Fragment shorthand. 116 | 117 | https://user-images.githubusercontent.com/55318172/147401848-3db5dba0-ef71-4f25-9f47-c7908beba69e.mp4 118 | 119 | 120 | ## 0.5.0 - 2021-12-21 121 | 122 | ### New Features 123 | 124 | - #### Expansion Pack 125 | You can easily add expansion such as React Hook Form + Components, Storybook, and more just using a single command line. 126 | 127 | https://user-images.githubusercontent.com/55318172/146631994-e1cac137-1664-4cfe-950b-a96decc1eaa6.mp4 128 | 129 | Check out the [expansion pack repository](https://github.com/theodorusclarence/expansion-pack) for the commands 130 | 131 | ### Improvements & Bug Fixes 132 | 133 | - #### Can't Use Layout Fill on NextImage 134 | 135 | Using layout fill will make the width and height optional 136 | 137 | - #### Vertically center Button & ButtonLink 138 | 139 | Adds `items-center` to make the button vertically centered 140 | 141 | 142 | ## 0.4.1 - 2021-12-12 143 | 144 | ### New Features 145 | 146 | - #### Tailwind CSS v3 147 | 148 | The color palette configuration is also updated accordingly. 149 | 150 | ## 0.4.0 - 2021-12-02 151 | 152 | ### New Features 153 | 154 | - #### Button & ButtonLink Variants 155 | 156 | - New Variant: **Outline** and **Ghost**, you can also add `isDarkBg` prop if you are using these variants with dark background. 157 | - Animated Underline style on **Primary**, **Dark**, **Light** is removed 158 | - Added `ring-primary-500` on `focus-visible` 159 | 160 | ![Button Variants](https://user-images.githubusercontent.com/55318172/144385213-632b3e1f-9a0e-4184-82e0-7905ee3318b4.gif) 161 | 162 | - #### ArrowLink 163 | 164 | 165 | Adds an animated arrow, this component is Polymorphic, the default element is `CustomLink`, you can extend it with `as` prop. 166 | 167 | ```tsx 168 | 173 | Register now 174 | 175 | ``` 176 | 177 | ![Arrow Link Feature](https://user-images.githubusercontent.com/55318172/144385991-f3521d52-e0a8-49c5-8e87-409231fdd5b6.gif) 178 | 179 | - #### Change default theme to white 180 | 181 | | ![Home Page](https://user-images.githubusercontent.com/55318172/144386763-00e6c3fd-ee2e-4c9e-87f8-18b036bdc2e1.png) | ![404](https://user-images.githubusercontent.com/55318172/144386764-0e4b4fb0-35a8-4725-a795-f998b06543a1.png) | 182 | | - | - | 183 | 184 | ### Improvements & Bug Fixes 185 | 186 | - #### Split Next.js Link Props Type 187 | 188 | Now, to add props to Next.js `` component, you can use `nextLinkProps`. 189 | 190 | ```tsx 191 | 197 | Link 198 | 199 | ``` 200 | 201 | The rest of `` props can be directly added as a prop. 202 | 203 | - #### Add Motion Safe to Animations 204 | 205 | All components animation respect user preference about motion. 206 | 207 | ## 0.3.0 - 2021-12-01 208 | 209 | ### New Features 210 | 211 | - #### Create Branch & Auto Resolve Issue Actions 212 | 213 | | ![Create Branch Actions](https://user-images.githubusercontent.com/55318172/144379834-8c3e4d4f-d584-4253-9ad8-b9f1d468ed01.gif)
Auto Create Branch | ![Auto Resolve](https://user-images.githubusercontent.com/55318172/144382044-0132e755-9cd5-4805-a756-4086f67b3282.gif)
Auto Resolve | 214 | | :--: | :--: | 215 | 216 | You have to install the app for your organization/account/repository from the [GitHub Marketplace](https://github.com/marketplace/create-issue-branch) for this to work. 217 | 218 | The branch will be created on **assign** with format `i${number}-${issue_title_lowercase}`. 219 | 220 | - #### Custom Tailwind CSS Class Sorter 221 | 222 | Classes are sorted using [prettier-plugin-sort-class-names](https://github.com/PutziSan/prettier-plugin-sort-class-names) with custom class order on [this file](https://github.com/theodorusclarence/ts-nextjs-tailwind-starter/blob/main/prettier-plugin-sort-class-names-order) and custom variant order on [prettierrc](https://github.com/theodorusclarence/ts-nextjs-tailwind-starter/blob/main/.prettierrc.js) 223 | 224 | With this plugin, we can now safely check the order of the classes using the preconfigured lint action. 225 | 226 | ## 0.2.0 - 2021-11-10 227 | 228 | ### New Features 229 | 230 | - #### Jest 231 | 232 | Jest is configured and will be run every push on Github Actions 233 | 234 | - #### Lint Github Action 235 | 236 | 1. **ESLint** - will fail if there are any warning and error. 237 | 2. **Type Check** - will fail on `tsc` error. 238 | 3. **Prettier Check** - will fail if there are any formatting error. 239 | 4. **Test** - will fail if there are any test failure. 240 | 241 | ## 0.1.0 242 | 243 | ### New Features 244 | 245 | - #### Installed Packages 246 | 247 | 1. [clsx](https://bundlephobia.com/package/clsx@latest), utility for constructing `className` strings conditionally. 248 | 2. [react-icons](https://bundlephobia.com/package/react-icons@latest), svg react icons of popular icon packs. 249 | 250 | - #### UnstyledLink Component 251 | 252 | Used as a component for Next.js Link. Will render out Next/Link if the href started with `/` or `#`, else will render an `a` tag with `target='_blank'`. Also add a cursor style for outside links 253 | 254 | - #### CustomLink Component 255 | 256 | ![customlink](https://user-images.githubusercontent.com/55318172/129183546-4e8c2059-0493-4459-a1e9-755fbd32fe39.gif) 257 | 258 | 259 | - #### Absolute Import 260 | 261 | You can import without using relative path 262 | 263 | ```tsx 264 | import Nav from '../../../components/Nav'; 265 | 266 | simplified to 267 | 268 | import Nav from '@/components/Nav'; 269 | ``` 270 | 271 | - #### Seo Component 272 | 273 | Configure the default in `src/components/Seo.tsx`. If you want to use the default, just add `` on top of your page. 274 | 275 | You can also customize it per page by overriding the title, description as props 276 | 277 | ```tsx 278 | 279 | ``` 280 | 281 | or if you want to still keep the title like `%s | Next.js Tailwind Starter`, you can use `templateTitle` props. 282 | 283 | - #### Custom 404 Page 284 | 285 | ![404](https://user-images.githubusercontent.com/55318172/129184274-d90631f2-6688-4ed2-bef2-a4d018a4863c.gif) 286 | 287 | - #### Workspace Snippets 288 | 289 | Snippets such as React import, useState, useEffect, React Component. [View more](/.vscode/typescriptreact.code-snippets) 290 | 291 | - #### Husky, Prettier, Lint, and Commitlint Configured 292 | 293 | 3 Husky hooks including: 294 | 295 | 1. pre-commit, running `next lint` and format the code using prettier 296 | 2. commit-msg, running commitlint to ensure the use of [Conventional Commit](https://theodorusclarence.com/library/conventional-commit-readme) for commit messages 297 | 3. post-merge, running `yarn` every `git pull` or after merge to ensure all new packages are installed and ready to go 298 | 299 | - #### Default Favicon Declaration 300 | 301 | Use [Favicon Generator](https://www.favicon-generator.org/) and then overwrite the files in `/public/favicon` 302 | 303 | - #### Default Tailwind CSS Base Styles 304 | 305 | There are default styles for responsive heading sizes, and `.layout` to support a max-width for larger screen size. Find more about it on [my blog post](https://theodorusclarence.com/blog/tailwindcss-best-practice#1-using-layout-class-or-container) 306 | 307 | - #### Open Graph Generator 308 | 309 | | ![image](https://user-images.githubusercontent.com/55318172/137617070-806a0509-84bd-4cae-a900-2ab17e418d8d.png) | ![image](https://user-images.githubusercontent.com/55318172/137617090-c24f684a-bfe5-41b6-8ba9-fa99bae5cadf.png) | 310 | | --------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | 311 | 312 | Open Graph is generated using [og.thcl.dev](https://og.thcl.dev), but please fork and self-host if your website is going to have a lot of traffic. 313 | 314 | Check out the [repository](https://github.com/theodorusclarence/og) to see the API parameters. 315 | 316 | - #### Preloaded & Self Hosted Inter Fonts 317 | 318 | Inter fonts is a variable fonts that is self hosted and preloaded. 319 | 320 | ## Snippets 321 | 322 | This starter is equipped with workspace-snippets, it is encouraged to use it, especially the `np` and `rc` 323 | 324 | ### Next.js Page 325 | 326 | File inside `src/pages` will be the webpage route, there are 2 things that need to be added in Next.js page: 327 | 328 | 1. Seo component 329 | 2. Layout class to give constraint to viewport width. [Read more about layout class](https://theodorusclarence.com/blog/tailwindcss-best-practice#1-using-layout-class-or-container). 330 | 331 | Snippets: `np` 332 | 333 | ```tsx 334 | import * as React from 'react'; 335 | import Seo from '@/components/Seo'; 336 | export default function TestPage() { 337 | return ( 338 | <> 339 | 340 |
341 |
342 |
343 |
344 |
345 | 346 | ); 347 | } 348 | ``` 349 | 350 | ### React Components 351 | 352 | To make a new component, It is encouraged to use `export default function`. Because when we need to rename it, we only need to do it once. 353 | 354 | Snippets: `rc` 355 | 356 | ```tsx 357 | import * as React from 'react'; 358 | export default function Component() { 359 | return
; 360 | } 361 | ``` 362 | 363 | #### Import React 364 | 365 | Snippets: `ir` 366 | 367 | ```tsx 368 | import * as React from 'react'; 369 | ``` 370 | 371 | #### Import Next Image 372 | 373 | Snippets: `imimg` 374 | 375 | ```tsx 376 | import Image from 'next/image'; 377 | ``` 378 | 379 | #### Import Next Link 380 | 381 | Snippets: `iml` 382 | 383 | ```tsx 384 | import Link from 'next/link'; 385 | ``` 386 | 387 | #### useState Hook 388 | 389 | Snippets: `us` 390 | 391 | ```tsx 392 | const [state, setState] = React.useState(initialState); 393 | ``` 394 | 395 | #### useEffect Hook 396 | 397 | Snippets: `uf` 398 | 399 | ```tsx 400 | React.useEffect(() => {}, []); 401 | ``` 402 | 403 | #### useReducer Hook 404 | 405 | Snippets: `ur` 406 | 407 | ```tsx 408 | const [state, dispatch] = React.useReducer(someReducer, {}); 409 | ``` 410 | 411 | #### useRef Hook 412 | 413 | Snippets: `urf` 414 | 415 | ```tsx 416 | const someRef = React.useRef(); 417 | ``` 418 | 419 | #### Region Comment 420 | 421 | It is really useful when we need to group code. It is also collapsible in VSCode 422 | 423 | Snippets: `reg` 424 | 425 | ```tsx 426 | //#region //*============== FORM SUBMIT 427 | //#endregion //*============== FORM SUBMIT 428 | ``` 429 | 430 | You should also use [Better Comments](https://marketplace.visualstudio.com/items?itemName=aaron-bond.better-comments) extension. 431 | 432 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Next.js + Tailwind CSS + TypeScript Starter and Boilerplate 2 | 3 |
4 |

🔋 ts-nextjs-tailwind-starter

5 |

Next.js + Tailwind CSS + TypeScript starter packed with useful development features.

6 |

Made by Theodorus Clarence

7 | 8 | [![GitHub Repo stars](https://img.shields.io/github/stars/theodorusclarence/ts-nextjs-tailwind-starter)](https://github.com/theodorusclarence/ts-nextjs-tailwind-starter/stargazers) 9 | [![Depfu](https://badges.depfu.com/badges/fc6e730632ab9dacaf7df478a08684a7/overview.svg)](https://depfu.com/github/theodorusclarence/ts-nextjs-tailwind-starter?project_id=30160) 10 | [![Last Update](https://img.shields.io/badge/deps%20update-every%20sunday-blue.svg)](https://shields.io/) 11 | 12 |
13 | 14 | ## Features 15 | 16 | This repository is 🔋 battery packed with: 17 | 18 | - ⚡️ Next.js 14 with App Router 19 | - ⚛️ React 18 20 | - ✨ TypeScript 21 | - 💨 Tailwind CSS 3 — Configured with CSS Variables to extend the **primary** color 22 | - 💎 Pre-built Components — Components that will **automatically adapt** with your brand color, [check here for the demo](https://tsnext-tw.thcl.dev/components) 23 | - 🃏 Jest — Configured for unit testing 24 | - 📈 Absolute Import and Path Alias — Import components using `@/` prefix 25 | - 📏 ESLint — Find and fix problems in your code, also will **auto sort** your imports 26 | - 💖 Prettier — Format your code consistently 27 | - 🐶 Husky & Lint Staged — Run scripts on your staged files before they are committed 28 | - 🤖 Conventional Commit Lint — Make sure you & your teammates follow conventional commit 29 | - ⏰ Release Please — Generate your changelog by activating the `release-please` workflow 30 | - 👷 Github Actions — Lint your code on PR 31 | - 🚘 Automatic Branch and Issue Autolink — Branch will be automatically created on issue **assign**, and auto linked on PR 32 | - 🔥 Snippets — A collection of useful snippets 33 | - 👀 Open Graph Helper Function — Awesome open graph generated using [og](https://github.com/theodorusclarence/og), fork it and deploy! 34 | - 🗺 Site Map — Automatically generate sitemap.xml 35 | - 📦 Expansion Pack — Easily install common libraries, additional components, and configs. 36 | 37 | See the 👉 [feature details and changelog](https://github.com/theodorusclarence/ts-nextjs-tailwind-starter/blob/main/CHANGELOG.md) 👈 for more. 38 | 39 | You can also check all of the **details and demos** on my blog post: 40 | 41 | - [One-stop Starter to Maximize Efficiency on Next.js & Tailwind CSS Projects](https://theodorusclarence.com/blog/one-stop-starter) 42 | 43 | ## Getting Started 44 | 45 | ### 1. Clone this template using one of the three ways 46 | 47 | 1. Use this repository as template 48 | 49 | **Disclosure:** by using this repository as a template, there will be an attribution on your repository. 50 | 51 | I'll appreciate if you do, so this template can be known by others too 😄 52 | 53 | ![Use as template](https://user-images.githubusercontent.com/55318172/129183039-1a61e68d-dd90-4548-9489-7b3ccbb35810.png) 54 | 55 | 2. Using `create-next-app` 56 | 57 | ```bash 58 | pnpm create next-app -e https://github.com/theodorusclarence/ts-nextjs-tailwind-starter ts-pnpm 59 | ``` 60 | 61 | If you still want to use **pages directory** (_is not actively maintained_) you can use this command 62 | 63 | ```bash 64 | npx create-next-app -e https://github.com/theodorusclarence/ts-nextjs-tailwind-starter/tree/pages-directory project-name 65 | ``` 66 | 67 | 3. Using `degit` 68 | 69 | ```bash 70 | npx degit theodorusclarence/ts-nextjs-tailwind-starter YOUR_APP_NAME 71 | ``` 72 | 73 | 4. Deploy to Vercel 74 | 75 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https%3A%2F%2Fgithub.com%2Ftheodorusclarence%2Fts-nextjs-tailwind-starter) 76 | 77 | ### 2. Install dependencies 78 | 79 | It is encouraged to use **pnpm** so the husky hooks can work properly. 80 | 81 | ```bash 82 | pnpm install 83 | ``` 84 | 85 | ### 3. Run the development server 86 | 87 | You can start the server using this command: 88 | 89 | ```bash 90 | pnpm dev 91 | ``` 92 | 93 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. You can start editing the page by modifying `src/pages/index.tsx`. 94 | 95 | ### 4. Change defaults 96 | 97 | There are some things you need to change including title, urls, favicons, etc. 98 | 99 | Find all comments with !STARTERCONF, then follow the guide. 100 | 101 | Don't forget to change the package name in package.json 102 | 103 | ### 5. Commit Message Convention 104 | 105 | This starter is using [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/), it is mandatory to use it to commit changes. 106 | 107 | ## Projects using ts-nextjs-tailwind-starter 108 | 109 | 114 | 115 | - [theodorusclarence.com](https://theodorusclarence.com) ([Source](https://github.com/theodorusclarence/theodorusclarence.com)) 116 | - [Notiolink](https://notiolink.thcl.dev/) ([Source](https://github.com/theodorusclarence/notiolink)) 117 | - [NextJs + Materia UI + Typescript](https://github.com/AlexStack/nextjs-materia-mui-typescript-hook-form-scaffold-boilerplate-starter) 118 | 119 | Are you using this starter? Please add your page (and repo) to the end of the list via a [Pull Request](https://github.com/theodorusclarence/ts-nextjs-tailwind-starter/edit/main/README.md). 😃 120 | 121 | ## Expansion Pack 📦 122 | 123 | This starter is now equipped with an [expansion pack](https://github.com/theodorusclarence/expansion-pack). 124 | 125 | You can easily add expansion such as React Hook Form + Components, Storybook, and more just using a single command line. 126 | 127 | 128 | 129 | Check out the [expansion pack repository](https://github.com/theodorusclarence/expansion-pack) for the commands 130 | 131 | ### App Router Update 132 | 133 | Due to App Router update, the expansion pack is currently **outdated**. It will be updated in the future. You can still use them by copy and pasting the files. 134 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | rules: { 4 | // TODO Add Scope Enum Here 5 | // 'scope-enum': [2, 'always', ['yourscope', 'yourscope']], 6 | 'type-enum': [ 7 | 2, 8 | 'always', 9 | [ 10 | 'feat', 11 | 'fix', 12 | 'docs', 13 | 'chore', 14 | 'style', 15 | 'refactor', 16 | 'ci', 17 | 'test', 18 | 'perf', 19 | 'revert', 20 | 'vercel', 21 | ], 22 | ], 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires 2 | const nextJest = require('next/jest'); 3 | 4 | const createJestConfig = nextJest({ 5 | // Provide the path to your Next.js app to load next.config.js and .env files in your test environment 6 | dir: './', 7 | }); 8 | 9 | // Add any custom config to be passed to Jest 10 | const customJestConfig = { 11 | // Add more setup options before each test is run 12 | setupFilesAfterEnv: ['/jest.setup.js'], 13 | 14 | // if using TypeScript with a baseUrl set to the root directory then you need the below for alias' to work 15 | moduleDirectories: ['node_modules', '/'], 16 | 17 | testEnvironment: 'jest-environment-jsdom', 18 | 19 | /** 20 | * Absolute imports and Module Path Aliases 21 | */ 22 | moduleNameMapper: { 23 | '^@/(.*)$': '/src/$1', 24 | '^~/(.*)$': '/public/$1', 25 | '^.+\\.(svg)$': '/src/__mocks__/svg.tsx', 26 | }, 27 | }; 28 | 29 | // createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async 30 | module.exports = createJestConfig(customJestConfig); 31 | -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom/extend-expect'; 2 | 3 | // Allow router mocks. 4 | // eslint-disable-next-line no-undef 5 | jest.mock('next/router', () => require('next-router-mock')); 6 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /next-sitemap.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('next-sitemap').IConfig} 3 | * @see https://github.com/iamvishnusankar/next-sitemap#readme 4 | */ 5 | module.exports = { 6 | // !STARTERCONF Change the siteUrl 7 | /** Without additional '/' on the end, e.g. https://theodorusclarence.com */ 8 | siteUrl: 'https://tsnext-tw.thcl.dev', 9 | generateRobotsTxt: true, 10 | robotsTxtOptions: { 11 | policies: [{ userAgent: '*', allow: '/' }], 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | eslint: { 4 | dirs: ['src'], 5 | }, 6 | 7 | reactStrictMode: true, 8 | swcMinify: true, 9 | 10 | // Uncoment to add domain whitelist 11 | // images: { 12 | // remotePatterns: [ 13 | // { 14 | // protocol: 'https', 15 | // hostname: 'res.cloudinary.com', 16 | // }, 17 | // ] 18 | // }, 19 | 20 | webpack(config) { 21 | // Grab the existing rule that handles SVG imports 22 | const fileLoaderRule = config.module.rules.find((rule) => 23 | rule.test?.test?.('.svg') 24 | ); 25 | 26 | config.module.rules.push( 27 | // Reapply the existing rule, but only for svg imports ending in ?url 28 | { 29 | ...fileLoaderRule, 30 | test: /\.svg$/i, 31 | resourceQuery: /url/, // *.svg?url 32 | }, 33 | // Convert all other *.svg imports to React components 34 | { 35 | test: /\.svg$/i, 36 | issuer: { not: /\.(css|scss|sass)$/ }, 37 | resourceQuery: { not: /url/ }, // exclude if *.svg?url 38 | loader: '@svgr/webpack', 39 | options: { 40 | dimensions: false, 41 | titleProp: true, 42 | }, 43 | } 44 | ); 45 | 46 | // Modify the file loader rule to ignore *.svg, since we have it handled now. 47 | fileLoaderRule.exclude = /\.svg$/i; 48 | 49 | return config; 50 | }, 51 | }; 52 | 53 | module.exports = nextConfig; 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ts-nextjs-tailwind-starter", 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 | "lint:fix": "eslint src --fix && pnpm format", 11 | "lint:strict": "eslint --max-warnings=0 src", 12 | "typecheck": "tsc --noEmit --incremental false", 13 | "test:watch": "jest --watch", 14 | "test": "jest", 15 | "format": "prettier -w .", 16 | "format:check": "prettier -c .", 17 | "postbuild": "next-sitemap --config next-sitemap.config.js", 18 | "prepare": "husky install" 19 | }, 20 | "dependencies": { 21 | "clsx": "^2.0.0", 22 | "lucide-react": "^0.438.0", 23 | "next": "^14.2.23", 24 | "react": "^18.2.0", 25 | "react-dom": "^18.2.0", 26 | "react-icons": "^5.4.0", 27 | "tailwind-merge": "^2.6.0", 28 | "zod": "^3.24.1" 29 | }, 30 | "devDependencies": { 31 | "@commitlint/cli": "^16.3.0", 32 | "@commitlint/config-conventional": "^16.2.4", 33 | "@svgr/webpack": "^8.1.0", 34 | "@tailwindcss/forms": "^0.5.10", 35 | "@testing-library/jest-dom": "^5.17.0", 36 | "@testing-library/react": "^15.0.7", 37 | "@types/react": "^18.3.18", 38 | "@types/testing-library__jest-dom": "^5.14.9", 39 | "@typescript-eslint/eslint-plugin": "^5.62.0", 40 | "@typescript-eslint/parser": "^5.62.0", 41 | "autoprefixer": "^10.4.20", 42 | "eslint": "^8.57.1", 43 | "eslint-config-next": "^14.2.23", 44 | "eslint-config-prettier": "^8.10.0", 45 | "eslint-plugin-simple-import-sort": "^7.0.0", 46 | "eslint-plugin-unused-imports": "^2.0.0", 47 | "husky": "^7.0.4", 48 | "jest": "^27.5.1", 49 | "lint-staged": "^12.5.0", 50 | "next-router-mock": "^0.9.0", 51 | "next-sitemap": "^2.5.28", 52 | "postcss": "^8.5.1", 53 | "prettier": "^2.8.8", 54 | "prettier-plugin-tailwindcss": "^0.5.0", 55 | "tailwindcss": "^3.4.17", 56 | "typescript": "^4.9.5" 57 | }, 58 | "lint-staged": { 59 | "**/*.{js,jsx,ts,tsx}": [ 60 | "eslint --max-warnings=0", 61 | "prettier -w" 62 | ], 63 | "**/*.{json,css,scss,md,webmanifest}": [ 64 | "prettier -w" 65 | ] 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/ts-nextjs-tailwind-starter/7083a0526711da294f2a6ea274cec1bf80797a14/public/favicon.ico -------------------------------------------------------------------------------- /public/favicon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/ts-nextjs-tailwind-starter/7083a0526711da294f2a6ea274cec1bf80797a14/public/favicon/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/favicon/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/ts-nextjs-tailwind-starter/7083a0526711da294f2a6ea274cec1bf80797a14/public/favicon/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/ts-nextjs-tailwind-starter/7083a0526711da294f2a6ea274cec1bf80797a14/public/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /public/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/ts-nextjs-tailwind-starter/7083a0526711da294f2a6ea274cec1bf80797a14/public/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/ts-nextjs-tailwind-starter/7083a0526711da294f2a6ea274cec1bf80797a14/public/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/ts-nextjs-tailwind-starter/7083a0526711da294f2a6ea274cec1bf80797a14/public/favicon/favicon.ico -------------------------------------------------------------------------------- /public/favicon/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/favicon/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/favicon/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /public/fonts/inter-var-latin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/ts-nextjs-tailwind-starter/7083a0526711da294f2a6ea274cec1bf80797a14/public/fonts/inter-var-latin.woff2 -------------------------------------------------------------------------------- /public/images/new-tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/ts-nextjs-tailwind-starter/7083a0526711da294f2a6ea274cec1bf80797a14/public/images/new-tab.png -------------------------------------------------------------------------------- /public/images/og.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/ts-nextjs-tailwind-starter/7083a0526711da294f2a6ea274cec1bf80797a14/public/images/og.jpg -------------------------------------------------------------------------------- /public/svg/Logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/svg/Vercel.svg: -------------------------------------------------------------------------------- 1 | Vercel -------------------------------------------------------------------------------- /src/__mocks__/svg.tsx: -------------------------------------------------------------------------------- 1 | import React, { SVGProps } from 'react'; 2 | 3 | const SvgrMock = React.forwardRef>( 4 | (props, ref) => 5 | ); 6 | 7 | export const ReactComponent = SvgrMock; 8 | export default SvgrMock; 9 | -------------------------------------------------------------------------------- /src/__tests__/pages/HomePage.test.tsx: -------------------------------------------------------------------------------- 1 | // !STARTERCONF You should delete this page 2 | 3 | import { render, screen } from '@testing-library/react'; 4 | 5 | import HomePage from '@/app/page'; 6 | 7 | describe('Homepage', () => { 8 | it('renders the Components', () => { 9 | render(); 10 | 11 | const heading = screen.getByText(/A starter for Next.js/i); 12 | 13 | expect(heading).toBeInTheDocument(); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/app/api/hello/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | 3 | export async function GET() { 4 | return NextResponse.json({ hello: 'Next.js' }); 5 | } 6 | -------------------------------------------------------------------------------- /src/app/components/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next'; 2 | import * as React from 'react'; 3 | 4 | import '@/styles/colors.css'; 5 | 6 | export const metadata: Metadata = { 7 | title: 'Components', 8 | description: 'Pre-built components with awesome default', 9 | }; 10 | 11 | export default function ComponentsLayout({ 12 | children, 13 | }: { 14 | children: React.ReactNode; 15 | }) { 16 | return <>{children}; 17 | } 18 | -------------------------------------------------------------------------------- /src/app/components/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import clsx from 'clsx'; 4 | import { 5 | ArrowRight, 6 | CreditCard, 7 | Laptop, 8 | Phone, 9 | Plus, 10 | Shield, 11 | } from 'lucide-react'; 12 | import React from 'react'; 13 | 14 | import Button from '@/components/buttons/Button'; 15 | import IconButton from '@/components/buttons/IconButton'; 16 | import TextButton from '@/components/buttons/TextButton'; 17 | import ArrowLink from '@/components/links/ArrowLink'; 18 | import ButtonLink from '@/components/links/ButtonLink'; 19 | import PrimaryLink from '@/components/links/PrimaryLink'; 20 | import UnderlineLink from '@/components/links/UnderlineLink'; 21 | import UnstyledLink from '@/components/links/UnstyledLink'; 22 | import NextImage from '@/components/NextImage'; 23 | import Skeleton from '@/components/Skeleton'; 24 | 25 | type Color = (typeof colorList)[number]; 26 | 27 | export default function ComponentPage() { 28 | const [mode, setMode] = React.useState<'dark' | 'light'>('light'); 29 | const [color, setColor] = React.useState('sky'); 30 | function toggleMode() { 31 | return mode === 'dark' ? setMode('light') : setMode('dark'); 32 | } 33 | 34 | const textColor = mode === 'dark' ? 'text-gray-300' : 'text-gray-600'; 35 | 36 | return ( 37 |
38 |
41 |
47 |

Built-in Components

48 | 49 | Back to Home 50 | 51 | 52 |
53 | 59 | {/* */} 60 |
61 | 62 |
    63 |
  1. 64 |

    Customize Colors

    65 |

    66 | You can change primary color to any Tailwind CSS colors. See 67 | globals.css to change your color. 68 |

    69 |
    70 | 89 | 90 | Check list of colors 91 | 92 |
    93 |
    94 |
    95 | 50 96 |
    97 |
    98 | 100 99 |
    100 |
    101 | 200 102 |
    103 |
    104 | 300 105 |
    106 |
    107 | 400 108 |
    109 |
    110 | 500 111 |
    112 |
    113 | 600 114 |
    115 |
    116 | 700 117 |
    118 |
    119 | 800 120 |
    121 |
    122 | 900 123 |
    124 |
    125 | 950 126 |
    127 |
    128 |
  2. 129 |
  3. 130 |

    UnstyledLink

    131 |

    132 | No style applied, differentiate internal and outside links, give 133 | custom cursor for outside links. 134 |

    135 |
    136 | Internal Links 137 | 138 | Outside Links 139 | 140 |
    141 |
  4. 142 |
  5. 143 |

    PrimaryLink

    144 |

    145 | Add styling on top of UnstyledLink, giving a primary color to 146 | the link. 147 |

    148 |
    149 | Internal Links 150 | 151 | Outside Links 152 | 153 |
    154 |
  6. 155 |
  7. 156 |

    UnderlineLink

    157 |

    158 | Add styling on top of UnstyledLink, giving a dotted and animated 159 | underline. 160 |

    161 |
    162 | Internal Links 163 | 164 | Outside Links 165 | 166 |
    167 |
  8. 168 |
  9. 169 |

    ArrowLink

    170 |

    171 | Useful for indicating navigation, I use this quite a lot, so why 172 | not build a component with some whimsy touch? 173 |

    174 |
    175 | 176 | Direction Left 177 | 178 | Direction Right 179 | 184 | Polymorphic 185 | 186 | 192 | Polymorphic 193 | 194 |
    195 |
  10. 196 |
  11. 197 |

    ButtonLink

    198 |

    199 | Button styled link with 3 variants. 200 |

    201 |
    202 | 206 | Primary Variant 207 | 208 | 213 | Outline Variant 214 | 215 | 220 | Ghost Variant 221 | 222 | 223 | Dark Variant 224 | 225 | 229 | Light Variant 230 | 231 |
    232 |
  12. 233 |
  13. 234 |

    Button

    235 |

    236 | Ordinary button with style. 237 |

    238 |
    239 | 240 | 243 | 246 | 247 | 248 |
    249 |
    250 | 257 | 265 | 273 | 276 | 279 |
    280 |
    281 | 284 | 287 | 290 | 293 | 296 |
    297 |
    298 | 306 | 315 | 324 | 325 | 333 | 341 |
    342 | 343 |
    344 | 347 | 350 | 353 | 356 | 359 |
    360 |
    361 | 364 | 367 | 370 | 373 | 376 |
    377 |
  14. 378 |
  15. 379 |

    TextButton

    380 |

    381 | Button with a text style 382 |

    383 |
    384 | Primary Variant 385 | Basic Variant 386 |
    387 |
  16. 388 |
  17. 389 |

    IconButton

    390 |

    391 | Button with only icon inside 392 |

    393 |
    394 | 395 | 396 | 397 | 398 | 399 |
    400 |
  18. 401 |
  19. 402 |

    Custom 404 Page

    403 |

    404 | Styled 404 page with some animation. 405 |

    406 |
    407 | Visit the 404 page 408 |
    409 |
  20. 410 |
  21. 411 |

    Next Image

    412 |

    413 | Next Image with default props and skeleton animation 414 |

    415 | 423 |
  22. 424 |
  23. 425 |

    Skeleton

    426 |

    427 | Skeleton with shimmer effect 428 |

    429 | 430 |
  24. 431 |
432 |
433 |
434 |
435 | ); 436 | } 437 | 438 | const colorList = [ 439 | 'slate', 440 | 'gray', 441 | 'zinc', 442 | 'neutral', 443 | 'stone', 444 | 'red', 445 | 'orange', 446 | 'amber', 447 | 'yellow', 448 | 'lime', 449 | 'green', 450 | 'emerald', 451 | 'teal', 452 | 'cyan', 453 | 'sky', 454 | 'blue', 455 | 'indigo', 456 | 'violet', 457 | 'purple', 458 | 'fuchsia', 459 | 'pink', 460 | 'rose', 461 | ] as const; 462 | -------------------------------------------------------------------------------- /src/app/error.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; // Error components must be Client Components 2 | 3 | import * as React from 'react'; 4 | import { RiAlarmWarningFill } from 'react-icons/ri'; 5 | 6 | import TextButton from '@/components/buttons/TextButton'; 7 | 8 | export default function Error({ 9 | error, 10 | reset, 11 | }: { 12 | error: Error & { digest?: string }; 13 | reset: () => void; 14 | }) { 15 | React.useEffect(() => { 16 | // eslint-disable-next-line no-console 17 | console.error(error); 18 | }, [error]); 19 | 20 | return ( 21 |
22 |
23 |
24 | 28 |

29 | Oops, something went wrong! 30 |

31 | 32 | Try again 33 | 34 |
35 |
36 |
37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next'; 2 | import * as React from 'react'; 3 | 4 | import '@/styles/globals.css'; 5 | // !STARTERCONF This is for demo purposes, remove @/styles/colors.css import immediately 6 | import '@/styles/colors.css'; 7 | 8 | import { siteConfig } from '@/constant/config'; 9 | 10 | // !STARTERCONF Change these default meta 11 | // !STARTERCONF Look at @/constant/config to change them 12 | export const metadata: Metadata = { 13 | metadataBase: new URL(siteConfig.url), 14 | title: { 15 | default: siteConfig.title, 16 | template: `%s | ${siteConfig.title}`, 17 | }, 18 | description: siteConfig.description, 19 | robots: { index: true, follow: true }, 20 | // !STARTERCONF this is the default favicon, you can generate your own from https://realfavicongenerator.net/ 21 | // ! copy to /favicon folder 22 | icons: { 23 | icon: '/favicon/favicon.ico', 24 | shortcut: '/favicon/favicon-16x16.png', 25 | apple: '/favicon/apple-touch-icon.png', 26 | }, 27 | manifest: `/favicon/site.webmanifest`, 28 | openGraph: { 29 | url: siteConfig.url, 30 | title: siteConfig.title, 31 | description: siteConfig.description, 32 | siteName: siteConfig.title, 33 | images: [`${siteConfig.url}/images/og.jpg`], 34 | type: 'website', 35 | locale: 'en_US', 36 | }, 37 | twitter: { 38 | card: 'summary_large_image', 39 | title: siteConfig.title, 40 | description: siteConfig.description, 41 | images: [`${siteConfig.url}/images/og.jpg`], 42 | // creator: '@th_clarence', 43 | }, 44 | // authors: [ 45 | // { 46 | // name: 'Theodorus Clarence', 47 | // url: 'https://theodorusclarence.com', 48 | // }, 49 | // ], 50 | }; 51 | 52 | export default function RootLayout({ 53 | children, 54 | }: { 55 | children: React.ReactNode; 56 | }) { 57 | return ( 58 | 59 | {children} 60 | 61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /src/app/not-found.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next'; 2 | import * as React from 'react'; 3 | import { RiAlarmWarningFill } from 'react-icons/ri'; 4 | 5 | export const metadata: Metadata = { 6 | title: 'Not Found', 7 | }; 8 | 9 | export default function NotFound() { 10 | return ( 11 |
12 |
13 |
14 | 18 |

Page Not Found

19 | Back to home 20 |
21 |
22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Head from 'next/head'; 4 | import * as React from 'react'; 5 | import '@/lib/env'; 6 | 7 | import ArrowLink from '@/components/links/ArrowLink'; 8 | import ButtonLink from '@/components/links/ButtonLink'; 9 | import UnderlineLink from '@/components/links/UnderlineLink'; 10 | import UnstyledLink from '@/components/links/UnstyledLink'; 11 | 12 | /** 13 | * SVGR Support 14 | * Caveat: No React Props Type. 15 | * 16 | * You can override the next-env if the type is important to you 17 | * @see https://stackoverflow.com/questions/68103844/how-to-override-next-js-svg-module-declaration 18 | */ 19 | import Logo from '~/svg/Logo.svg'; 20 | 21 | // !STARTERCONF -> Select !STARTERCONF and CMD + SHIFT + F 22 | // Before you begin editing, follow all comments with `STARTERCONF`, 23 | // to customize the default configuration. 24 | 25 | export default function HomePage() { 26 | return ( 27 |
28 | 29 | Hi 30 | 31 |
32 |
33 | 34 |

Next.js + Tailwind CSS + TypeScript Starter

35 |

36 | A starter for Next.js, Tailwind CSS, and TypeScript with Absolute 37 | Import, Seo, Link component, pre-configured with Husky{' '} 38 |

39 |

40 | 41 | See the repository 42 | 43 |

44 | 45 | 46 | See all components 47 | 48 | 49 | 53 | {/* eslint-disable-next-line @next/next/no-img-element */} 54 | Deploy with Vercel 60 | 61 | 62 |
63 | © {new Date().getFullYear()} By{' '} 64 | 65 | Theodorus Clarence 66 | 67 |
68 |
69 |
70 |
71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /src/components/NextImage.tsx: -------------------------------------------------------------------------------- 1 | import Image, { ImageProps } from 'next/image'; 2 | import * as React from 'react'; 3 | 4 | import { cn } from '@/lib/utils'; 5 | 6 | type NextImageProps = { 7 | useSkeleton?: boolean; 8 | classNames?: { 9 | image?: string; 10 | blur?: string; 11 | }; 12 | alt: string; 13 | } & ( 14 | | { width: string | number; height: string | number } 15 | | { layout: 'fill'; width?: string | number; height?: string | number } 16 | ) & 17 | ImageProps; 18 | 19 | /** 20 | * 21 | * @description Must set width using `w-` className 22 | * @param useSkeleton add background with pulse animation, don't use it if image is transparent 23 | */ 24 | export default function NextImage({ 25 | useSkeleton = false, 26 | src, 27 | width, 28 | height, 29 | alt, 30 | className, 31 | classNames, 32 | ...rest 33 | }: NextImageProps) { 34 | const [status, setStatus] = React.useState( 35 | useSkeleton ? 'loading' : 'complete' 36 | ); 37 | const widthIsSet = className?.includes('w-') ?? false; 38 | 39 | return ( 40 |
44 | {alt} setStatus('complete')} 54 | {...rest} 55 | /> 56 |
57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /src/components/Skeleton.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | 5 | type SkeletonProps = React.ComponentPropsWithoutRef<'div'>; 6 | 7 | export default function Skeleton({ className, ...rest }: SkeletonProps) { 8 | return ( 9 |
19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/components/buttons/Button.tsx: -------------------------------------------------------------------------------- 1 | import { LucideIcon } from 'lucide-react'; 2 | import * as React from 'react'; 3 | import { IconType } from 'react-icons'; 4 | import { ImSpinner2 } from 'react-icons/im'; 5 | 6 | import { cn } from '@/lib/utils'; 7 | 8 | const ButtonVariant = ['primary', 'outline', 'ghost', 'light', 'dark'] as const; 9 | const ButtonSize = ['sm', 'base'] as const; 10 | 11 | type ButtonProps = { 12 | isLoading?: boolean; 13 | isDarkBg?: boolean; 14 | variant?: (typeof ButtonVariant)[number]; 15 | size?: (typeof ButtonSize)[number]; 16 | leftIcon?: IconType | LucideIcon; 17 | rightIcon?: IconType | LucideIcon; 18 | classNames?: { 19 | leftIcon?: string; 20 | rightIcon?: string; 21 | }; 22 | } & React.ComponentPropsWithRef<'button'>; 23 | 24 | const Button = React.forwardRef( 25 | ( 26 | { 27 | children, 28 | className, 29 | disabled: buttonDisabled, 30 | isLoading, 31 | variant = 'primary', 32 | size = 'base', 33 | isDarkBg = false, 34 | leftIcon: LeftIcon, 35 | rightIcon: RightIcon, 36 | classNames, 37 | ...rest 38 | }, 39 | ref 40 | ) => { 41 | const disabled = isLoading || buttonDisabled; 42 | 43 | return ( 44 | 156 | ); 157 | } 158 | ); 159 | 160 | export default Button; 161 | -------------------------------------------------------------------------------- /src/components/buttons/IconButton.tsx: -------------------------------------------------------------------------------- 1 | import { LucideIcon } from 'lucide-react'; 2 | import * as React from 'react'; 3 | import { IconType } from 'react-icons'; 4 | import { ImSpinner2 } from 'react-icons/im'; 5 | 6 | import { cn } from '@/lib/utils'; 7 | 8 | const IconButtonVariant = [ 9 | 'primary', 10 | 'outline', 11 | 'ghost', 12 | 'light', 13 | 'dark', 14 | ] as const; 15 | 16 | type IconButtonProps = { 17 | isLoading?: boolean; 18 | isDarkBg?: boolean; 19 | variant?: (typeof IconButtonVariant)[number]; 20 | icon?: IconType | LucideIcon; 21 | classNames?: { 22 | icon?: string; 23 | }; 24 | } & React.ComponentPropsWithRef<'button'>; 25 | 26 | const IconButton = React.forwardRef( 27 | ( 28 | { 29 | className, 30 | disabled: buttonDisabled, 31 | isLoading, 32 | variant = 'primary', 33 | isDarkBg = false, 34 | icon: Icon, 35 | classNames, 36 | ...rest 37 | }, 38 | ref 39 | ) => { 40 | const disabled = isLoading || buttonDisabled; 41 | 42 | return ( 43 | 112 | ); 113 | } 114 | ); 115 | 116 | export default IconButton; 117 | -------------------------------------------------------------------------------- /src/components/buttons/TextButton.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | 5 | const TextButtonVariant = ['primary', 'basic'] as const; 6 | 7 | type TextButtonProps = { 8 | variant?: (typeof TextButtonVariant)[number]; 9 | } & React.ComponentPropsWithRef<'button'>; 10 | 11 | const TextButton = React.forwardRef( 12 | ( 13 | { 14 | children, 15 | className, 16 | variant = 'primary', 17 | disabled: buttonDisabled, 18 | ...rest 19 | }, 20 | ref 21 | ) => { 22 | return ( 23 | 48 | ); 49 | } 50 | ); 51 | 52 | export default TextButton; 53 | -------------------------------------------------------------------------------- /src/components/links/ArrowLink.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | 5 | import UnderlineLink from '@/components/links/UnderlineLink'; 6 | import { UnstyledLinkProps } from '@/components/links/UnstyledLink'; 7 | 8 | type ArrowLinkProps = { 9 | as?: C; 10 | direction?: 'left' | 'right'; 11 | } & UnstyledLinkProps & 12 | React.ComponentProps; 13 | 14 | export default function ArrowLink({ 15 | children, 16 | className, 17 | direction = 'right', 18 | as, 19 | ...rest 20 | }: ArrowLinkProps) { 21 | const Component = as || UnderlineLink; 22 | 23 | return ( 24 | 32 | {children} 33 | 46 | 50 | 61 | 62 | 63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /src/components/links/ButtonLink.tsx: -------------------------------------------------------------------------------- 1 | import { LucideIcon } from 'lucide-react'; 2 | import * as React from 'react'; 3 | import { IconType } from 'react-icons'; 4 | 5 | import { cn } from '@/lib/utils'; 6 | 7 | import UnstyledLink, { 8 | UnstyledLinkProps, 9 | } from '@/components/links/UnstyledLink'; 10 | 11 | const ButtonLinkVariant = [ 12 | 'primary', 13 | 'outline', 14 | 'ghost', 15 | 'light', 16 | 'dark', 17 | ] as const; 18 | const ButtonLinkSize = ['sm', 'base'] as const; 19 | 20 | type ButtonLinkProps = { 21 | isDarkBg?: boolean; 22 | variant?: (typeof ButtonLinkVariant)[number]; 23 | size?: (typeof ButtonLinkSize)[number]; 24 | leftIcon?: IconType | LucideIcon; 25 | rightIcon?: IconType | LucideIcon; 26 | classNames?: { 27 | leftIcon?: string; 28 | rightIcon?: string; 29 | }; 30 | } & UnstyledLinkProps; 31 | 32 | const ButtonLink = React.forwardRef( 33 | ( 34 | { 35 | children, 36 | className, 37 | variant = 'primary', 38 | size = 'base', 39 | isDarkBg = false, 40 | leftIcon: LeftIcon, 41 | rightIcon: RightIcon, 42 | classNames, 43 | ...rest 44 | }, 45 | ref 46 | ) => { 47 | return ( 48 | 102 | {LeftIcon && ( 103 |
109 | 119 |
120 | )} 121 | {children} 122 | {RightIcon && ( 123 |
129 | 139 |
140 | )} 141 |
142 | ); 143 | } 144 | ); 145 | 146 | export default ButtonLink; 147 | -------------------------------------------------------------------------------- /src/components/links/IconLink.tsx: -------------------------------------------------------------------------------- 1 | import { LucideIcon } from 'lucide-react'; 2 | import * as React from 'react'; 3 | import { IconType } from 'react-icons'; 4 | 5 | import { cn } from '@/lib/utils'; 6 | 7 | import UnstyledLink, { 8 | UnstyledLinkProps, 9 | } from '@/components/links/UnstyledLink'; 10 | 11 | const IconLinkVariant = [ 12 | 'primary', 13 | 'outline', 14 | 'ghost', 15 | 'light', 16 | 'dark', 17 | ] as const; 18 | 19 | type IconLinkProps = { 20 | isDarkBg?: boolean; 21 | variant?: (typeof IconLinkVariant)[number]; 22 | icon?: IconType | LucideIcon; 23 | classNames?: { 24 | icon?: string; 25 | }; 26 | } & Omit; 27 | 28 | const IconLink = React.forwardRef( 29 | ( 30 | { 31 | className, 32 | icon: Icon, 33 | variant = 'outline', 34 | isDarkBg = false, 35 | classNames, 36 | ...rest 37 | }, 38 | ref 39 | ) => { 40 | return ( 41 | 91 | {Icon && } 92 | 93 | ); 94 | } 95 | ); 96 | 97 | export default IconLink; 98 | -------------------------------------------------------------------------------- /src/components/links/PrimaryLink.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | 5 | import UnstyledLink, { 6 | UnstyledLinkProps, 7 | } from '@/components/links/UnstyledLink'; 8 | 9 | const PrimaryLinkVariant = ['primary', 'basic'] as const; 10 | type PrimaryLinkProps = { 11 | variant?: (typeof PrimaryLinkVariant)[number]; 12 | } & UnstyledLinkProps; 13 | 14 | const PrimaryLink = React.forwardRef( 15 | ({ className, children, variant = 'primary', ...rest }, ref) => { 16 | return ( 17 | 37 | {children} 38 | 39 | ); 40 | } 41 | ); 42 | 43 | export default PrimaryLink; 44 | -------------------------------------------------------------------------------- /src/components/links/UnderlineLink.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | 5 | import UnstyledLink, { 6 | UnstyledLinkProps, 7 | } from '@/components/links/UnstyledLink'; 8 | 9 | const UnderlineLink = React.forwardRef( 10 | ({ children, className, ...rest }, ref) => { 11 | return ( 12 | 22 | {children} 23 | 24 | ); 25 | } 26 | ); 27 | 28 | export default UnderlineLink; 29 | -------------------------------------------------------------------------------- /src/components/links/UnstyledLink.tsx: -------------------------------------------------------------------------------- 1 | import Link, { LinkProps } from 'next/link'; 2 | import * as React from 'react'; 3 | 4 | import { cn } from '@/lib/utils'; 5 | 6 | export type UnstyledLinkProps = { 7 | href: string; 8 | children: React.ReactNode; 9 | openNewTab?: boolean; 10 | className?: string; 11 | nextLinkProps?: Omit; 12 | } & React.ComponentPropsWithRef<'a'>; 13 | 14 | const UnstyledLink = React.forwardRef( 15 | ({ children, href, openNewTab, className, nextLinkProps, ...rest }, ref) => { 16 | const isNewTab = 17 | openNewTab !== undefined 18 | ? openNewTab 19 | : href && !href.startsWith('/') && !href.startsWith('#'); 20 | 21 | if (!isNewTab) { 22 | return ( 23 | 30 | {children} 31 | 32 | ); 33 | } 34 | 35 | return ( 36 | 44 | {children} 45 | 46 | ); 47 | } 48 | ); 49 | 50 | export default UnstyledLink; 51 | -------------------------------------------------------------------------------- /src/constant/config.ts: -------------------------------------------------------------------------------- 1 | export const siteConfig = { 2 | title: 'Next.js + Tailwind CSS + TypeScript Starter', 3 | description: 4 | 'A starter for Next.js, Tailwind CSS, and TypeScript with Absolute Import, Seo, Link component, pre-configured with Husky', 5 | /** Without additional '/' on the end, e.g. https://theodorusclarence.com */ 6 | url: 'https://tsnext-tw.thcl.dev', 7 | }; 8 | -------------------------------------------------------------------------------- /src/constant/env.ts: -------------------------------------------------------------------------------- 1 | export const isProd = process.env.NODE_ENV === 'production'; 2 | export const isLocal = process.env.NODE_ENV === 'development'; 3 | 4 | export const showLogger = isLocal 5 | ? true 6 | : process.env.NEXT_PUBLIC_SHOW_LOGGER === 'true' ?? false; 7 | -------------------------------------------------------------------------------- /src/lib/__tests__/og.test.ts: -------------------------------------------------------------------------------- 1 | import { openGraph } from '@/lib/og'; 2 | 3 | describe('Open Graph function should work correctly', () => { 4 | it('should not return templateTitle when not specified', () => { 5 | const result = openGraph({ 6 | description: 'Test description', 7 | siteName: 'Test site name', 8 | }); 9 | expect(result).not.toContain('&templateTitle='); 10 | }); 11 | 12 | it('should return templateTitle when specified', () => { 13 | const result = openGraph({ 14 | templateTitle: 'Test Template Title', 15 | description: 'Test description', 16 | siteName: 'Test site name', 17 | }); 18 | expect(result).toContain('&templateTitle=Test%20Template%20Title'); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/lib/env.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-namespace */ 2 | /** 3 | * Configuration for type-safe environment variables. 4 | * Imported through src/app/page.tsx 5 | * @see https://x.com/mattpocockuk/status/1760991147793449396 6 | */ 7 | import { z } from 'zod'; 8 | 9 | const envVariables = z.object({ 10 | NEXT_PUBLIC_SHOW_LOGGER: z.enum(['true', 'false']).optional(), 11 | }); 12 | 13 | envVariables.parse(process.env); 14 | 15 | declare global { 16 | namespace NodeJS { 17 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 18 | interface ProcessEnv extends z.infer {} 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/lib/helper.ts: -------------------------------------------------------------------------------- 1 | export function getFromLocalStorage(key: string): string | null { 2 | if (typeof window !== 'undefined') { 3 | return window.localStorage.getItem(key); 4 | } 5 | return null; 6 | } 7 | 8 | export function getFromSessionStorage(key: string): string | null { 9 | if (typeof sessionStorage !== 'undefined') { 10 | return sessionStorage.getItem(key); 11 | } 12 | return null; 13 | } 14 | -------------------------------------------------------------------------------- /src/lib/logger.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import { showLogger } from '@/constant/env'; 3 | 4 | /** 5 | * A logger function that will only logs on development 6 | * @param object - The object to log 7 | * @param comment - Autogenerated with `lg` snippet 8 | */ 9 | export default function logger(object: unknown, comment?: string): void { 10 | if (!showLogger) return; 11 | 12 | console.log( 13 | '%c ============== INFO LOG \n', 14 | 'color: #22D3EE', 15 | `${typeof window !== 'undefined' && window?.location.pathname}\n`, 16 | `=== ${comment ?? ''}\n`, 17 | object 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/lib/og.ts: -------------------------------------------------------------------------------- 1 | type OpenGraphType = { 2 | siteName: string; 3 | description: string; 4 | templateTitle?: string; 5 | logo?: string; 6 | }; 7 | // !STARTERCONF This OG is generated from https://github.com/theodorusclarence/og 8 | // Please clone them and self-host if your site is going to be visited by many people. 9 | // Then change the url and the default logo. 10 | export function openGraph({ 11 | siteName, 12 | templateTitle, 13 | description, 14 | // !STARTERCONF Or, you can use my server with your own logo. 15 | logo = 'https://og./images/logo.jpg', 16 | }: OpenGraphType): string { 17 | const ogLogo = encodeURIComponent(logo); 18 | const ogSiteName = encodeURIComponent(siteName.trim()); 19 | const ogTemplateTitle = templateTitle 20 | ? encodeURIComponent(templateTitle.trim()) 21 | : undefined; 22 | const ogDesc = encodeURIComponent(description.trim()); 23 | 24 | return `https://og./api/general?siteName=${ogSiteName}&description=${ogDesc}&logo=${ogLogo}${ 25 | ogTemplateTitle ? `&templateTitle=${ogTemplateTitle}` : '' 26 | }`; 27 | } 28 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import clsx, { ClassValue } from 'clsx'; 2 | import { twMerge } from 'tailwind-merge'; 3 | 4 | /** Merge classes with tailwind-merge with clsx full feature */ 5 | export function cn(...inputs: ClassValue[]) { 6 | return twMerge(clsx(inputs)); 7 | } 8 | -------------------------------------------------------------------------------- /src/styles/colors.css: -------------------------------------------------------------------------------- 1 | /* //!STARTERCONF Remove this file after copying your desired color, this is a large file you should remove it. */ 2 | 3 | .slate { 4 | --tw-color-primary-50: 248 250 252; 5 | --tw-color-primary-100: 241 245 249; 6 | --tw-color-primary-200: 226 232 240; 7 | --tw-color-primary-300: 203 213 225; 8 | --tw-color-primary-400: 148 163 184; 9 | --tw-color-primary-500: 100 116 139; 10 | --tw-color-primary-600: 71 85 105; 11 | --tw-color-primary-700: 51 65 85; 12 | --tw-color-primary-800: 30 41 59; 13 | --tw-color-primary-900: 15 23 42; 14 | --tw-color-primary-950: 2 6 23; 15 | --color-primary-50: rgb(var(--tw-color-primary-50)); /* #f8fafc */ 16 | --color-primary-100: rgb(var(--tw-color-primary-100)); /* #f1f5f9 */ 17 | --color-primary-200: rgb(var(--tw-color-primary-200)); /* #e2e8f0 */ 18 | --color-primary-300: rgb(var(--tw-color-primary-300)); /* #cbd5e1 */ 19 | --color-primary-400: rgb(var(--tw-color-primary-400)); /* #94a3b8 */ 20 | --color-primary-500: rgb(var(--tw-color-primary-500)); /* #64748b */ 21 | --color-primary-600: rgb(var(--tw-color-primary-600)); /* #475569 */ 22 | --color-primary-700: rgb(var(--tw-color-primary-700)); /* #334155 */ 23 | --color-primary-800: rgb(var(--tw-color-primary-800)); /* #1e293b */ 24 | --color-primary-900: rgb(var(--tw-color-primary-900)); /* #0f172a */ 25 | --color-primary-950: rgb(var(--tw-color-primary-950)); /* #020617 */ 26 | } 27 | 28 | .gray { 29 | --tw-color-primary-50: 249 250 251; 30 | --tw-color-primary-100: 243 244 246; 31 | --tw-color-primary-200: 229 231 235; 32 | --tw-color-primary-300: 209 213 219; 33 | --tw-color-primary-400: 156 163 175; 34 | --tw-color-primary-500: 107 114 128; 35 | --tw-color-primary-600: 75 85 99; 36 | --tw-color-primary-700: 55 65 81; 37 | --tw-color-primary-800: 31 41 55; 38 | --tw-color-primary-900: 17 24 39; 39 | --tw-color-primary-950: 3 7 18; 40 | --color-primary-50: rgb(var(--tw-color-primary-50)); /* #f9fafb */ 41 | --color-primary-100: rgb(var(--tw-color-primary-100)); /* #f3f4f6 */ 42 | --color-primary-200: rgb(var(--tw-color-primary-200)); /* #e5e7eb */ 43 | --color-primary-300: rgb(var(--tw-color-primary-300)); /* #d1d5db */ 44 | --color-primary-400: rgb(var(--tw-color-primary-400)); /* #9ca3af */ 45 | --color-primary-500: rgb(var(--tw-color-primary-500)); /* #6b7280 */ 46 | --color-primary-600: rgb(var(--tw-color-primary-600)); /* #4b5563 */ 47 | --color-primary-700: rgb(var(--tw-color-primary-700)); /* #374151 */ 48 | --color-primary-800: rgb(var(--tw-color-primary-800)); /* #1f2937 */ 49 | --color-primary-900: rgb(var(--tw-color-primary-900)); /* #111827 */ 50 | --color-primary-950: rgb(var(--tw-color-primary-950)); /* #030712 */ 51 | } 52 | 53 | .zinc { 54 | --tw-color-primary-50: 250 250 250; 55 | --tw-color-primary-100: 244 244 245; 56 | --tw-color-primary-200: 228 228 231; 57 | --tw-color-primary-300: 212 212 216; 58 | --tw-color-primary-400: 161 161 170; 59 | --tw-color-primary-500: 113 113 122; 60 | --tw-color-primary-600: 82 82 91; 61 | --tw-color-primary-700: 63 63 70; 62 | --tw-color-primary-800: 39 39 42; 63 | --tw-color-primary-900: 24 24 27; 64 | --tw-color-primary-950: 9 9 11; 65 | --color-primary-50: rgb(var(--tw-color-primary-50)); /* #fafafa */ 66 | --color-primary-100: rgb(var(--tw-color-primary-100)); /* #f4f4f5 */ 67 | --color-primary-200: rgb(var(--tw-color-primary-200)); /* #e4e4e7 */ 68 | --color-primary-300: rgb(var(--tw-color-primary-300)); /* #d4d4d8 */ 69 | --color-primary-400: rgb(var(--tw-color-primary-400)); /* #a1a1aa */ 70 | --color-primary-500: rgb(var(--tw-color-primary-500)); /* #71717a */ 71 | --color-primary-600: rgb(var(--tw-color-primary-600)); /* #52525b */ 72 | --color-primary-700: rgb(var(--tw-color-primary-700)); /* #3f3f46 */ 73 | --color-primary-800: rgb(var(--tw-color-primary-800)); /* #27272a */ 74 | --color-primary-900: rgb(var(--tw-color-primary-900)); /* #18181b */ 75 | --color-primary-950: rgb(var(--tw-color-primary-950)); /* #09090b */ 76 | } 77 | 78 | .neutral { 79 | --tw-color-primary-50: 250 250 250; 80 | --tw-color-primary-100: 245 245 245; 81 | --tw-color-primary-200: 229 229 229; 82 | --tw-color-primary-300: 212 212 212; 83 | --tw-color-primary-400: 163 163 163; 84 | --tw-color-primary-500: 115 115 115; 85 | --tw-color-primary-600: 82 82 82; 86 | --tw-color-primary-700: 64 64 64; 87 | --tw-color-primary-800: 38 38 38; 88 | --tw-color-primary-900: 23 23 23; 89 | --tw-color-primary-950: 10 10 10; 90 | --color-primary-50: rgb(var(--tw-color-primary-50)); /* #fafafa */ 91 | --color-primary-100: rgb(var(--tw-color-primary-100)); /* #f5f5f5 */ 92 | --color-primary-200: rgb(var(--tw-color-primary-200)); /* #e5e5e5 */ 93 | --color-primary-300: rgb(var(--tw-color-primary-300)); /* #d4d4d4 */ 94 | --color-primary-400: rgb(var(--tw-color-primary-400)); /* #a3a3a3 */ 95 | --color-primary-500: rgb(var(--tw-color-primary-500)); /* #737373 */ 96 | --color-primary-600: rgb(var(--tw-color-primary-600)); /* #525252 */ 97 | --color-primary-700: rgb(var(--tw-color-primary-700)); /* #404040 */ 98 | --color-primary-800: rgb(var(--tw-color-primary-800)); /* #262626 */ 99 | --color-primary-900: rgb(var(--tw-color-primary-900)); /* #171717 */ 100 | --color-primary-950: rgb(var(--tw-color-primary-950)); /* #0a0a0a */ 101 | } 102 | 103 | .stone { 104 | --tw-color-primary-50: 250 250 249; 105 | --tw-color-primary-100: 245 245 244; 106 | --tw-color-primary-200: 231 229 228; 107 | --tw-color-primary-300: 214 211 209; 108 | --tw-color-primary-400: 168 162 158; 109 | --tw-color-primary-500: 120 113 108; 110 | --tw-color-primary-600: 87 83 78; 111 | --tw-color-primary-700: 68 64 60; 112 | --tw-color-primary-800: 41 37 36; 113 | --tw-color-primary-900: 28 25 23; 114 | --tw-color-primary-950: 12 10 9; 115 | --color-primary-50: rgb(var(--tw-color-primary-50)); /* #fafaf9 */ 116 | --color-primary-100: rgb(var(--tw-color-primary-100)); /* #f5f5f4 */ 117 | --color-primary-200: rgb(var(--tw-color-primary-200)); /* #e7e5e4 */ 118 | --color-primary-300: rgb(var(--tw-color-primary-300)); /* #d6d3d1 */ 119 | --color-primary-400: rgb(var(--tw-color-primary-400)); /* #a8a29e */ 120 | --color-primary-500: rgb(var(--tw-color-primary-500)); /* #78716c */ 121 | --color-primary-600: rgb(var(--tw-color-primary-600)); /* #57534e */ 122 | --color-primary-700: rgb(var(--tw-color-primary-700)); /* #44403c */ 123 | --color-primary-800: rgb(var(--tw-color-primary-800)); /* #292524 */ 124 | --color-primary-900: rgb(var(--tw-color-primary-900)); /* #1c1917 */ 125 | --color-primary-950: rgb(var(--tw-color-primary-950)); /* #0c0a09 */ 126 | } 127 | 128 | .red { 129 | --tw-color-primary-50: 254 242 242; 130 | --tw-color-primary-100: 254 226 226; 131 | --tw-color-primary-200: 254 202 202; 132 | --tw-color-primary-300: 252 165 165; 133 | --tw-color-primary-400: 248 113 113; 134 | --tw-color-primary-500: 239 68 68; 135 | --tw-color-primary-600: 220 38 38; 136 | --tw-color-primary-700: 185 28 28; 137 | --tw-color-primary-800: 153 27 27; 138 | --tw-color-primary-900: 127 29 29; 139 | --tw-color-primary-950: 69 10 10; 140 | --color-primary-50: rgb(var(--tw-color-primary-50)); /* #fef2f2 */ 141 | --color-primary-100: rgb(var(--tw-color-primary-100)); /* #fee2e2 */ 142 | --color-primary-200: rgb(var(--tw-color-primary-200)); /* #fecaca */ 143 | --color-primary-300: rgb(var(--tw-color-primary-300)); /* #fca5a5 */ 144 | --color-primary-400: rgb(var(--tw-color-primary-400)); /* #f87171 */ 145 | --color-primary-500: rgb(var(--tw-color-primary-500)); /* #ef4444 */ 146 | --color-primary-600: rgb(var(--tw-color-primary-600)); /* #dc2626 */ 147 | --color-primary-700: rgb(var(--tw-color-primary-700)); /* #b91c1c */ 148 | --color-primary-800: rgb(var(--tw-color-primary-800)); /* #991b1b */ 149 | --color-primary-900: rgb(var(--tw-color-primary-900)); /* #7f1d1d */ 150 | --color-primary-950: rgb(var(--tw-color-primary-950)); /* #450a0a */ 151 | } 152 | 153 | .orange { 154 | --tw-color-primary-50: 255 247 237; 155 | --tw-color-primary-100: 255 237 213; 156 | --tw-color-primary-200: 254 215 170; 157 | --tw-color-primary-300: 253 186 116; 158 | --tw-color-primary-400: 251 146 60; 159 | --tw-color-primary-500: 249 115 22; 160 | --tw-color-primary-600: 234 88 12; 161 | --tw-color-primary-700: 194 65 12; 162 | --tw-color-primary-800: 154 52 18; 163 | --tw-color-primary-900: 124 45 18; 164 | --tw-color-primary-950: 67 20 7; 165 | --color-primary-50: rgb(var(--tw-color-primary-50)); /* #fff7ed */ 166 | --color-primary-100: rgb(var(--tw-color-primary-100)); /* #ffedd5 */ 167 | --color-primary-200: rgb(var(--tw-color-primary-200)); /* #fed7aa */ 168 | --color-primary-300: rgb(var(--tw-color-primary-300)); /* #fdba74 */ 169 | --color-primary-400: rgb(var(--tw-color-primary-400)); /* #fb923c */ 170 | --color-primary-500: rgb(var(--tw-color-primary-500)); /* #f97316 */ 171 | --color-primary-600: rgb(var(--tw-color-primary-600)); /* #ea580c */ 172 | --color-primary-700: rgb(var(--tw-color-primary-700)); /* #c2410c */ 173 | --color-primary-800: rgb(var(--tw-color-primary-800)); /* #9a3412 */ 174 | --color-primary-900: rgb(var(--tw-color-primary-900)); /* #7c2d12 */ 175 | --color-primary-950: rgb(var(--tw-color-primary-950)); /* #431407 */ 176 | } 177 | 178 | .amber { 179 | --tw-color-primary-50: 255 251 235; 180 | --tw-color-primary-100: 254 243 199; 181 | --tw-color-primary-200: 253 230 138; 182 | --tw-color-primary-300: 252 211 77; 183 | --tw-color-primary-400: 251 191 36; 184 | --tw-color-primary-500: 245 158 11; 185 | --tw-color-primary-600: 217 119 6; 186 | --tw-color-primary-700: 180 83 9; 187 | --tw-color-primary-800: 146 64 14; 188 | --tw-color-primary-900: 120 53 15; 189 | --tw-color-primary-950: 69 26 3; 190 | --color-primary-50: rgb(var(--tw-color-primary-50)); /* #fffbeb */ 191 | --color-primary-100: rgb(var(--tw-color-primary-100)); /* #fef3c7 */ 192 | --color-primary-200: rgb(var(--tw-color-primary-200)); /* #fde68a */ 193 | --color-primary-300: rgb(var(--tw-color-primary-300)); /* #fcd34d */ 194 | --color-primary-400: rgb(var(--tw-color-primary-400)); /* #fbbf24 */ 195 | --color-primary-500: rgb(var(--tw-color-primary-500)); /* #f59e0b */ 196 | --color-primary-600: rgb(var(--tw-color-primary-600)); /* #d97706 */ 197 | --color-primary-700: rgb(var(--tw-color-primary-700)); /* #b45309 */ 198 | --color-primary-800: rgb(var(--tw-color-primary-800)); /* #92400e */ 199 | --color-primary-900: rgb(var(--tw-color-primary-900)); /* #78350f */ 200 | --color-primary-950: rgb(var(--tw-color-primary-950)); /* #451a03 */ 201 | } 202 | 203 | .yellow { 204 | --tw-color-primary-50: 254 252 232; 205 | --tw-color-primary-100: 254 249 195; 206 | --tw-color-primary-200: 254 240 138; 207 | --tw-color-primary-300: 253 224 71; 208 | --tw-color-primary-400: 250 204 21; 209 | --tw-color-primary-500: 234 179 8; 210 | --tw-color-primary-600: 202 138 4; 211 | --tw-color-primary-700: 161 98 7; 212 | --tw-color-primary-800: 133 77 14; 213 | --tw-color-primary-900: 113 63 18; 214 | --tw-color-primary-950: 66 32 6; 215 | --color-primary-50: rgb(var(--tw-color-primary-50)); /* #fefce8 */ 216 | --color-primary-100: rgb(var(--tw-color-primary-100)); /* #fef9c3 */ 217 | --color-primary-200: rgb(var(--tw-color-primary-200)); /* #fef08a */ 218 | --color-primary-300: rgb(var(--tw-color-primary-300)); /* #fde047 */ 219 | --color-primary-400: rgb(var(--tw-color-primary-400)); /* #facc15 */ 220 | --color-primary-500: rgb(var(--tw-color-primary-500)); /* #eab308 */ 221 | --color-primary-600: rgb(var(--tw-color-primary-600)); /* #ca8a04 */ 222 | --color-primary-700: rgb(var(--tw-color-primary-700)); /* #a16207 */ 223 | --color-primary-800: rgb(var(--tw-color-primary-800)); /* #854d0e */ 224 | --color-primary-900: rgb(var(--tw-color-primary-900)); /* #713f12 */ 225 | --color-primary-950: rgb(var(--tw-color-primary-950)); /* #422006 */ 226 | } 227 | .lime { 228 | --tw-color-primary-50: 247 254 231; 229 | --tw-color-primary-100: 236 252 203; 230 | --tw-color-primary-200: 217 249 157; 231 | --tw-color-primary-300: 190 242 100; 232 | --tw-color-primary-400: 163 230 53; 233 | --tw-color-primary-500: 132 204 22; 234 | --tw-color-primary-600: 101 163 13; 235 | --tw-color-primary-700: 77 124 15; 236 | --tw-color-primary-800: 63 98 18; 237 | --tw-color-primary-900: 54 83 20; 238 | --tw-color-primary-950: 26 46 5; 239 | --color-primary-50: rgb(var(--tw-color-primary-50)); /* #f7fee7 */ 240 | --color-primary-100: rgb(var(--tw-color-primary-100)); /* #ecfccb */ 241 | --color-primary-200: rgb(var(--tw-color-primary-200)); /* #d9f99d */ 242 | --color-primary-300: rgb(var(--tw-color-primary-300)); /* #bef264 */ 243 | --color-primary-400: rgb(var(--tw-color-primary-400)); /* #a3e635 */ 244 | --color-primary-500: rgb(var(--tw-color-primary-500)); /* #84cc16 */ 245 | --color-primary-600: rgb(var(--tw-color-primary-600)); /* #65a30d */ 246 | --color-primary-700: rgb(var(--tw-color-primary-700)); /* #4d7c0f */ 247 | --color-primary-800: rgb(var(--tw-color-primary-800)); /* #3f6212 */ 248 | --color-primary-900: rgb(var(--tw-color-primary-900)); /* #365314 */ 249 | --color-primary-950: rgb(var(--tw-color-primary-950)); /* #1a2e05 */ 250 | } 251 | 252 | .green { 253 | --tw-color-primary-50: 240 253 244; 254 | --tw-color-primary-100: 220 252 231; 255 | --tw-color-primary-200: 187 247 208; 256 | --tw-color-primary-300: 134 239 172; 257 | --tw-color-primary-400: 74 222 128; 258 | --tw-color-primary-500: 34 197 94; 259 | --tw-color-primary-600: 22 163 74; 260 | --tw-color-primary-700: 21 128 61; 261 | --tw-color-primary-800: 22 101 52; 262 | --tw-color-primary-900: 20 83 45; 263 | --tw-color-primary-950: 5 46 22; 264 | --color-primary-50: rgb(var(--tw-color-primary-50)); /* #f0fdf4 */ 265 | --color-primary-100: rgb(var(--tw-color-primary-100)); /* #dcfce7 */ 266 | --color-primary-200: rgb(var(--tw-color-primary-200)); /* #bbf7d0 */ 267 | --color-primary-300: rgb(var(--tw-color-primary-300)); /* #86efac */ 268 | --color-primary-400: rgb(var(--tw-color-primary-400)); /* #4ade80 */ 269 | --color-primary-500: rgb(var(--tw-color-primary-500)); /* #22c55e */ 270 | --color-primary-600: rgb(var(--tw-color-primary-600)); /* #16a34a */ 271 | --color-primary-700: rgb(var(--tw-color-primary-700)); /* #15803d */ 272 | --color-primary-800: rgb(var(--tw-color-primary-800)); /* #166534 */ 273 | --color-primary-900: rgb(var(--tw-color-primary-900)); /* #14532d */ 274 | --color-primary-950: rgb(var(--tw-color-primary-950)); /* #052e16 */ 275 | } 276 | 277 | .emerald { 278 | --tw-color-primary-50: 236 253 245; 279 | --tw-color-primary-100: 209 250 229; 280 | --tw-color-primary-200: 167 243 208; 281 | --tw-color-primary-300: 110 231 183; 282 | --tw-color-primary-400: 52 211 153; 283 | --tw-color-primary-500: 16 185 129; 284 | --tw-color-primary-600: 5 150 105; 285 | --tw-color-primary-700: 4 120 87; 286 | --tw-color-primary-800: 6 95 70; 287 | --tw-color-primary-900: 6 78 59; 288 | --tw-color-primary-950: 2 44 34; 289 | --color-primary-50: rgb(var(--tw-color-primary-50)); /* #ecfdf5 */ 290 | --color-primary-100: rgb(var(--tw-color-primary-100)); /* #d1fae5 */ 291 | --color-primary-200: rgb(var(--tw-color-primary-200)); /* #a7f3d0 */ 292 | --color-primary-300: rgb(var(--tw-color-primary-300)); /* #6ee7b7 */ 293 | --color-primary-400: rgb(var(--tw-color-primary-400)); /* #34d399 */ 294 | --color-primary-500: rgb(var(--tw-color-primary-500)); /* #10b981 */ 295 | --color-primary-600: rgb(var(--tw-color-primary-600)); /* #059669 */ 296 | --color-primary-700: rgb(var(--tw-color-primary-700)); /* #047857 */ 297 | --color-primary-800: rgb(var(--tw-color-primary-800)); /* #065f46 */ 298 | --color-primary-900: rgb(var(--tw-color-primary-900)); /* #064e3b */ 299 | --color-primary-950: rgb(var(--tw-color-primary-950)); /* #022c22 */ 300 | } 301 | 302 | .teal { 303 | --tw-color-primary-50: 240 253 250; 304 | --tw-color-primary-100: 204 251 241; 305 | --tw-color-primary-200: 153 246 228; 306 | --tw-color-primary-300: 94 234 212; 307 | --tw-color-primary-400: 45 212 191; 308 | --tw-color-primary-500: 20 184 166; 309 | --tw-color-primary-600: 13 148 136; 310 | --tw-color-primary-700: 15 118 110; 311 | --tw-color-primary-800: 17 94 89; 312 | --tw-color-primary-900: 19 78 74; 313 | --tw-color-primary-950: 4 47 46; 314 | --color-primary-50: rgb(var(--tw-color-primary-50)); /* #f0fdfa */ 315 | --color-primary-100: rgb(var(--tw-color-primary-100)); /* #ccfbf1 */ 316 | --color-primary-200: rgb(var(--tw-color-primary-200)); /* #99f6e4 */ 317 | --color-primary-300: rgb(var(--tw-color-primary-300)); /* #5eead4 */ 318 | --color-primary-400: rgb(var(--tw-color-primary-400)); /* #2dd4bf */ 319 | --color-primary-500: rgb(var(--tw-color-primary-500)); /* #14b8a6 */ 320 | --color-primary-600: rgb(var(--tw-color-primary-600)); /* #0d9488 */ 321 | --color-primary-700: rgb(var(--tw-color-primary-700)); /* #0f766e */ 322 | --color-primary-800: rgb(var(--tw-color-primary-800)); /* #115e59 */ 323 | --color-primary-900: rgb(var(--tw-color-primary-900)); /* #134e4a */ 324 | --color-primary-950: rgb(var(--tw-color-primary-950)); /* #042f2e */ 325 | } 326 | 327 | .cyan { 328 | --tw-color-primary-50: 236 254 255; 329 | --tw-color-primary-100: 207 250 254; 330 | --tw-color-primary-200: 165 243 252; 331 | --tw-color-primary-300: 103 232 249; 332 | --tw-color-primary-400: 34 211 238; 333 | --tw-color-primary-500: 6 182 212; 334 | --tw-color-primary-600: 8 145 178; 335 | --tw-color-primary-700: 14 116 144; 336 | --tw-color-primary-800: 21 94 117; 337 | --tw-color-primary-900: 22 78 99; 338 | --tw-color-primary-950: 8 51 68; 339 | --color-primary-50: rgb(var(--tw-color-primary-50)); /* #ecfeff */ 340 | --color-primary-100: rgb(var(--tw-color-primary-100)); /* #cffafe */ 341 | --color-primary-200: rgb(var(--tw-color-primary-200)); /* #a5f3fc */ 342 | --color-primary-300: rgb(var(--tw-color-primary-300)); /* #67e8f9 */ 343 | --color-primary-400: rgb(var(--tw-color-primary-400)); /* #22d3ee */ 344 | --color-primary-500: rgb(var(--tw-color-primary-500)); /* #06b6d4 */ 345 | --color-primary-600: rgb(var(--tw-color-primary-600)); /* #0891b2 */ 346 | --color-primary-700: rgb(var(--tw-color-primary-700)); /* #0e7490 */ 347 | --color-primary-800: rgb(var(--tw-color-primary-800)); /* #155e75 */ 348 | --color-primary-900: rgb(var(--tw-color-primary-900)); /* #164e63 */ 349 | --color-primary-950: rgb(var(--tw-color-primary-950)); /* #083344 */ 350 | } 351 | 352 | .sky { 353 | --tw-color-primary-50: 240 249 255; 354 | --tw-color-primary-100: 224 242 254; 355 | --tw-color-primary-200: 186 230 253; 356 | --tw-color-primary-300: 125 211 252; 357 | --tw-color-primary-400: 56 189 248; 358 | --tw-color-primary-500: 14 165 233; 359 | --tw-color-primary-600: 2 132 199; 360 | --tw-color-primary-700: 3 105 161; 361 | --tw-color-primary-800: 7 89 133; 362 | --tw-color-primary-900: 12 74 110; 363 | --tw-color-primary-950: 8 47 73; 364 | --color-primary-50: rgb(var(--tw-color-primary-50)); /* #f0f9ff */ 365 | --color-primary-100: rgb(var(--tw-color-primary-100)); /* #e0f2fe */ 366 | --color-primary-200: rgb(var(--tw-color-primary-200)); /* #bae6fd */ 367 | --color-primary-300: rgb(var(--tw-color-primary-300)); /* #7dd3fc */ 368 | --color-primary-400: rgb(var(--tw-color-primary-400)); /* #38bdf8 */ 369 | --color-primary-500: rgb(var(--tw-color-primary-500)); /* #0ea5e9 */ 370 | --color-primary-600: rgb(var(--tw-color-primary-600)); /* #0284c7 */ 371 | --color-primary-700: rgb(var(--tw-color-primary-700)); /* #0369a1 */ 372 | --color-primary-800: rgb(var(--tw-color-primary-800)); /* #075985 */ 373 | --color-primary-900: rgb(var(--tw-color-primary-900)); /* #0c4a6e */ 374 | --color-primary-950: rgb(var(--tw-color-primary-950)); /* #082f49 */ 375 | } 376 | 377 | .blue { 378 | --tw-color-primary-50: 239 246 255; 379 | --tw-color-primary-100: 219 234 254; 380 | --tw-color-primary-200: 191 219 254; 381 | --tw-color-primary-300: 147 197 253; 382 | --tw-color-primary-400: 96 165 250; 383 | --tw-color-primary-500: 59 130 246; 384 | --tw-color-primary-600: 37 99 235; 385 | --tw-color-primary-700: 29 78 216; 386 | --tw-color-primary-800: 30 64 175; 387 | --tw-color-primary-900: 30 58 138; 388 | --tw-color-primary-950: 23 37 84; 389 | --color-primary-50: rgb(var(--tw-color-primary-50)); /* #eff6ff */ 390 | --color-primary-100: rgb(var(--tw-color-primary-100)); /* #dbeafe */ 391 | --color-primary-200: rgb(var(--tw-color-primary-200)); /* #bfdbfe */ 392 | --color-primary-300: rgb(var(--tw-color-primary-300)); /* #93c5fd */ 393 | --color-primary-400: rgb(var(--tw-color-primary-400)); /* #60a5fa */ 394 | --color-primary-500: rgb(var(--tw-color-primary-500)); /* #3b82f6 */ 395 | --color-primary-600: rgb(var(--tw-color-primary-600)); /* #2563eb */ 396 | --color-primary-700: rgb(var(--tw-color-primary-700)); /* #1d4ed8 */ 397 | --color-primary-800: rgb(var(--tw-color-primary-800)); /* #1e40af */ 398 | --color-primary-900: rgb(var(--tw-color-primary-900)); /* #1e3a8a */ 399 | --color-primary-950: rgb(var(--tw-color-primary-950)); /* #172554 */ 400 | } 401 | 402 | .indigo { 403 | --tw-color-primary-50: 238 242 255; 404 | --tw-color-primary-100: 224 231 255; 405 | --tw-color-primary-200: 199 210 254; 406 | --tw-color-primary-300: 165 180 252; 407 | --tw-color-primary-400: 129 140 248; 408 | --tw-color-primary-500: 99 102 241; 409 | --tw-color-primary-600: 79 70 229; 410 | --tw-color-primary-700: 67 56 202; 411 | --tw-color-primary-800: 55 48 163; 412 | --tw-color-primary-900: 49 46 129; 413 | --tw-color-primary-950: 30 27 75; 414 | --color-primary-50: rgb(var(--tw-color-primary-50)); /* #eef2ff */ 415 | --color-primary-100: rgb(var(--tw-color-primary-100)); /* #e0e7ff */ 416 | --color-primary-200: rgb(var(--tw-color-primary-200)); /* #c7d2fe */ 417 | --color-primary-300: rgb(var(--tw-color-primary-300)); /* #a5b4fc */ 418 | --color-primary-400: rgb(var(--tw-color-primary-400)); /* #818cf8 */ 419 | --color-primary-500: rgb(var(--tw-color-primary-500)); /* #6366f1 */ 420 | --color-primary-600: rgb(var(--tw-color-primary-600)); /* #4f46e5 */ 421 | --color-primary-700: rgb(var(--tw-color-primary-700)); /* #4338ca */ 422 | --color-primary-800: rgb(var(--tw-color-primary-800)); /* #3730a3 */ 423 | --color-primary-900: rgb(var(--tw-color-primary-900)); /* #312e81 */ 424 | --color-primary-950: rgb(var(--tw-color-primary-950)); /* #1e1b4b */ 425 | } 426 | 427 | .violet { 428 | --tw-color-primary-50: 245 243 255; 429 | --tw-color-primary-100: 237 233 254; 430 | --tw-color-primary-200: 221 214 254; 431 | --tw-color-primary-300: 196 181 253; 432 | --tw-color-primary-400: 167 139 250; 433 | --tw-color-primary-500: 139 92 246; 434 | --tw-color-primary-600: 124 58 237; 435 | --tw-color-primary-700: 109 40 217; 436 | --tw-color-primary-800: 91 33 182; 437 | --tw-color-primary-900: 76 29 149; 438 | --tw-color-primary-950: 46 16 101; 439 | --color-primary-50: rgb(var(--tw-color-primary-50)); /* #f5f3ff */ 440 | --color-primary-100: rgb(var(--tw-color-primary-100)); /* #ede9fe */ 441 | --color-primary-200: rgb(var(--tw-color-primary-200)); /* #ddd6fe */ 442 | --color-primary-300: rgb(var(--tw-color-primary-300)); /* #c4b5fd */ 443 | --color-primary-400: rgb(var(--tw-color-primary-400)); /* #a78bfa */ 444 | --color-primary-500: rgb(var(--tw-color-primary-500)); /* #8b5cf6 */ 445 | --color-primary-600: rgb(var(--tw-color-primary-600)); /* #7c3aed */ 446 | --color-primary-700: rgb(var(--tw-color-primary-700)); /* #6d28d9 */ 447 | --color-primary-800: rgb(var(--tw-color-primary-800)); /* #5b21b6 */ 448 | --color-primary-900: rgb(var(--tw-color-primary-900)); /* #4c1d95 */ 449 | --color-primary-950: rgb(var(--tw-color-primary-950)); /* #2e1065 */ 450 | } 451 | 452 | .purple { 453 | --tw-color-primary-50: 250 245 255; 454 | --tw-color-primary-100: 243 232 255; 455 | --tw-color-primary-200: 233 213 255; 456 | --tw-color-primary-300: 216 180 254; 457 | --tw-color-primary-400: 192 132 252; 458 | --tw-color-primary-500: 168 85 247; 459 | --tw-color-primary-600: 147 51 234; 460 | --tw-color-primary-700: 126 34 206; 461 | --tw-color-primary-800: 107 33 168; 462 | --tw-color-primary-900: 88 28 135; 463 | --tw-color-primary-950: 59 7 100; 464 | --color-primary-50: rgb(var(--tw-color-primary-50)); /* #faf5ff */ 465 | --color-primary-100: rgb(var(--tw-color-primary-100)); /* #f3e8ff */ 466 | --color-primary-200: rgb(var(--tw-color-primary-200)); /* #e9d5ff */ 467 | --color-primary-300: rgb(var(--tw-color-primary-300)); /* #d8b4fe */ 468 | --color-primary-400: rgb(var(--tw-color-primary-400)); /* #c084fc */ 469 | --color-primary-500: rgb(var(--tw-color-primary-500)); /* #a855f7 */ 470 | --color-primary-600: rgb(var(--tw-color-primary-600)); /* #9333ea */ 471 | --color-primary-700: rgb(var(--tw-color-primary-700)); /* #7e22ce */ 472 | --color-primary-800: rgb(var(--tw-color-primary-800)); /* #6b21a8 */ 473 | --color-primary-900: rgb(var(--tw-color-primary-900)); /* #581c87 */ 474 | --color-primary-950: rgb(var(--tw-color-primary-950)); /* #3b0764 */ 475 | } 476 | 477 | .fuchsia { 478 | --tw-color-primary-50: 253 244 255; 479 | --tw-color-primary-100: 250 232 255; 480 | --tw-color-primary-200: 245 208 254; 481 | --tw-color-primary-300: 240 171 252; 482 | --tw-color-primary-400: 232 121 249; 483 | --tw-color-primary-500: 217 70 239; 484 | --tw-color-primary-600: 192 38 211; 485 | --tw-color-primary-700: 162 28 175; 486 | --tw-color-primary-800: 134 25 143; 487 | --tw-color-primary-900: 112 26 117; 488 | --tw-color-primary-950: 74 4 78; 489 | --color-primary-50: rgb(var(--tw-color-primary-50)); /* #fdf4ff */ 490 | --color-primary-100: rgb(var(--tw-color-primary-100)); /* #fae8ff */ 491 | --color-primary-200: rgb(var(--tw-color-primary-200)); /* #f5d0fe */ 492 | --color-primary-300: rgb(var(--tw-color-primary-300)); /* #f0abfc */ 493 | --color-primary-400: rgb(var(--tw-color-primary-400)); /* #e879f9 */ 494 | --color-primary-500: rgb(var(--tw-color-primary-500)); /* #d946ef */ 495 | --color-primary-600: rgb(var(--tw-color-primary-600)); /* #c026d3 */ 496 | --color-primary-700: rgb(var(--tw-color-primary-700)); /* #a21caf */ 497 | --color-primary-800: rgb(var(--tw-color-primary-800)); /* #86198f */ 498 | --color-primary-900: rgb(var(--tw-color-primary-900)); /* #701a75 */ 499 | --color-primary-950: rgb(var(--tw-color-primary-950)); /* #4a044e */ 500 | } 501 | 502 | .pink { 503 | --tw-color-primary-50: 253 242 248; 504 | --tw-color-primary-100: 252 231 243; 505 | --tw-color-primary-200: 251 207 232; 506 | --tw-color-primary-300: 249 168 212; 507 | --tw-color-primary-400: 244 114 182; 508 | --tw-color-primary-500: 236 72 153; 509 | --tw-color-primary-600: 219 39 119; 510 | --tw-color-primary-700: 190 24 93; 511 | --tw-color-primary-800: 157 23 77; 512 | --tw-color-primary-900: 131 24 67; 513 | --tw-color-primary-950: 80 4 36; 514 | --color-primary-50: rgb(var(--tw-color-primary-50)); /* #fdf2f8 */ 515 | --color-primary-100: rgb(var(--tw-color-primary-100)); /* #fce7f3 */ 516 | --color-primary-200: rgb(var(--tw-color-primary-200)); /* #fbcfe8 */ 517 | --color-primary-300: rgb(var(--tw-color-primary-300)); /* #f9a8d4 */ 518 | --color-primary-400: rgb(var(--tw-color-primary-400)); /* #f472b6 */ 519 | --color-primary-500: rgb(var(--tw-color-primary-500)); /* #ec4899 */ 520 | --color-primary-600: rgb(var(--tw-color-primary-600)); /* #db2777 */ 521 | --color-primary-700: rgb(var(--tw-color-primary-700)); /* #be185d */ 522 | --color-primary-800: rgb(var(--tw-color-primary-800)); /* #9d174d */ 523 | --color-primary-900: rgb(var(--tw-color-primary-900)); /* #831843 */ 524 | --color-primary-950: rgb(var(--tw-color-primary-950)); /* #500724 */ 525 | } 526 | 527 | .rose { 528 | --tw-color-primary-50: 255 241 242; 529 | --tw-color-primary-100: 255 228 230; 530 | --tw-color-primary-200: 254 205 211; 531 | --tw-color-primary-300: 253 164 175; 532 | --tw-color-primary-400: 251 113 133; 533 | --tw-color-primary-500: 244 63 94; 534 | --tw-color-primary-600: 225 29 72; 535 | --tw-color-primary-700: 190 18 60; 536 | --tw-color-primary-800: 159 18 57; 537 | --tw-color-primary-900: 136 19 55; 538 | --tw-color-primary-950: 76 5 25; 539 | --color-primary-50: rgb(var(--tw-color-primary-50)); /* #fff1f2 */ 540 | --color-primary-100: rgb(var(--tw-color-primary-100)); /* #ffe4e6 */ 541 | --color-primary-200: rgb(var(--tw-color-primary-200)); /* #fecdd3 */ 542 | --color-primary-300: rgb(var(--tw-color-primary-300)); /* #fda4af */ 543 | --color-primary-400: rgb(var(--tw-color-primary-400)); /* #fb7185 */ 544 | --color-primary-500: rgb(var(--tw-color-primary-500)); /* #f43f5e */ 545 | --color-primary-600: rgb(var(--tw-color-primary-600)); /* #e11d48 */ 546 | --color-primary-700: rgb(var(--tw-color-primary-700)); /* #be123c */ 547 | --color-primary-800: rgb(var(--tw-color-primary-800)); /* #9f1239 */ 548 | --color-primary-900: rgb(var(--tw-color-primary-900)); /* #881337 */ 549 | --color-primary-950: rgb(var(--tw-color-primary-950)); /* #4c0519 */ 550 | } 551 | -------------------------------------------------------------------------------- /src/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | /* #region /**=========== Primary Color =========== */ 7 | /* !STARTERCONF Customize these variable, copy and paste from /styles/colors.css for list of colors */ 8 | --tw-color-primary-50: 240 249 255; 9 | --tw-color-primary-100: 224 242 254; 10 | --tw-color-primary-200: 186 230 253; 11 | --tw-color-primary-300: 125 211 252; 12 | --tw-color-primary-400: 56 189 248; 13 | --tw-color-primary-500: 14 165 233; 14 | --tw-color-primary-600: 2 132 199; 15 | --tw-color-primary-700: 3 105 161; 16 | --tw-color-primary-800: 7 89 133; 17 | --tw-color-primary-900: 12 74 110; 18 | --color-primary-50: rgb(var(--tw-color-primary-50)); /* #f0f9ff */ 19 | --color-primary-100: rgb(var(--tw-color-primary-100)); /* #e0f2fe */ 20 | --color-primary-200: rgb(var(--tw-color-primary-200)); /* #bae6fd */ 21 | --color-primary-300: rgb(var(--tw-color-primary-300)); /* #7dd3fc */ 22 | --color-primary-400: rgb(var(--tw-color-primary-400)); /* #38bdf8 */ 23 | --color-primary-500: rgb(var(--tw-color-primary-500)); /* #0ea5e9 */ 24 | --color-primary-600: rgb(var(--tw-color-primary-600)); /* #0284c7 */ 25 | --color-primary-700: rgb(var(--tw-color-primary-700)); /* #0369a1 */ 26 | --color-primary-800: rgb(var(--tw-color-primary-800)); /* #075985 */ 27 | --color-primary-900: rgb(var(--tw-color-primary-900)); /* #0c4a6e */ 28 | /* #endregion /**======== Primary Color =========== */ 29 | } 30 | 31 | @layer base { 32 | /* inter var - latin */ 33 | @font-face { 34 | font-family: 'Inter'; 35 | font-style: normal; 36 | font-weight: 100 900; 37 | font-display: block; 38 | src: url('/fonts/inter-var-latin.woff2') format('woff2'); 39 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, 40 | U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, 41 | U+2215, U+FEFF, U+FFFD; 42 | } 43 | 44 | .cursor-newtab { 45 | cursor: url('/images/new-tab.png') 10 10, pointer; 46 | } 47 | 48 | /* #region /**=========== Typography =========== */ 49 | .h0 { 50 | @apply font-primary text-3xl font-bold md:text-5xl; 51 | } 52 | 53 | h1, 54 | .h1 { 55 | @apply font-primary text-2xl font-bold md:text-4xl; 56 | } 57 | 58 | h2, 59 | .h2 { 60 | @apply font-primary text-xl font-bold md:text-3xl; 61 | } 62 | 63 | h3, 64 | .h3 { 65 | @apply font-primary text-lg font-bold md:text-2xl; 66 | } 67 | 68 | h4, 69 | .h4 { 70 | @apply font-primary text-base font-bold md:text-lg; 71 | } 72 | 73 | body, 74 | .p { 75 | @apply font-primary text-sm md:text-base; 76 | } 77 | /* #endregion /**======== Typography =========== */ 78 | 79 | .layout { 80 | /* 1100px */ 81 | max-width: 68.75rem; 82 | @apply mx-auto w-11/12; 83 | } 84 | 85 | .bg-dark a.custom-link { 86 | @apply border-gray-200 hover:border-gray-200/0; 87 | } 88 | 89 | /* Class to adjust with sticky footer */ 90 | .min-h-main { 91 | @apply min-h-[calc(100vh-56px)]; 92 | } 93 | } 94 | 95 | @layer utilities { 96 | .animated-underline { 97 | background-image: linear-gradient(#33333300, #33333300), 98 | linear-gradient( 99 | to right, 100 | var(--color-primary-400), 101 | var(--color-primary-500) 102 | ); 103 | background-size: 100% 2px, 0 2px; 104 | background-position: 100% 100%, 0 100%; 105 | background-repeat: no-repeat; 106 | } 107 | @media (prefers-reduced-motion: no-preference) { 108 | .animated-underline { 109 | transition: 0.3s ease; 110 | transition-property: background-size, color, background-color, 111 | border-color; 112 | } 113 | } 114 | .animated-underline:hover, 115 | .animated-underline:focus-visible { 116 | background-size: 0 2px, 100% 2px; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss'; 2 | import defaultTheme from 'tailwindcss/defaultTheme'; 3 | 4 | export default { 5 | content: ['./src/**/*.{js,jsx,ts,tsx}'], 6 | theme: { 7 | extend: { 8 | fontFamily: { 9 | primary: ['Inter', ...defaultTheme.fontFamily.sans], 10 | }, 11 | colors: { 12 | primary: { 13 | // Customize it on globals.css :root 14 | 50: 'rgb(var(--tw-color-primary-50) / )', 15 | 100: 'rgb(var(--tw-color-primary-100) / )', 16 | 200: 'rgb(var(--tw-color-primary-200) / )', 17 | 300: 'rgb(var(--tw-color-primary-300) / )', 18 | 400: 'rgb(var(--tw-color-primary-400) / )', 19 | 500: 'rgb(var(--tw-color-primary-500) / )', 20 | 600: 'rgb(var(--tw-color-primary-600) / )', 21 | 700: 'rgb(var(--tw-color-primary-700) / )', 22 | 800: 'rgb(var(--tw-color-primary-800) / )', 23 | 900: 'rgb(var(--tw-color-primary-900) / )', 24 | 950: 'rgb(var(--tw-color-primary-950) / )', 25 | }, 26 | dark: '#222222', 27 | }, 28 | keyframes: { 29 | flicker: { 30 | '0%, 19.999%, 22%, 62.999%, 64%, 64.999%, 70%, 100%': { 31 | opacity: '0.99', 32 | filter: 33 | 'drop-shadow(0 0 1px rgba(252, 211, 77)) drop-shadow(0 0 15px rgba(245, 158, 11)) drop-shadow(0 0 1px rgba(252, 211, 77))', 34 | }, 35 | '20%, 21.999%, 63%, 63.999%, 65%, 69.999%': { 36 | opacity: '0.4', 37 | filter: 'none', 38 | }, 39 | }, 40 | shimmer: { 41 | '0%': { 42 | backgroundPosition: '-700px 0', 43 | }, 44 | '100%': { 45 | backgroundPosition: '700px 0', 46 | }, 47 | }, 48 | }, 49 | animation: { 50 | flicker: 'flicker 3s linear infinite', 51 | shimmer: 'shimmer 1.3s linear infinite', 52 | }, 53 | }, 54 | }, 55 | plugins: [require('@tailwindcss/forms')], 56 | } satisfies Config; 57 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 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 | "baseUrl": ".", 17 | "paths": { 18 | "@/*": ["./src/*"], 19 | "~/*": ["./public/*"] 20 | }, 21 | "incremental": true, 22 | "plugins": [ 23 | { 24 | "name": "next" 25 | } 26 | ] 27 | }, 28 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 29 | "exclude": ["node_modules"], 30 | "moduleResolution": ["node_modules", ".next", "node"] 31 | } 32 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "headers": [ 3 | { 4 | "source": "/fonts/inter-var-latin.woff2", 5 | "headers": [ 6 | { 7 | "key": "Cache-Control", 8 | "value": "public, max-age=31536000, immutable" 9 | } 10 | ] 11 | } 12 | ] 13 | } 14 | --------------------------------------------------------------------------------