Initial commit from Create Next App
6
.env.example
Normal file
@@ -0,0 +1,6 @@
|
||||
# !STARTERCONF Duplicate this to .env.local
|
||||
|
||||
# DEVELOPMENT TOOLS
|
||||
# Ideally, don't add them to production deployment envs
|
||||
# !STARTERCONF Change to true if you want to log data
|
||||
NEXT_PUBLIC_SHOW_LOGGER="false"
|
||||
84
.eslintrc.js
Normal file
@@ -0,0 +1,84 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
browser: true,
|
||||
es2021: true,
|
||||
node: true,
|
||||
},
|
||||
plugins: ['@typescript-eslint', 'simple-import-sort', 'unused-imports'],
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'next',
|
||||
'next/core-web-vitals',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'prettier',
|
||||
],
|
||||
rules: {
|
||||
'no-unused-vars': 'off',
|
||||
'no-console': 'warn',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'react/no-unescaped-entities': 'off',
|
||||
|
||||
'react/display-name': 'off',
|
||||
'react/jsx-curly-brace-presence': [
|
||||
'warn',
|
||||
{ props: 'never', children: 'never' },
|
||||
],
|
||||
|
||||
//#region //*=========== Unused Import ===========
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
'unused-imports/no-unused-imports': 'warn',
|
||||
'unused-imports/no-unused-vars': [
|
||||
'warn',
|
||||
{
|
||||
vars: 'all',
|
||||
varsIgnorePattern: '^_',
|
||||
args: 'after-used',
|
||||
argsIgnorePattern: '^_',
|
||||
},
|
||||
],
|
||||
//#endregion //*======== Unused Import ===========
|
||||
|
||||
//#region //*=========== Import Sort ===========
|
||||
'simple-import-sort/exports': 'warn',
|
||||
'simple-import-sort/imports': [
|
||||
'warn',
|
||||
{
|
||||
groups: [
|
||||
// ext library & side effect imports
|
||||
['^@?\\w', '^\\u0000'],
|
||||
// {s}css files
|
||||
['^.+\\.s?css$'],
|
||||
// Lib and hooks
|
||||
['^@/lib', '^@/hooks'],
|
||||
// static data
|
||||
['^@/data'],
|
||||
// components
|
||||
['^@/components', '^@/container'],
|
||||
// zustand store
|
||||
['^@/store'],
|
||||
// Other imports
|
||||
['^@/'],
|
||||
// relative paths up until 3 level
|
||||
[
|
||||
'^\\./?$',
|
||||
'^\\.(?!/?$)',
|
||||
'^\\.\\./?$',
|
||||
'^\\.\\.(?!/?$)',
|
||||
'^\\.\\./\\.\\./?$',
|
||||
'^\\.\\./\\.\\.(?!/?$)',
|
||||
'^\\.\\./\\.\\./\\.\\./?$',
|
||||
'^\\.\\./\\.\\./\\.\\.(?!/?$)',
|
||||
],
|
||||
['^@/types'],
|
||||
// other that didnt fit in
|
||||
['^'],
|
||||
],
|
||||
},
|
||||
],
|
||||
//#endregion //*======== Import Sort ===========
|
||||
},
|
||||
globals: {
|
||||
React: true,
|
||||
JSX: true,
|
||||
},
|
||||
};
|
||||
14
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
# !STARTERCONF You can delete this file :) Your support is much appreciated!
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: theodorusclarence
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||
custom: ['https://saweria.co/theodorusclarence']
|
||||
9
.github/issue-branch.yml
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
# https://github.com/robvanderleek/create-issue-branch#option-2-configure-github-action
|
||||
|
||||
# ex: i4-lower_camel_upper
|
||||
branchName: 'i${issue.number}-${issue.title,}'
|
||||
branches:
|
||||
- label: epic
|
||||
skip: true
|
||||
- label: debt
|
||||
skip: true
|
||||
15
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
# Description & Technical Solution
|
||||
|
||||
Describe problems, if any, clearly and concisely.
|
||||
Summarize the impact to the system.
|
||||
Please also include relevant motivation and context.
|
||||
Please include a summary of the technical solution and how it solves the problem.
|
||||
|
||||
# Checklist
|
||||
|
||||
- [ ] I have commented my code, particularly in hard-to-understand areas.
|
||||
- [ ] Already rebased against main branch.
|
||||
|
||||
# Screenshots
|
||||
|
||||
Provide screenshots or videos of the changes made if any.
|
||||
14
.github/workflows/create-branch.yml
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
name: Create Branch from Issue
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [assigned]
|
||||
|
||||
jobs:
|
||||
create_issue_branch_job:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Create Issue Branch
|
||||
uses: robvanderleek/create-issue-branch@main
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
16
.github/workflows/issue-autolink.yml
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
name: 'Issue Autolink'
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
issue-links:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: tkt-actions/add-issue-links@v1.8.1
|
||||
with:
|
||||
repo-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
branch-prefix: 'i'
|
||||
resolve: 'true'
|
||||
44
.github/workflows/lint.yml
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
name: Code Check
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request: {}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.job }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: ⬣ ESLint, ʦ TypeScript, 💅 Prettier, and 🃏 Test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: ⬇️ Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 🤌 Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- name: ⎔ Setup node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: 📥 Download deps
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: 🔬 Lint
|
||||
run: pnpm run lint:strict
|
||||
|
||||
- name: 🔎 Type check
|
||||
run: pnpm run typecheck
|
||||
|
||||
- name: 💅 Prettier check
|
||||
run: pnpm run format:check
|
||||
|
||||
- name: 🃏 Run jest
|
||||
run: pnpm run test
|
||||
17
.github/workflows/release-please.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
name: release-please
|
||||
on:
|
||||
# !STARTERCONF Choose your preferred event
|
||||
# !Option 1: Manual Trigger from GitHub
|
||||
workflow_dispatch:
|
||||
# !Option 2: Release on every push on main branch
|
||||
# push:
|
||||
# branches:
|
||||
# - main
|
||||
jobs:
|
||||
release-please:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: google-github-actions/release-please-action@v3
|
||||
with:
|
||||
release-type: node
|
||||
package-name: release-please-action
|
||||
42
.gitignore
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# local env files
|
||||
.env
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# next-sitemap
|
||||
robots.txt
|
||||
sitemap.xml
|
||||
sitemap-*.xml
|
||||
4
.husky/commit-msg
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npx --no-install commitlint --edit "$1"
|
||||
4
.husky/post-merge
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
pnpm install
|
||||
4
.husky/pre-commit
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npx lint-staged
|
||||
41
.prettierignore
Normal file
@@ -0,0 +1,41 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
.next
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# changelog
|
||||
CHANGELOG.md
|
||||
|
||||
pnpm-lock.yaml
|
||||
7
.prettierrc.js
Normal file
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
arrowParens: 'always',
|
||||
singleQuote: true,
|
||||
jsxSingleQuote: true,
|
||||
tabWidth: 2,
|
||||
semi: true,
|
||||
};
|
||||
10
.vscode/css.code-snippets
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"Region CSS": {
|
||||
"prefix": "regc",
|
||||
"body": [
|
||||
"/* #region /**=========== ${1} =========== */",
|
||||
"$0",
|
||||
"/* #endregion /**======== ${1} =========== */"
|
||||
]
|
||||
}
|
||||
}
|
||||
9
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"recommendations": [
|
||||
// Tailwind CSS Intellisense
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"esbenp.prettier-vscode",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"aaron-bond.better-comments"
|
||||
]
|
||||
}
|
||||
17
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"css.validate": false,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.tabSize": 2,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": "explicit"
|
||||
},
|
||||
"[css]": { "editor.defaultFormatter": "esbenp.prettier-vscode" },
|
||||
// Tailwind CSS Autocomplete, add more if used in projects
|
||||
"tailwindCSS.classAttributes": [
|
||||
"class",
|
||||
"className",
|
||||
"classNames",
|
||||
"containerClassName"
|
||||
],
|
||||
"typescript.preferences.importModuleSpecifier": "non-relative"
|
||||
}
|
||||
193
.vscode/typescriptreact.code-snippets
vendored
Normal file
@@ -0,0 +1,193 @@
|
||||
{
|
||||
//#region //*=========== React ===========
|
||||
"import React": {
|
||||
"prefix": "ir",
|
||||
"body": ["import * as React from 'react';"]
|
||||
},
|
||||
"React.useState": {
|
||||
"prefix": "us",
|
||||
"body": [
|
||||
"const [${1}, set${1/(^[a-zA-Z])(.*)/${1:/upcase}${2}/}] = React.useState<$3>(${2:initial${1/(^[a-zA-Z])(.*)/${1:/upcase}${2}/}})$0"
|
||||
]
|
||||
},
|
||||
"React.useEffect": {
|
||||
"prefix": "uf",
|
||||
"body": ["React.useEffect(() => {", " $0", "}, []);"]
|
||||
},
|
||||
"React.useReducer": {
|
||||
"prefix": "ur",
|
||||
"body": [
|
||||
"const [state, dispatch] = React.useReducer(${0:someReducer}, {",
|
||||
" ",
|
||||
"})"
|
||||
]
|
||||
},
|
||||
"React.useRef": {
|
||||
"prefix": "urf",
|
||||
"body": ["const ${1:someRef} = React.useRef($0)"]
|
||||
},
|
||||
"React Functional Component": {
|
||||
"prefix": "rc",
|
||||
"body": [
|
||||
"import * as React from 'react';\n",
|
||||
"export default function ${1:${TM_FILENAME_BASE}}() {",
|
||||
" return (",
|
||||
" <div>",
|
||||
" $0",
|
||||
" </div>",
|
||||
" )",
|
||||
"}"
|
||||
]
|
||||
},
|
||||
"React Functional Component with Props": {
|
||||
"prefix": "rcp",
|
||||
"body": [
|
||||
"import * as React from 'react';\n",
|
||||
"import clsxm from '@/lib/clsxm';\n",
|
||||
"type ${1:${TM_FILENAME_BASE}}Props= {\n",
|
||||
"} & React.ComponentPropsWithoutRef<'div'>\n",
|
||||
"export default function ${1:${TM_FILENAME_BASE}}({className, ...rest}: ${1:${TM_FILENAME_BASE}}Props) {",
|
||||
" return (",
|
||||
" <div className={clsxm(['', className])} {...rest}>",
|
||||
" $0",
|
||||
" </div>",
|
||||
" )",
|
||||
"}"
|
||||
]
|
||||
},
|
||||
//#endregion //*======== React ===========
|
||||
|
||||
//#region //*=========== Commons ===========
|
||||
"Region": {
|
||||
"prefix": "reg",
|
||||
"scope": "javascript, typescript, javascriptreact, typescriptreact",
|
||||
"body": [
|
||||
"//#region //*=========== ${1} ===========",
|
||||
"${TM_SELECTED_TEXT}$0",
|
||||
"//#endregion //*======== ${1} ==========="
|
||||
]
|
||||
},
|
||||
"Region CSS": {
|
||||
"prefix": "regc",
|
||||
"scope": "css, scss",
|
||||
"body": [
|
||||
"/* #region /**=========== ${1} =========== */",
|
||||
"${TM_SELECTED_TEXT}$0",
|
||||
"/* #endregion /**======== ${1} =========== */"
|
||||
]
|
||||
},
|
||||
//#endregion //*======== Commons ===========
|
||||
|
||||
//#region //*=========== Next.js ===========
|
||||
"Next Pages": {
|
||||
"prefix": "np",
|
||||
"body": [
|
||||
"import * as React from 'react';\n",
|
||||
"import Layout from '@/components/layout/Layout';",
|
||||
"import Seo from '@/components/Seo';\n",
|
||||
"export default function ${1:${TM_FILENAME_BASE/(^[a-zA-Z])(.*)/${1:/upcase}${2}/}}Page() {",
|
||||
" return (",
|
||||
" <Layout>",
|
||||
" <Seo templateTitle='${1:${TM_FILENAME_BASE/(^[a-zA-Z])(.*)/${1:/upcase}${2}/}}' />\n",
|
||||
" <main>\n",
|
||||
" <section className=''>",
|
||||
" <div className='layout py-20 min-h-screen'>",
|
||||
" $0",
|
||||
" </div>",
|
||||
" </section>",
|
||||
" </main>",
|
||||
" </Layout>",
|
||||
" )",
|
||||
"}"
|
||||
]
|
||||
},
|
||||
"Next API": {
|
||||
"prefix": "napi",
|
||||
"body": [
|
||||
"import { NextApiRequest, NextApiResponse } from 'next';\n",
|
||||
"export default async function handler(req: NextApiRequest, res: NextApiResponse) {",
|
||||
" if (req.method === 'GET') {",
|
||||
" res.status(200).json({ name: 'Bambang' });",
|
||||
" } else {",
|
||||
" res.status(405).json({ message: 'Method Not Allowed' });",
|
||||
" }",
|
||||
"}"
|
||||
]
|
||||
},
|
||||
"Get Static Props": {
|
||||
"prefix": "gsp",
|
||||
"body": [
|
||||
"export const getStaticProps = async (context: GetStaticPropsContext) => {",
|
||||
" return {",
|
||||
" props: {}",
|
||||
" };",
|
||||
"}"
|
||||
]
|
||||
},
|
||||
"Get Static Paths": {
|
||||
"prefix": "gspa",
|
||||
"body": [
|
||||
"export const getStaticPaths: GetStaticPaths = async () => {",
|
||||
" return {",
|
||||
" paths: [",
|
||||
" { params: { $1 }}",
|
||||
" ],",
|
||||
" fallback: ",
|
||||
" };",
|
||||
"}"
|
||||
]
|
||||
},
|
||||
"Get Server Side Props": {
|
||||
"prefix": "gssp",
|
||||
"body": [
|
||||
"export const getServerSideProps = async (context: GetServerSidePropsContext) => {",
|
||||
" return {",
|
||||
" props: {}",
|
||||
" };",
|
||||
"}"
|
||||
]
|
||||
},
|
||||
"Infer Get Static Props": {
|
||||
"prefix": "igsp",
|
||||
"body": "InferGetStaticPropsType<typeof getStaticProps>"
|
||||
},
|
||||
"Infer Get Server Side Props": {
|
||||
"prefix": "igssp",
|
||||
"body": "InferGetServerSidePropsType<typeof getServerSideProps>"
|
||||
},
|
||||
"Import useRouter": {
|
||||
"prefix": "imust",
|
||||
"body": ["import { useRouter } from 'next/router';"]
|
||||
},
|
||||
"Import Next Image": {
|
||||
"prefix": "imimg",
|
||||
"body": ["import Image from 'next/image';"]
|
||||
},
|
||||
"Import Next Link": {
|
||||
"prefix": "iml",
|
||||
"body": ["import Link from 'next/link';"]
|
||||
},
|
||||
//#endregion //*======== Next.js ===========
|
||||
|
||||
//#region //*=========== Snippet Wrap ===========
|
||||
"Wrap with Fragment": {
|
||||
"prefix": "ff",
|
||||
"body": ["<>", "\t${TM_SELECTED_TEXT}", "</>"]
|
||||
},
|
||||
"Wrap with clsx": {
|
||||
"prefix": "cx",
|
||||
"body": ["{clsx([${TM_SELECTED_TEXT}$0])}"]
|
||||
},
|
||||
"Wrap with clsxm": {
|
||||
"prefix": "cxm",
|
||||
"body": ["{clsxm([${TM_SELECTED_TEXT}$0, className])}"]
|
||||
},
|
||||
//#endregion //*======== Snippet Wrap ===========
|
||||
|
||||
"Logger": {
|
||||
"prefix": "lg",
|
||||
"body": [
|
||||
"logger({ ${1:${CLIPBOARD}} }, '${TM_FILENAME} line ${TM_LINE_NUMBER}')"
|
||||
]
|
||||
}
|
||||
}
|
||||
431
CHANGELOG.md
Normal file
@@ -0,0 +1,431 @@
|
||||
<!-- //!STARTERCONF Remove this file, this is used as the starter changelog -->
|
||||
|
||||
# ts-nextjs-tailwind-starter changelog
|
||||
|
||||
This changelog is manually generated and not accurate with the package.json, only to show the changes since the last release.
|
||||
|
||||
## 1.0.0 - 2023-07-17
|
||||
|
||||
### New Features
|
||||
|
||||
- #### Next.js App Router
|
||||
|
||||
Now uses the new app directory structure.
|
||||
|
||||
### Improvements & Bug Fixes
|
||||
|
||||
- #### Rename `clsxm` to `cn`
|
||||
|
||||
For better support with shadcn/ui
|
||||
|
||||
- #### Faster Lint Actions
|
||||
|
||||
Lint jobs is now merged into one for faster performance, also updated the concurrency rule
|
||||
|
||||
## 0.5.4 - 2022-07-22
|
||||
|
||||
### New Features
|
||||
|
||||
- #### Release Please
|
||||
|
||||
Standard Version is now deprecated, and ts-nextjs-tailwind-starter is now using release please. Activate them on `.github/workflows/release-please`
|
||||
|
||||
### Improvements & Bug Fixes
|
||||
|
||||
- #### More Efficient Lint Actions
|
||||
|
||||
Lint workflow is now cached and will cancel previous run if there are 2 concurrent runs.
|
||||
|
||||
## 0.5.3 - 2022-02-27
|
||||
|
||||
### New Features
|
||||
|
||||
- #### Shimmer for NextImage and Skeleton
|
||||
|
||||
Addition of shimmer & blur placeholder for NextImage, and new Skeleton Component with shimmer effect.
|
||||
|
||||
https://user-images.githubusercontent.com/55318172/155867729-8c3176ad-ede4-4443-b42b-780517615e5a.mp4
|
||||
|
||||
|
||||
- #### Support for SVGR
|
||||
|
||||
You can directly import SVG like
|
||||
|
||||
```tsx
|
||||
import Vercel from '~/svg/Vercel.svg';
|
||||
|
||||
<Vercel className='text-5xl' />
|
||||
```
|
||||
|
||||
- #### Public Folder Path Mapping
|
||||
|
||||
Easily access public folder with `~/` prefix.
|
||||
|
||||
- #### Tailwind CSS Prettier Sorter
|
||||
|
||||
ts-nextjs-tailwind-starter now use first-party plugin `prettier-plugin-tailwindcss`
|
||||
|
||||
### Improvements & Bug Fixes
|
||||
|
||||
- #### Layout Declared Twice
|
||||
|
||||
Fix issue where adding elements to Layout ends up rendering them twice
|
||||
|
||||
- #### ESLint Curly Brace Rule
|
||||
|
||||
New autofixable rule
|
||||
|
||||
```tsx
|
||||
props={'hi'}
|
||||
|
||||
will become
|
||||
|
||||
props='hi'
|
||||
```
|
||||
|
||||
## 0.5.2 - 2021-12-30
|
||||
|
||||
### New Features
|
||||
|
||||
- #### New Component: PrimaryLink
|
||||
|
||||
Add a link component with accent color on top of UnstyledLink.
|
||||
|
||||
### Improvements & Bug Fixes
|
||||
|
||||
- #### Unused Import ESlint Autofix
|
||||
|
||||
Unused import will automatically be removed by the ESlint autofix.
|
||||
|
||||
- #### Renamed CustomLink to UnderlineLink
|
||||
|
||||
This is to compensate the new PrimaryLink component
|
||||
|
||||
- #### Primary Button & ButtonLink Shade
|
||||
|
||||
The shade of the button is now using the `500` instead of `400`.
|
||||
|
||||
|
||||
## 0.5.1 - 2021-12-26
|
||||
|
||||
### New Features
|
||||
|
||||
- #### New Snippets Wrap: clsx and fragment `<></>`
|
||||
|
||||
You can select text then wrap it with clsx or React.Fragment shorthand.
|
||||
|
||||
https://user-images.githubusercontent.com/55318172/147401848-3db5dba0-ef71-4f25-9f47-c7908beba69e.mp4
|
||||
|
||||
|
||||
## 0.5.0 - 2021-12-21
|
||||
|
||||
### New Features
|
||||
|
||||
- #### Expansion Pack
|
||||
You can easily add expansion such as React Hook Form + Components, Storybook, and more just using a single command line.
|
||||
|
||||
https://user-images.githubusercontent.com/55318172/146631994-e1cac137-1664-4cfe-950b-a96decc1eaa6.mp4
|
||||
|
||||
Check out the [expansion pack repository](https://github.com/theodorusclarence/expansion-pack) for the commands
|
||||
|
||||
### Improvements & Bug Fixes
|
||||
|
||||
- #### Can't Use Layout Fill on NextImage
|
||||
|
||||
Using layout fill will make the width and height optional
|
||||
|
||||
- #### Vertically center Button & ButtonLink
|
||||
|
||||
Adds `items-center` to make the button vertically centered
|
||||
|
||||
|
||||
## 0.4.1 - 2021-12-12
|
||||
|
||||
### New Features
|
||||
|
||||
- #### Tailwind CSS v3
|
||||
|
||||
The color palette configuration is also updated accordingly.
|
||||
|
||||
## 0.4.0 - 2021-12-02
|
||||
|
||||
### New Features
|
||||
|
||||
- #### Button & ButtonLink Variants
|
||||
|
||||
- New Variant: **Outline** and **Ghost**, you can also add `isDarkBg` prop if you are using these variants with dark background.
|
||||
- Animated Underline style on **Primary**, **Dark**, **Light** is removed
|
||||
- Added `ring-primary-500` on `focus-visible`
|
||||
|
||||

|
||||
|
||||
- #### ArrowLink
|
||||
|
||||
|
||||
Adds an animated arrow, this component is Polymorphic, the default element is `CustomLink`, you can extend it with `as` prop.
|
||||
|
||||
```tsx
|
||||
<ArrowLink
|
||||
as={ButtonLink}
|
||||
variant='light'
|
||||
href='/'
|
||||
>
|
||||
Register now
|
||||
</ArrowLink>
|
||||
```
|
||||
|
||||

|
||||
|
||||
- #### Change default theme to white
|
||||
|
||||
|  |  |
|
||||
| - | - |
|
||||
|
||||
### Improvements & Bug Fixes
|
||||
|
||||
- #### Split Next.js Link Props Type
|
||||
|
||||
Now, to add props to Next.js `<Link>` component, you can use `nextLinkProps`.
|
||||
|
||||
```tsx
|
||||
<UnstyledLink
|
||||
href='/'
|
||||
nextLinkProps={{
|
||||
shallow: true,
|
||||
}}
|
||||
>
|
||||
Link
|
||||
</UnstyledLink>
|
||||
```
|
||||
|
||||
The rest of `<a>` props can be directly added as a prop.
|
||||
|
||||
- #### Add Motion Safe to Animations
|
||||
|
||||
All components animation respect user preference about motion.
|
||||
|
||||
## 0.3.0 - 2021-12-01
|
||||
|
||||
### New Features
|
||||
|
||||
- #### Create Branch & Auto Resolve Issue Actions
|
||||
|
||||
|  <br> Auto Create Branch |  <br> Auto Resolve |
|
||||
| :--: | :--: |
|
||||
|
||||
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.
|
||||
|
||||
The branch will be created on **assign** with format `i${number}-${issue_title_lowercase}`.
|
||||
|
||||
- #### Custom Tailwind CSS Class Sorter
|
||||
|
||||
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)
|
||||
|
||||
With this plugin, we can now safely check the order of the classes using the preconfigured lint action.
|
||||
|
||||
## 0.2.0 - 2021-11-10
|
||||
|
||||
### New Features
|
||||
|
||||
- #### Jest
|
||||
|
||||
Jest is configured and will be run every push on Github Actions
|
||||
|
||||
- #### Lint Github Action
|
||||
|
||||
1. **ESLint** - will fail if there are any warning and error.
|
||||
2. **Type Check** - will fail on `tsc` error.
|
||||
3. **Prettier Check** - will fail if there are any formatting error.
|
||||
4. **Test** - will fail if there are any test failure.
|
||||
|
||||
## 0.1.0
|
||||
|
||||
### New Features
|
||||
|
||||
- #### Installed Packages
|
||||
|
||||
1. [clsx](https://bundlephobia.com/package/clsx@latest), utility for constructing `className` strings conditionally.
|
||||
2. [react-icons](https://bundlephobia.com/package/react-icons@latest), svg react icons of popular icon packs.
|
||||
|
||||
- #### UnstyledLink Component
|
||||
|
||||
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
|
||||
|
||||
- #### CustomLink Component
|
||||
|
||||

|
||||
|
||||
|
||||
- #### Absolute Import
|
||||
|
||||
You can import without using relative path
|
||||
|
||||
```tsx
|
||||
import Nav from '../../../components/Nav';
|
||||
|
||||
simplified to
|
||||
|
||||
import Nav from '@/components/Nav';
|
||||
```
|
||||
|
||||
- #### Seo Component
|
||||
|
||||
Configure the default in `src/components/Seo.tsx`. If you want to use the default, just add `<Seo />` on top of your page.
|
||||
|
||||
You can also customize it per page by overriding the title, description as props
|
||||
|
||||
```tsx
|
||||
<Seo title='Next.js Tailwind Starter' description='your description' />
|
||||
```
|
||||
|
||||
or if you want to still keep the title like `%s | Next.js Tailwind Starter`, you can use `templateTitle` props.
|
||||
|
||||
- #### Custom 404 Page
|
||||
|
||||

|
||||
|
||||
- #### Workspace Snippets
|
||||
|
||||
Snippets such as React import, useState, useEffect, React Component. [View more](/.vscode/typescriptreact.code-snippets)
|
||||
|
||||
- #### Husky, Prettier, Lint, and Commitlint Configured
|
||||
|
||||
3 Husky hooks including:
|
||||
|
||||
1. pre-commit, running `next lint` and format the code using prettier
|
||||
2. commit-msg, running commitlint to ensure the use of [Conventional Commit](https://theodorusclarence.com/library/conventional-commit-readme) for commit messages
|
||||
3. post-merge, running `yarn` every `git pull` or after merge to ensure all new packages are installed and ready to go
|
||||
|
||||
- #### Default Favicon Declaration
|
||||
|
||||
Use [Favicon Generator](https://www.favicon-generator.org/) and then overwrite the files in `/public/favicon`
|
||||
|
||||
- #### Default Tailwind CSS Base Styles
|
||||
|
||||
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)
|
||||
|
||||
- #### Open Graph Generator
|
||||
|
||||
|  |  |
|
||||
| --------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
|
||||
|
||||
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.
|
||||
|
||||
Check out the [repository](https://github.com/theodorusclarence/og) to see the API parameters.
|
||||
|
||||
- #### Preloaded & Self Hosted Inter Fonts
|
||||
|
||||
Inter fonts is a variable fonts that is self hosted and preloaded.
|
||||
|
||||
## Snippets
|
||||
|
||||
This starter is equipped with workspace-snippets, it is encouraged to use it, especially the `np` and `rc`
|
||||
|
||||
### Next.js Page
|
||||
|
||||
File inside `src/pages` will be the webpage route, there are 2 things that need to be added in Next.js page:
|
||||
|
||||
1. Seo component
|
||||
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).
|
||||
|
||||
Snippets: `np`
|
||||
|
||||
```tsx
|
||||
import * as React from 'react';
|
||||
import Seo from '@/components/Seo';
|
||||
export default function TestPage() {
|
||||
return (
|
||||
<>
|
||||
<Seo templateTitle='Test' />
|
||||
<main>
|
||||
<section className=''>
|
||||
<div className='layout'></div>
|
||||
</section>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### React Components
|
||||
|
||||
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.
|
||||
|
||||
Snippets: `rc`
|
||||
|
||||
```tsx
|
||||
import * as React from 'react';
|
||||
export default function Component() {
|
||||
return <div></div>;
|
||||
}
|
||||
```
|
||||
|
||||
#### Import React
|
||||
|
||||
Snippets: `ir`
|
||||
|
||||
```tsx
|
||||
import * as React from 'react';
|
||||
```
|
||||
|
||||
#### Import Next Image
|
||||
|
||||
Snippets: `imimg`
|
||||
|
||||
```tsx
|
||||
import Image from 'next/image';
|
||||
```
|
||||
|
||||
#### Import Next Link
|
||||
|
||||
Snippets: `iml`
|
||||
|
||||
```tsx
|
||||
import Link from 'next/link';
|
||||
```
|
||||
|
||||
#### useState Hook
|
||||
|
||||
Snippets: `us`
|
||||
|
||||
```tsx
|
||||
const [state, setState] = React.useState(initialState);
|
||||
```
|
||||
|
||||
#### useEffect Hook
|
||||
|
||||
Snippets: `uf`
|
||||
|
||||
```tsx
|
||||
React.useEffect(() => {}, []);
|
||||
```
|
||||
|
||||
#### useReducer Hook
|
||||
|
||||
Snippets: `ur`
|
||||
|
||||
```tsx
|
||||
const [state, dispatch] = React.useReducer(someReducer, {});
|
||||
```
|
||||
|
||||
#### useRef Hook
|
||||
|
||||
Snippets: `urf`
|
||||
|
||||
```tsx
|
||||
const someRef = React.useRef();
|
||||
```
|
||||
|
||||
#### Region Comment
|
||||
|
||||
It is really useful when we need to group code. It is also collapsible in VSCode
|
||||
|
||||
Snippets: `reg`
|
||||
|
||||
```tsx
|
||||
//#region //*============== FORM SUBMIT
|
||||
//#endregion //*============== FORM SUBMIT
|
||||
```
|
||||
|
||||
You should also use [Better Comments](https://marketplace.visualstudio.com/items?itemName=aaron-bond.better-comments) extension.
|
||||
|
||||
133
README.md
Normal file
@@ -0,0 +1,133 @@
|
||||
# Next.js + Tailwind CSS + TypeScript Starter and Boilerplate
|
||||
|
||||
<div align="center">
|
||||
<h2>🔋 ts-nextjs-tailwind-starter</h2>
|
||||
<p>Next.js + Tailwind CSS + TypeScript starter packed with useful development features.</p>
|
||||
<p>Made by <a href="https://theodorusclarence.com">Theodorus Clarence</a></p>
|
||||
|
||||
[](https://github.com/theodorusclarence/ts-nextjs-tailwind-starter/stargazers)
|
||||
[](https://depfu.com/github/theodorusclarence/ts-nextjs-tailwind-starter?project_id=30160)
|
||||
[](https://shields.io/)
|
||||
|
||||
</div>
|
||||
|
||||
## Features
|
||||
|
||||
This repository is 🔋 battery packed with:
|
||||
|
||||
- ⚡️ Next.js 14 with App Router
|
||||
- ⚛️ React 18
|
||||
- ✨ TypeScript
|
||||
- 💨 Tailwind CSS 3 — Configured with CSS Variables to extend the **primary** color
|
||||
- 💎 Pre-built Components — Components that will **automatically adapt** with your brand color, [check here for the demo](https://tsnext-tw.thcl.dev/components)
|
||||
- 🃏 Jest — Configured for unit testing
|
||||
- 📈 Absolute Import and Path Alias — Import components using `@/` prefix
|
||||
- 📏 ESLint — Find and fix problems in your code, also will **auto sort** your imports
|
||||
- 💖 Prettier — Format your code consistently
|
||||
- 🐶 Husky & Lint Staged — Run scripts on your staged files before they are committed
|
||||
- 🤖 Conventional Commit Lint — Make sure you & your teammates follow conventional commit
|
||||
- ⏰ Release Please — Generate your changelog by activating the `release-please` workflow
|
||||
- 👷 Github Actions — Lint your code on PR
|
||||
- 🚘 Automatic Branch and Issue Autolink — Branch will be automatically created on issue **assign**, and auto linked on PR
|
||||
- 🔥 Snippets — A collection of useful snippets
|
||||
- 👀 Open Graph Helper Function — Awesome open graph generated using [og](https://github.com/theodorusclarence/og), fork it and deploy!
|
||||
- 🗺 Site Map — Automatically generate sitemap.xml
|
||||
- 📦 Expansion Pack — Easily install common libraries, additional components, and configs.
|
||||
|
||||
See the 👉 [feature details and changelog](https://github.com/theodorusclarence/ts-nextjs-tailwind-starter/blob/main/CHANGELOG.md) 👈 for more.
|
||||
|
||||
You can also check all of the **details and demos** on my blog post:
|
||||
|
||||
- [One-stop Starter to Maximize Efficiency on Next.js & Tailwind CSS Projects](https://theodorusclarence.com/blog/one-stop-starter)
|
||||
|
||||
## Getting Started
|
||||
|
||||
### 1. Clone this template using one of the three ways
|
||||
|
||||
1. Use this repository as template
|
||||
|
||||
**Disclosure:** by using this repository as a template, there will be an attribution on your repository.
|
||||
|
||||
I'll appreciate if you do, so this template can be known by others too 😄
|
||||
|
||||

|
||||
|
||||
2. Using `create-next-app`
|
||||
|
||||
```bash
|
||||
pnpm create next-app -e https://github.com/theodorusclarence/ts-nextjs-tailwind-starter ts-pnpm
|
||||
```
|
||||
|
||||
If you still want to use **pages directory** (_is not actively maintained_) you can use this command
|
||||
|
||||
```bash
|
||||
npx create-next-app -e https://github.com/theodorusclarence/ts-nextjs-tailwind-starter/tree/pages-directory project-name
|
||||
```
|
||||
|
||||
3. Using `degit`
|
||||
|
||||
```bash
|
||||
npx degit theodorusclarence/ts-nextjs-tailwind-starter YOUR_APP_NAME
|
||||
```
|
||||
|
||||
4. Deploy to Vercel
|
||||
|
||||
[](https://vercel.com/new/git/external?repository-url=https%3A%2F%2Fgithub.com%2Ftheodorusclarence%2Fts-nextjs-tailwind-starter)
|
||||
|
||||
### 2. Install dependencies
|
||||
|
||||
It is encouraged to use **pnpm** so the husky hooks can work properly.
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
### 3. Run the development server
|
||||
|
||||
You can start the server using this command:
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
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`.
|
||||
|
||||
### 4. Change defaults
|
||||
|
||||
There are some things you need to change including title, urls, favicons, etc.
|
||||
|
||||
Find all comments with !STARTERCONF, then follow the guide.
|
||||
|
||||
Don't forget to change the package name in package.json
|
||||
|
||||
### 5. Commit Message Convention
|
||||
|
||||
This starter is using [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/), it is mandatory to use it to commit changes.
|
||||
|
||||
## Projects using ts-nextjs-tailwind-starter
|
||||
|
||||
<!--
|
||||
TEMPLATE
|
||||
- [sitename](https://sitelink.com) ([Source](https://github.com/githublink))
|
||||
- [sitename](https://sitelink.com)
|
||||
-->
|
||||
|
||||
- [theodorusclarence.com](https://theodorusclarence.com) ([Source](https://github.com/theodorusclarence/theodorusclarence.com))
|
||||
- [Notiolink](https://notiolink.thcl.dev/) ([Source](https://github.com/theodorusclarence/notiolink))
|
||||
- [NextJs + Materia UI + Typescript](https://github.com/AlexStack/nextjs-materia-mui-typescript-hook-form-scaffold-boilerplate-starter)
|
||||
|
||||
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). 😃
|
||||
|
||||
## Expansion Pack 📦
|
||||
|
||||
This starter is now equipped with an [expansion pack](https://github.com/theodorusclarence/expansion-pack).
|
||||
|
||||
You can easily add expansion such as React Hook Form + Components, Storybook, and more just using a single command line.
|
||||
|
||||
<https://user-images.githubusercontent.com/55318172/146631994-e1cac137-1664-4cfe-950b-a96decc1eaa6.mp4>
|
||||
|
||||
Check out the [expansion pack repository](https://github.com/theodorusclarence/expansion-pack) for the commands
|
||||
|
||||
### App Router Update
|
||||
|
||||
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.
|
||||
24
commitlint.config.js
Normal file
@@ -0,0 +1,24 @@
|
||||
module.exports = {
|
||||
extends: ['@commitlint/config-conventional'],
|
||||
rules: {
|
||||
// TODO Add Scope Enum Here
|
||||
// 'scope-enum': [2, 'always', ['yourscope', 'yourscope']],
|
||||
'type-enum': [
|
||||
2,
|
||||
'always',
|
||||
[
|
||||
'feat',
|
||||
'fix',
|
||||
'docs',
|
||||
'chore',
|
||||
'style',
|
||||
'refactor',
|
||||
'ci',
|
||||
'test',
|
||||
'perf',
|
||||
'revert',
|
||||
'vercel',
|
||||
],
|
||||
],
|
||||
},
|
||||
};
|
||||
30
jest.config.js
Normal file
@@ -0,0 +1,30 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const nextJest = require('next/jest');
|
||||
|
||||
const createJestConfig = nextJest({
|
||||
// Provide the path to your Next.js app to load next.config.js and .env files in your test environment
|
||||
dir: './',
|
||||
});
|
||||
|
||||
// Add any custom config to be passed to Jest
|
||||
const customJestConfig = {
|
||||
// Add more setup options before each test is run
|
||||
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
|
||||
|
||||
// if using TypeScript with a baseUrl set to the root directory then you need the below for alias' to work
|
||||
moduleDirectories: ['node_modules', '<rootDir>/'],
|
||||
|
||||
testEnvironment: 'jest-environment-jsdom',
|
||||
|
||||
/**
|
||||
* Absolute imports and Module Path Aliases
|
||||
*/
|
||||
moduleNameMapper: {
|
||||
'^@/(.*)$': '<rootDir>/src/$1',
|
||||
'^~/(.*)$': '<rootDir>/public/$1',
|
||||
'^.+\\.(svg)$': '<rootDir>/src/__mocks__/svg.tsx',
|
||||
},
|
||||
};
|
||||
|
||||
// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
|
||||
module.exports = createJestConfig(customJestConfig);
|
||||
5
jest.setup.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
|
||||
// Allow router mocks.
|
||||
// eslint-disable-next-line no-undef
|
||||
jest.mock('next/router', () => require('next-router-mock'));
|
||||
13
next-sitemap.config.js
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* @type {import('next-sitemap').IConfig}
|
||||
* @see https://github.com/iamvishnusankar/next-sitemap#readme
|
||||
*/
|
||||
module.exports = {
|
||||
// !STARTERCONF Change the siteUrl
|
||||
/** Without additional '/' on the end, e.g. https://theodorusclarence.com */
|
||||
siteUrl: 'https://tsnext-tw.thcl.dev',
|
||||
generateRobotsTxt: true,
|
||||
robotsTxtOptions: {
|
||||
policies: [{ userAgent: '*', allow: '/' }],
|
||||
},
|
||||
};
|
||||
53
next.config.js
Normal file
@@ -0,0 +1,53 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
eslint: {
|
||||
dirs: ['src'],
|
||||
},
|
||||
|
||||
reactStrictMode: true,
|
||||
swcMinify: true,
|
||||
|
||||
// Uncoment to add domain whitelist
|
||||
// images: {
|
||||
// remotePatterns: [
|
||||
// {
|
||||
// protocol: 'https',
|
||||
// hostname: 'res.cloudinary.com',
|
||||
// },
|
||||
// ]
|
||||
// },
|
||||
|
||||
webpack(config) {
|
||||
// Grab the existing rule that handles SVG imports
|
||||
const fileLoaderRule = config.module.rules.find((rule) =>
|
||||
rule.test?.test?.('.svg')
|
||||
);
|
||||
|
||||
config.module.rules.push(
|
||||
// Reapply the existing rule, but only for svg imports ending in ?url
|
||||
{
|
||||
...fileLoaderRule,
|
||||
test: /\.svg$/i,
|
||||
resourceQuery: /url/, // *.svg?url
|
||||
},
|
||||
// Convert all other *.svg imports to React components
|
||||
{
|
||||
test: /\.svg$/i,
|
||||
issuer: { not: /\.(css|scss|sass)$/ },
|
||||
resourceQuery: { not: /url/ }, // exclude if *.svg?url
|
||||
loader: '@svgr/webpack',
|
||||
options: {
|
||||
dimensions: false,
|
||||
titleProp: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// Modify the file loader rule to ignore *.svg, since we have it handled now.
|
||||
fileLoaderRule.exclude = /\.svg$/i;
|
||||
|
||||
return config;
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
||||
67
package.json
Normal file
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"name": "ts-nextjs-tailwind-starter",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"lint:fix": "eslint src --fix && pnpm format",
|
||||
"lint:strict": "eslint --max-warnings=0 src",
|
||||
"typecheck": "tsc --noEmit --incremental false",
|
||||
"test:watch": "jest --watch",
|
||||
"test": "jest",
|
||||
"format": "prettier -w .",
|
||||
"format:check": "prettier -c .",
|
||||
"postbuild": "next-sitemap --config next-sitemap.config.js",
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"dependencies": {
|
||||
"clsx": "^2.0.0",
|
||||
"lucide-react": "^0.438.0",
|
||||
"next": "^14.2.23",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-icons": "^5.4.0",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^16.3.0",
|
||||
"@commitlint/config-conventional": "^16.2.4",
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"@tailwindcss/forms": "^0.5.10",
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^15.0.7",
|
||||
"@types/react": "^18.3.18",
|
||||
"@types/testing-library__jest-dom": "^5.14.9",
|
||||
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
||||
"@typescript-eslint/parser": "^5.62.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-config-next": "^14.2.23",
|
||||
"eslint-config-prettier": "^8.10.0",
|
||||
"eslint-plugin-simple-import-sort": "^7.0.0",
|
||||
"eslint-plugin-unused-imports": "^2.0.0",
|
||||
"husky": "^7.0.4",
|
||||
"jest": "^27.5.1",
|
||||
"lint-staged": "^12.5.0",
|
||||
"next-router-mock": "^0.9.0",
|
||||
"next-sitemap": "^2.5.28",
|
||||
"postcss": "^8.5.1",
|
||||
"prettier": "^2.8.8",
|
||||
"prettier-plugin-tailwindcss": "^0.5.0",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^4.9.5"
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/*.{js,jsx,ts,tsx}": [
|
||||
"eslint --max-warnings=0",
|
||||
"prettier -w"
|
||||
],
|
||||
"**/*.{json,css,scss,md,webmanifest}": [
|
||||
"prettier -w"
|
||||
]
|
||||
}
|
||||
}
|
||||
9267
pnpm-lock.yaml
generated
Normal file
6
postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
BIN
public/favicon.ico
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
public/favicon/android-chrome-192x192.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
public/favicon/android-chrome-512x512.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
public/favicon/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
public/favicon/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 937 B |
BIN
public/favicon/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
public/favicon/favicon.ico
Normal file
|
After Width: | Height: | Size: 11 KiB |
19
public/favicon/site.webmanifest
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "",
|
||||
"short_name": "",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/favicon/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/favicon/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
||||
BIN
public/fonts/inter-var-latin.woff2
Normal file
BIN
public/images/new-tab.png
Normal file
|
After Width: | Height: | Size: 565 B |
BIN
public/images/og.jpg
Normal file
|
After Width: | Height: | Size: 146 KiB |
1
public/svg/Logo.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#FFAC33" d="M32.938 15.651C32.792 15.26 32.418 15 32 15H19.925L26.89 1.458c.219-.426.106-.947-.271-1.243C26.437.071 26.218 0 26 0c-.233 0-.466.082-.653.243L18 6.588 3.347 19.243c-.316.273-.43.714-.284 1.105S3.582 21 4 21h12.075L9.11 34.542c-.219.426-.106.947.271 1.243.182.144.401.215.619.215.233 0 .466-.082.653-.243L18 29.412l14.653-12.655c.317-.273.43-.714.285-1.106z"/></svg>
|
||||
|
After Width: | Height: | Size: 451 B |
1
public/svg/Vercel.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Vercel</title><path d="M24 22.525H0l12-21.05 12 21.05z"/></svg>
|
||||
|
After Width: | Height: | Size: 141 B |
8
src/__mocks__/svg.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import React, { SVGProps } from 'react';
|
||||
|
||||
const SvgrMock = React.forwardRef<SVGSVGElement, SVGProps<SVGSVGElement>>(
|
||||
(props, ref) => <svg ref={ref} {...props} />
|
||||
);
|
||||
|
||||
export const ReactComponent = SvgrMock;
|
||||
export default SvgrMock;
|
||||
15
src/__tests__/pages/HomePage.test.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
// !STARTERCONF You should delete this page
|
||||
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import HomePage from '@/app/page';
|
||||
|
||||
describe('Homepage', () => {
|
||||
it('renders the Components', () => {
|
||||
render(<HomePage />);
|
||||
|
||||
const heading = screen.getByText(/A starter for Next.js/i);
|
||||
|
||||
expect(heading).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
5
src/app/api/hello/route.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
export async function GET() {
|
||||
return NextResponse.json({ hello: 'Next.js' });
|
||||
}
|
||||
17
src/app/components/layout.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Metadata } from 'next';
|
||||
import * as React from 'react';
|
||||
|
||||
import '@/styles/colors.css';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Components',
|
||||
description: 'Pre-built components with awesome default',
|
||||
};
|
||||
|
||||
export default function ComponentsLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
461
src/app/components/page.tsx
Normal file
@@ -0,0 +1,461 @@
|
||||
'use client';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import {
|
||||
ArrowRight,
|
||||
CreditCard,
|
||||
Laptop,
|
||||
Phone,
|
||||
Plus,
|
||||
Shield,
|
||||
} from 'lucide-react';
|
||||
import React from 'react';
|
||||
|
||||
import Button from '@/components/buttons/Button';
|
||||
import IconButton from '@/components/buttons/IconButton';
|
||||
import TextButton from '@/components/buttons/TextButton';
|
||||
import ArrowLink from '@/components/links/ArrowLink';
|
||||
import ButtonLink from '@/components/links/ButtonLink';
|
||||
import PrimaryLink from '@/components/links/PrimaryLink';
|
||||
import UnderlineLink from '@/components/links/UnderlineLink';
|
||||
import UnstyledLink from '@/components/links/UnstyledLink';
|
||||
import NextImage from '@/components/NextImage';
|
||||
import Skeleton from '@/components/Skeleton';
|
||||
|
||||
type Color = (typeof colorList)[number];
|
||||
|
||||
export default function ComponentPage() {
|
||||
const [mode, setMode] = React.useState<'dark' | 'light'>('light');
|
||||
const [color, setColor] = React.useState<Color>('sky');
|
||||
function toggleMode() {
|
||||
return mode === 'dark' ? setMode('light') : setMode('dark');
|
||||
}
|
||||
|
||||
const textColor = mode === 'dark' ? 'text-gray-300' : 'text-gray-600';
|
||||
|
||||
return (
|
||||
<main>
|
||||
<section
|
||||
className={clsx(mode === 'dark' ? 'bg-dark' : 'bg-white', color)}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
'layout min-h-screen py-20',
|
||||
mode === 'dark' ? 'text-white' : 'text-black'
|
||||
)}
|
||||
>
|
||||
<h1>Built-in Components</h1>
|
||||
<ArrowLink direction='left' className='mt-2' href='/'>
|
||||
Back to Home
|
||||
</ArrowLink>
|
||||
|
||||
<div className='mt-8 flex flex-wrap gap-2'>
|
||||
<Button
|
||||
onClick={toggleMode}
|
||||
variant={mode === 'dark' ? 'light' : 'dark'}
|
||||
>
|
||||
Set to {mode === 'dark' ? 'light' : 'dark'}
|
||||
</Button>
|
||||
{/* <Button onClick={randomize}>Randomize CSS Variable</Button> */}
|
||||
</div>
|
||||
|
||||
<ol className='mt-8 space-y-6'>
|
||||
<li className='space-y-2'>
|
||||
<h2 className='text-lg md:text-xl'>Customize Colors</h2>
|
||||
<p className={clsx('!mt-1 text-sm', textColor)}>
|
||||
You can change primary color to any Tailwind CSS colors. See
|
||||
globals.css to change your color.
|
||||
</p>
|
||||
<div className='flex flex-wrap gap-2'>
|
||||
<select
|
||||
name='color'
|
||||
id='color'
|
||||
value={color}
|
||||
className={clsx(
|
||||
'block max-w-xs rounded',
|
||||
mode === 'dark'
|
||||
? 'bg-dark border border-gray-600'
|
||||
: 'border-gray-300 bg-white',
|
||||
'focus:border-primary-400 focus:ring-primary-400 focus:outline-none focus:ring'
|
||||
)}
|
||||
onChange={(e) => setColor(e.target.value as Color)}
|
||||
>
|
||||
{colorList.map((c) => (
|
||||
<option key={c} value={c}>
|
||||
{c}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<ButtonLink href='https://github.com/theodorusclarence/ts-nextjs-tailwind-starter/blob/main/src/styles/colors.css'>
|
||||
Check list of colors
|
||||
</ButtonLink>
|
||||
</div>
|
||||
<div className='flex flex-wrap gap-2 text-xs font-medium'>
|
||||
<div className='bg-primary-50 flex h-10 w-10 items-center justify-center rounded text-black'>
|
||||
50
|
||||
</div>
|
||||
<div className='bg-primary-100 flex h-10 w-10 items-center justify-center rounded text-black'>
|
||||
100
|
||||
</div>
|
||||
<div className='bg-primary-200 flex h-10 w-10 items-center justify-center rounded text-black'>
|
||||
200
|
||||
</div>
|
||||
<div className='bg-primary-300 flex h-10 w-10 items-center justify-center rounded text-black'>
|
||||
300
|
||||
</div>
|
||||
<div className='bg-primary-400 flex h-10 w-10 items-center justify-center rounded text-black'>
|
||||
400
|
||||
</div>
|
||||
<div className='bg-primary-500 flex h-10 w-10 items-center justify-center rounded text-black'>
|
||||
500
|
||||
</div>
|
||||
<div className='bg-primary-600 flex h-10 w-10 items-center justify-center rounded text-white'>
|
||||
600
|
||||
</div>
|
||||
<div className='bg-primary-700 flex h-10 w-10 items-center justify-center rounded text-white'>
|
||||
700
|
||||
</div>
|
||||
<div className='bg-primary-800 flex h-10 w-10 items-center justify-center rounded text-white'>
|
||||
800
|
||||
</div>
|
||||
<div className='bg-primary-900 flex h-10 w-10 items-center justify-center rounded text-white'>
|
||||
900
|
||||
</div>
|
||||
<div className='bg-primary-950 flex h-10 w-10 items-center justify-center rounded text-white'>
|
||||
950
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li className='space-y-2'>
|
||||
<h2 className='text-lg md:text-xl'>UnstyledLink</h2>
|
||||
<p className={clsx('!mt-1 text-sm', textColor)}>
|
||||
No style applied, differentiate internal and outside links, give
|
||||
custom cursor for outside links.
|
||||
</p>
|
||||
<div className='space-x-2'>
|
||||
<UnstyledLink href='/'>Internal Links</UnstyledLink>
|
||||
<UnstyledLink href='https://theodorusclarence.com'>
|
||||
Outside Links
|
||||
</UnstyledLink>
|
||||
</div>
|
||||
</li>
|
||||
<li className='space-y-2'>
|
||||
<h2 className='text-lg md:text-xl'>PrimaryLink</h2>
|
||||
<p className={clsx('!mt-1 text-sm', textColor)}>
|
||||
Add styling on top of UnstyledLink, giving a primary color to
|
||||
the link.
|
||||
</p>
|
||||
<div className='space-x-2'>
|
||||
<PrimaryLink href='/'>Internal Links</PrimaryLink>
|
||||
<PrimaryLink href='https://theodorusclarence.com'>
|
||||
Outside Links
|
||||
</PrimaryLink>
|
||||
</div>
|
||||
</li>
|
||||
<li className='space-y-2'>
|
||||
<h2 className='text-lg md:text-xl'>UnderlineLink</h2>
|
||||
<p className={clsx('!mt-1 text-sm', textColor)}>
|
||||
Add styling on top of UnstyledLink, giving a dotted and animated
|
||||
underline.
|
||||
</p>
|
||||
<div className='space-x-2'>
|
||||
<UnderlineLink href='/'>Internal Links</UnderlineLink>
|
||||
<UnderlineLink href='https://theodorusclarence.com'>
|
||||
Outside Links
|
||||
</UnderlineLink>
|
||||
</div>
|
||||
</li>
|
||||
<li className='space-y-2'>
|
||||
<h2 className='text-lg md:text-xl'>ArrowLink</h2>
|
||||
<p className={clsx('!mt-1 text-sm', textColor)}>
|
||||
Useful for indicating navigation, I use this quite a lot, so why
|
||||
not build a component with some whimsy touch?
|
||||
</p>
|
||||
<div className='flex flex-wrap items-center gap-4'>
|
||||
<ArrowLink href='/' direction='left'>
|
||||
Direction Left
|
||||
</ArrowLink>
|
||||
<ArrowLink href='/'>Direction Right</ArrowLink>
|
||||
<ArrowLink
|
||||
as={UnstyledLink}
|
||||
className='inline-flex items-center'
|
||||
href='/'
|
||||
>
|
||||
Polymorphic
|
||||
</ArrowLink>
|
||||
<ArrowLink
|
||||
as={ButtonLink}
|
||||
variant='light'
|
||||
className='inline-flex items-center'
|
||||
href='/'
|
||||
>
|
||||
Polymorphic
|
||||
</ArrowLink>
|
||||
</div>
|
||||
</li>
|
||||
<li className='space-y-2'>
|
||||
<h2 className='text-lg md:text-xl'>ButtonLink</h2>
|
||||
<p className={clsx('!mt-1 text-sm', textColor)}>
|
||||
Button styled link with 3 variants.
|
||||
</p>
|
||||
<div className='flex flex-wrap gap-2'>
|
||||
<ButtonLink
|
||||
variant='primary'
|
||||
href='https://theodorusclarence.com'
|
||||
>
|
||||
Primary Variant
|
||||
</ButtonLink>
|
||||
<ButtonLink
|
||||
variant='outline'
|
||||
isDarkBg={mode === 'dark'}
|
||||
href='https://theodorusclarence.com'
|
||||
>
|
||||
Outline Variant
|
||||
</ButtonLink>
|
||||
<ButtonLink
|
||||
variant='ghost'
|
||||
isDarkBg={mode === 'dark'}
|
||||
href='https://theodorusclarence.com'
|
||||
>
|
||||
Ghost Variant
|
||||
</ButtonLink>
|
||||
<ButtonLink variant='dark' href='https://theodorusclarence.com'>
|
||||
Dark Variant
|
||||
</ButtonLink>
|
||||
<ButtonLink
|
||||
variant='light'
|
||||
href='https://theodorusclarence.com'
|
||||
>
|
||||
Light Variant
|
||||
</ButtonLink>
|
||||
</div>
|
||||
</li>
|
||||
<li className='space-y-2'>
|
||||
<h2 className='text-lg md:text-xl'>Button</h2>
|
||||
<p className={clsx('!mt-1 text-sm', textColor)}>
|
||||
Ordinary button with style.
|
||||
</p>
|
||||
<div className='flex flex-wrap gap-2'>
|
||||
<Button variant='primary'>Primary Variant</Button>
|
||||
<Button variant='outline' isDarkBg={mode === 'dark'}>
|
||||
Outline Variant
|
||||
</Button>
|
||||
<Button variant='ghost' isDarkBg={mode === 'dark'}>
|
||||
Ghost Variant
|
||||
</Button>
|
||||
<Button variant='dark'>Dark Variant</Button>
|
||||
<Button variant='light'>Light Variant</Button>
|
||||
</div>
|
||||
<div className='flex flex-wrap gap-2'>
|
||||
<Button
|
||||
variant='primary'
|
||||
leftIcon={Plus}
|
||||
rightIcon={ArrowRight}
|
||||
>
|
||||
Icon
|
||||
</Button>
|
||||
<Button
|
||||
variant='outline'
|
||||
leftIcon={Plus}
|
||||
rightIcon={ArrowRight}
|
||||
isDarkBg={mode === 'dark'}
|
||||
>
|
||||
Icon
|
||||
</Button>
|
||||
<Button
|
||||
variant='ghost'
|
||||
leftIcon={Plus}
|
||||
rightIcon={ArrowRight}
|
||||
isDarkBg={mode === 'dark'}
|
||||
>
|
||||
Icon
|
||||
</Button>
|
||||
<Button variant='dark' leftIcon={Plus} rightIcon={ArrowRight}>
|
||||
Icon
|
||||
</Button>
|
||||
<Button variant='light' leftIcon={Plus} rightIcon={ArrowRight}>
|
||||
Icon
|
||||
</Button>
|
||||
</div>
|
||||
<div className='!mt-4 flex flex-wrap gap-2'>
|
||||
<Button size='sm' variant='primary'>
|
||||
Small Size
|
||||
</Button>
|
||||
<Button size='sm' variant='outline' isDarkBg={mode === 'dark'}>
|
||||
Small Size
|
||||
</Button>
|
||||
<Button size='sm' variant='ghost' isDarkBg={mode === 'dark'}>
|
||||
Small Size
|
||||
</Button>
|
||||
<Button size='sm' variant='dark'>
|
||||
Small Size
|
||||
</Button>
|
||||
<Button size='sm' variant='light'>
|
||||
Small Size
|
||||
</Button>
|
||||
</div>
|
||||
<div className='flex flex-wrap gap-2'>
|
||||
<Button
|
||||
size='sm'
|
||||
variant='primary'
|
||||
leftIcon={Plus}
|
||||
rightIcon={ArrowRight}
|
||||
>
|
||||
Icon
|
||||
</Button>
|
||||
<Button
|
||||
size='sm'
|
||||
variant='outline'
|
||||
leftIcon={Plus}
|
||||
rightIcon={ArrowRight}
|
||||
isDarkBg={mode === 'dark'}
|
||||
>
|
||||
Icon
|
||||
</Button>
|
||||
<Button
|
||||
size='sm'
|
||||
variant='ghost'
|
||||
leftIcon={Plus}
|
||||
rightIcon={ArrowRight}
|
||||
isDarkBg={mode === 'dark'}
|
||||
>
|
||||
Icon
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size='sm'
|
||||
variant='dark'
|
||||
leftIcon={Plus}
|
||||
rightIcon={ArrowRight}
|
||||
>
|
||||
Icon
|
||||
</Button>
|
||||
<Button
|
||||
size='sm'
|
||||
variant='light'
|
||||
leftIcon={Plus}
|
||||
rightIcon={ArrowRight}
|
||||
>
|
||||
Icon
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className='!mt-4 flex flex-wrap gap-2'>
|
||||
<Button disabled variant='primary'>
|
||||
Disabled
|
||||
</Button>
|
||||
<Button disabled variant='outline' isDarkBg={mode === 'dark'}>
|
||||
Disabled
|
||||
</Button>
|
||||
<Button disabled variant='ghost' isDarkBg={mode === 'dark'}>
|
||||
Disabled
|
||||
</Button>
|
||||
<Button disabled variant='dark'>
|
||||
Disabled
|
||||
</Button>
|
||||
<Button disabled variant='light'>
|
||||
Disabled
|
||||
</Button>
|
||||
</div>
|
||||
<div className='flex flex-wrap gap-2'>
|
||||
<Button isLoading variant='primary'>
|
||||
Disabled
|
||||
</Button>
|
||||
<Button isLoading variant='outline' isDarkBg={mode === 'dark'}>
|
||||
Disabled
|
||||
</Button>
|
||||
<Button isLoading variant='ghost' isDarkBg={mode === 'dark'}>
|
||||
Disabled
|
||||
</Button>
|
||||
<Button isLoading variant='dark'>
|
||||
Disabled
|
||||
</Button>
|
||||
<Button isLoading variant='light'>
|
||||
Disabled
|
||||
</Button>
|
||||
</div>
|
||||
</li>
|
||||
<li className='space-y-2'>
|
||||
<h2 className='text-lg md:text-xl'>TextButton</h2>
|
||||
<p className={clsx('!mt-1 text-sm', textColor)}>
|
||||
Button with a text style
|
||||
</p>
|
||||
<div className='space-x-2'>
|
||||
<TextButton>Primary Variant</TextButton>
|
||||
<TextButton variant='basic'>Basic Variant</TextButton>
|
||||
</div>
|
||||
</li>
|
||||
<li className='space-y-2'>
|
||||
<h2 className='text-lg md:text-xl'>IconButton</h2>
|
||||
<p className={clsx('!mt-1 text-sm', textColor)}>
|
||||
Button with only icon inside
|
||||
</p>
|
||||
<div className='space-x-2'>
|
||||
<IconButton icon={Plus} />
|
||||
<IconButton variant='outline' icon={Laptop} />
|
||||
<IconButton variant='ghost' icon={Phone} />
|
||||
<IconButton variant='dark' icon={Shield} />
|
||||
<IconButton variant='light' icon={CreditCard} />
|
||||
</div>
|
||||
</li>
|
||||
<li className='space-y-2'>
|
||||
<h2 className='text-lg md:text-xl'>Custom 404 Page</h2>
|
||||
<p className={clsx('!mt-1 text-sm', textColor)}>
|
||||
Styled 404 page with some animation.
|
||||
</p>
|
||||
<div className='flex flex-wrap gap-2'>
|
||||
<ButtonLink href='/404'>Visit the 404 page</ButtonLink>
|
||||
</div>
|
||||
</li>
|
||||
<li className='space-y-2'>
|
||||
<h2 className='text-lg md:text-xl'>Next Image</h2>
|
||||
<p className={clsx('!mt-1 text-sm', textColor)}>
|
||||
Next Image with default props and skeleton animation
|
||||
</p>
|
||||
<NextImage
|
||||
useSkeleton
|
||||
className='w-32 md:w-40'
|
||||
src='/favicon/android-chrome-192x192.png'
|
||||
width='180'
|
||||
height='180'
|
||||
alt='Icon'
|
||||
/>
|
||||
</li>
|
||||
<li className='space-y-2'>
|
||||
<h2 className='text-lg md:text-xl'>Skeleton</h2>
|
||||
<p className={clsx('!mt-1 text-sm', textColor)}>
|
||||
Skeleton with shimmer effect
|
||||
</p>
|
||||
<Skeleton className='h-72 w-72' />
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
const colorList = [
|
||||
'slate',
|
||||
'gray',
|
||||
'zinc',
|
||||
'neutral',
|
||||
'stone',
|
||||
'red',
|
||||
'orange',
|
||||
'amber',
|
||||
'yellow',
|
||||
'lime',
|
||||
'green',
|
||||
'emerald',
|
||||
'teal',
|
||||
'cyan',
|
||||
'sky',
|
||||
'blue',
|
||||
'indigo',
|
||||
'violet',
|
||||
'purple',
|
||||
'fuchsia',
|
||||
'pink',
|
||||
'rose',
|
||||
] as const;
|
||||
38
src/app/error.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
'use client'; // Error components must be Client Components
|
||||
|
||||
import * as React from 'react';
|
||||
import { RiAlarmWarningFill } from 'react-icons/ri';
|
||||
|
||||
import TextButton from '@/components/buttons/TextButton';
|
||||
|
||||
export default function Error({
|
||||
error,
|
||||
reset,
|
||||
}: {
|
||||
error: Error & { digest?: string };
|
||||
reset: () => void;
|
||||
}) {
|
||||
React.useEffect(() => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<main>
|
||||
<section className='bg-white'>
|
||||
<div className='layout flex min-h-screen flex-col items-center justify-center text-center text-black'>
|
||||
<RiAlarmWarningFill
|
||||
size={60}
|
||||
className='drop-shadow-glow animate-flicker text-red-500'
|
||||
/>
|
||||
<h1 className='mt-8 text-4xl md:text-6xl'>
|
||||
Oops, something went wrong!
|
||||
</h1>
|
||||
<TextButton variant='basic' onClick={reset} className='mt-4'>
|
||||
Try again
|
||||
</TextButton>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
62
src/app/layout.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import { Metadata } from 'next';
|
||||
import * as React from 'react';
|
||||
|
||||
import '@/styles/globals.css';
|
||||
// !STARTERCONF This is for demo purposes, remove @/styles/colors.css import immediately
|
||||
import '@/styles/colors.css';
|
||||
|
||||
import { siteConfig } from '@/constant/config';
|
||||
|
||||
// !STARTERCONF Change these default meta
|
||||
// !STARTERCONF Look at @/constant/config to change them
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL(siteConfig.url),
|
||||
title: {
|
||||
default: siteConfig.title,
|
||||
template: `%s | ${siteConfig.title}`,
|
||||
},
|
||||
description: siteConfig.description,
|
||||
robots: { index: true, follow: true },
|
||||
// !STARTERCONF this is the default favicon, you can generate your own from https://realfavicongenerator.net/
|
||||
// ! copy to /favicon folder
|
||||
icons: {
|
||||
icon: '/favicon/favicon.ico',
|
||||
shortcut: '/favicon/favicon-16x16.png',
|
||||
apple: '/favicon/apple-touch-icon.png',
|
||||
},
|
||||
manifest: `/favicon/site.webmanifest`,
|
||||
openGraph: {
|
||||
url: siteConfig.url,
|
||||
title: siteConfig.title,
|
||||
description: siteConfig.description,
|
||||
siteName: siteConfig.title,
|
||||
images: [`${siteConfig.url}/images/og.jpg`],
|
||||
type: 'website',
|
||||
locale: 'en_US',
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
title: siteConfig.title,
|
||||
description: siteConfig.description,
|
||||
images: [`${siteConfig.url}/images/og.jpg`],
|
||||
// creator: '@th_clarence',
|
||||
},
|
||||
// authors: [
|
||||
// {
|
||||
// name: 'Theodorus Clarence',
|
||||
// url: 'https://theodorusclarence.com',
|
||||
// },
|
||||
// ],
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html>
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
24
src/app/not-found.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Metadata } from 'next';
|
||||
import * as React from 'react';
|
||||
import { RiAlarmWarningFill } from 'react-icons/ri';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Not Found',
|
||||
};
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<main>
|
||||
<section className='bg-white'>
|
||||
<div className='layout flex min-h-screen flex-col items-center justify-center text-center text-black'>
|
||||
<RiAlarmWarningFill
|
||||
size={60}
|
||||
className='drop-shadow-glow animate-flicker text-red-500'
|
||||
/>
|
||||
<h1 className='mt-8 text-4xl md:text-6xl'>Page Not Found</h1>
|
||||
<a href='/'>Back to home</a>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
72
src/app/page.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
'use client';
|
||||
|
||||
import Head from 'next/head';
|
||||
import * as React from 'react';
|
||||
import '@/lib/env';
|
||||
|
||||
import ArrowLink from '@/components/links/ArrowLink';
|
||||
import ButtonLink from '@/components/links/ButtonLink';
|
||||
import UnderlineLink from '@/components/links/UnderlineLink';
|
||||
import UnstyledLink from '@/components/links/UnstyledLink';
|
||||
|
||||
/**
|
||||
* SVGR Support
|
||||
* Caveat: No React Props Type.
|
||||
*
|
||||
* You can override the next-env if the type is important to you
|
||||
* @see https://stackoverflow.com/questions/68103844/how-to-override-next-js-svg-module-declaration
|
||||
*/
|
||||
import Logo from '~/svg/Logo.svg';
|
||||
|
||||
// !STARTERCONF -> Select !STARTERCONF and CMD + SHIFT + F
|
||||
// Before you begin editing, follow all comments with `STARTERCONF`,
|
||||
// to customize the default configuration.
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<main>
|
||||
<Head>
|
||||
<title>Hi</title>
|
||||
</Head>
|
||||
<section className='bg-white'>
|
||||
<div className='layout relative flex min-h-screen flex-col items-center justify-center py-12 text-center'>
|
||||
<Logo className='w-16' />
|
||||
<h1 className='mt-4'>Next.js + Tailwind CSS + TypeScript Starter</h1>
|
||||
<p className='mt-2 text-sm text-gray-800'>
|
||||
A starter for Next.js, Tailwind CSS, and TypeScript with Absolute
|
||||
Import, Seo, Link component, pre-configured with Husky{' '}
|
||||
</p>
|
||||
<p className='mt-2 text-sm text-gray-700'>
|
||||
<ArrowLink href='https://github.com/theodorusclarence/ts-nextjs-tailwind-starter'>
|
||||
See the repository
|
||||
</ArrowLink>
|
||||
</p>
|
||||
|
||||
<ButtonLink className='mt-6' href='/components' variant='light'>
|
||||
See all components
|
||||
</ButtonLink>
|
||||
|
||||
<UnstyledLink
|
||||
href='https://vercel.com/new/git/external?repository-url=https%3A%2F%2Fgithub.com%2Ftheodorusclarence%2Fts-nextjs-tailwind-starter'
|
||||
className='mt-4'
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
width='92'
|
||||
height='32'
|
||||
src='https://vercel.com/button'
|
||||
alt='Deploy with Vercel'
|
||||
/>
|
||||
</UnstyledLink>
|
||||
|
||||
<footer className='absolute bottom-2 text-gray-700'>
|
||||
© {new Date().getFullYear()} By{' '}
|
||||
<UnderlineLink href='https://theodorusclarence.com?ref=tsnextstarter'>
|
||||
Theodorus Clarence
|
||||
</UnderlineLink>
|
||||
</footer>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
58
src/components/NextImage.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import Image, { ImageProps } from 'next/image';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
type NextImageProps = {
|
||||
useSkeleton?: boolean;
|
||||
classNames?: {
|
||||
image?: string;
|
||||
blur?: string;
|
||||
};
|
||||
alt: string;
|
||||
} & (
|
||||
| { width: string | number; height: string | number }
|
||||
| { layout: 'fill'; width?: string | number; height?: string | number }
|
||||
) &
|
||||
ImageProps;
|
||||
|
||||
/**
|
||||
*
|
||||
* @description Must set width using `w-` className
|
||||
* @param useSkeleton add background with pulse animation, don't use it if image is transparent
|
||||
*/
|
||||
export default function NextImage({
|
||||
useSkeleton = false,
|
||||
src,
|
||||
width,
|
||||
height,
|
||||
alt,
|
||||
className,
|
||||
classNames,
|
||||
...rest
|
||||
}: NextImageProps) {
|
||||
const [status, setStatus] = React.useState(
|
||||
useSkeleton ? 'loading' : 'complete'
|
||||
);
|
||||
const widthIsSet = className?.includes('w-') ?? false;
|
||||
|
||||
return (
|
||||
<figure
|
||||
style={!widthIsSet ? { width: `${width}px` } : undefined}
|
||||
className={className}
|
||||
>
|
||||
<Image
|
||||
className={cn(
|
||||
classNames?.image,
|
||||
status === 'loading' && cn('animate-pulse', classNames?.blur)
|
||||
)}
|
||||
src={src}
|
||||
width={width}
|
||||
height={height}
|
||||
alt={alt}
|
||||
onLoadingComplete={() => setStatus('complete')}
|
||||
{...rest}
|
||||
/>
|
||||
</figure>
|
||||
);
|
||||
}
|
||||
20
src/components/Skeleton.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
type SkeletonProps = React.ComponentPropsWithoutRef<'div'>;
|
||||
|
||||
export default function Skeleton({ className, ...rest }: SkeletonProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn('animate-shimmer bg-[#f6f7f8]', className)}
|
||||
style={{
|
||||
backgroundImage:
|
||||
'linear-gradient(to right, #f6f7f8 0%, #edeef1 20%, #f6f7f8 40%, #f6f7f8 100%)',
|
||||
backgroundSize: '700px 100%',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
}}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
160
src/components/buttons/Button.tsx
Normal file
@@ -0,0 +1,160 @@
|
||||
import { LucideIcon } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
import { IconType } from 'react-icons';
|
||||
import { ImSpinner2 } from 'react-icons/im';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const ButtonVariant = ['primary', 'outline', 'ghost', 'light', 'dark'] as const;
|
||||
const ButtonSize = ['sm', 'base'] as const;
|
||||
|
||||
type ButtonProps = {
|
||||
isLoading?: boolean;
|
||||
isDarkBg?: boolean;
|
||||
variant?: (typeof ButtonVariant)[number];
|
||||
size?: (typeof ButtonSize)[number];
|
||||
leftIcon?: IconType | LucideIcon;
|
||||
rightIcon?: IconType | LucideIcon;
|
||||
classNames?: {
|
||||
leftIcon?: string;
|
||||
rightIcon?: string;
|
||||
};
|
||||
} & React.ComponentPropsWithRef<'button'>;
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
(
|
||||
{
|
||||
children,
|
||||
className,
|
||||
disabled: buttonDisabled,
|
||||
isLoading,
|
||||
variant = 'primary',
|
||||
size = 'base',
|
||||
isDarkBg = false,
|
||||
leftIcon: LeftIcon,
|
||||
rightIcon: RightIcon,
|
||||
classNames,
|
||||
...rest
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const disabled = isLoading || buttonDisabled;
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
type='button'
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
'inline-flex items-center rounded font-medium',
|
||||
'focus-visible:ring-primary-500 focus:outline-none focus-visible:ring',
|
||||
'shadow-sm',
|
||||
'transition-colors duration-75',
|
||||
//#region //*=========== Size ===========
|
||||
[
|
||||
size === 'base' && ['px-3 py-1.5', 'text-sm md:text-base'],
|
||||
size === 'sm' && ['px-2 py-1', 'text-xs md:text-sm'],
|
||||
],
|
||||
//#endregion //*======== Size ===========
|
||||
//#region //*=========== Variants ===========
|
||||
[
|
||||
variant === 'primary' && [
|
||||
'bg-primary-500 text-white',
|
||||
'border-primary-600 border',
|
||||
'hover:bg-primary-600 hover:text-white',
|
||||
'active:bg-primary-700',
|
||||
'disabled:bg-primary-700',
|
||||
],
|
||||
variant === 'outline' && [
|
||||
'text-primary-500',
|
||||
'border-primary-500 border',
|
||||
'hover:bg-primary-50 active:bg-primary-100 disabled:bg-primary-100',
|
||||
isDarkBg &&
|
||||
'hover:bg-gray-900 active:bg-gray-800 disabled:bg-gray-800',
|
||||
],
|
||||
variant === 'ghost' && [
|
||||
'text-primary-500',
|
||||
'shadow-none',
|
||||
'hover:bg-primary-50 active:bg-primary-100 disabled:bg-primary-100',
|
||||
isDarkBg &&
|
||||
'hover:bg-gray-900 active:bg-gray-800 disabled:bg-gray-800',
|
||||
],
|
||||
variant === 'light' && [
|
||||
'bg-white text-gray-700',
|
||||
'border border-gray-300',
|
||||
'hover:text-dark hover:bg-gray-100',
|
||||
'active:bg-white/80 disabled:bg-gray-200',
|
||||
],
|
||||
variant === 'dark' && [
|
||||
'bg-gray-900 text-white',
|
||||
'border border-gray-600',
|
||||
'hover:bg-gray-800 active:bg-gray-700 disabled:bg-gray-700',
|
||||
],
|
||||
],
|
||||
//#endregion //*======== Variants ===========
|
||||
'disabled:cursor-not-allowed',
|
||||
isLoading &&
|
||||
'relative text-transparent transition-none hover:text-transparent disabled:cursor-wait',
|
||||
className
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{isLoading && (
|
||||
<div
|
||||
className={cn(
|
||||
'absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2',
|
||||
{
|
||||
'text-white': ['primary', 'dark'].includes(variant),
|
||||
'text-black': ['light'].includes(variant),
|
||||
'text-primary-500': ['outline', 'ghost'].includes(variant),
|
||||
}
|
||||
)}
|
||||
>
|
||||
<ImSpinner2 className='animate-spin' />
|
||||
</div>
|
||||
)}
|
||||
{LeftIcon && (
|
||||
<div
|
||||
className={cn([
|
||||
size === 'base' && 'mr-1',
|
||||
size === 'sm' && 'mr-1.5',
|
||||
])}
|
||||
>
|
||||
<LeftIcon
|
||||
size='1em'
|
||||
className={cn(
|
||||
[
|
||||
size === 'base' && 'md:text-md text-md',
|
||||
size === 'sm' && 'md:text-md text-sm',
|
||||
],
|
||||
classNames?.leftIcon
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
{RightIcon && (
|
||||
<div
|
||||
className={cn([
|
||||
size === 'base' && 'ml-1',
|
||||
size === 'sm' && 'ml-1.5',
|
||||
])}
|
||||
>
|
||||
<RightIcon
|
||||
size='1em'
|
||||
className={cn(
|
||||
[
|
||||
size === 'base' && 'text-md md:text-md',
|
||||
size === 'sm' && 'md:text-md text-sm',
|
||||
],
|
||||
classNames?.rightIcon
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default Button;
|
||||
116
src/components/buttons/IconButton.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
import { LucideIcon } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
import { IconType } from 'react-icons';
|
||||
import { ImSpinner2 } from 'react-icons/im';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const IconButtonVariant = [
|
||||
'primary',
|
||||
'outline',
|
||||
'ghost',
|
||||
'light',
|
||||
'dark',
|
||||
] as const;
|
||||
|
||||
type IconButtonProps = {
|
||||
isLoading?: boolean;
|
||||
isDarkBg?: boolean;
|
||||
variant?: (typeof IconButtonVariant)[number];
|
||||
icon?: IconType | LucideIcon;
|
||||
classNames?: {
|
||||
icon?: string;
|
||||
};
|
||||
} & React.ComponentPropsWithRef<'button'>;
|
||||
|
||||
const IconButton = React.forwardRef<HTMLButtonElement, IconButtonProps>(
|
||||
(
|
||||
{
|
||||
className,
|
||||
disabled: buttonDisabled,
|
||||
isLoading,
|
||||
variant = 'primary',
|
||||
isDarkBg = false,
|
||||
icon: Icon,
|
||||
classNames,
|
||||
...rest
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const disabled = isLoading || buttonDisabled;
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
type='button'
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
'inline-flex items-center justify-center rounded font-medium',
|
||||
'focus-visible:ring-primary-500 focus:outline-none focus-visible:ring',
|
||||
'shadow-sm',
|
||||
'transition-colors duration-75',
|
||||
'min-h-[28px] min-w-[28px] p-1 md:min-h-[34px] md:min-w-[34px] md:p-2',
|
||||
//#region //*=========== Variants ===========
|
||||
[
|
||||
variant === 'primary' && [
|
||||
'bg-primary-500 text-white',
|
||||
'border-primary-600 border',
|
||||
'hover:bg-primary-600 hover:text-white',
|
||||
'active:bg-primary-700',
|
||||
'disabled:bg-primary-700',
|
||||
],
|
||||
variant === 'outline' && [
|
||||
'text-primary-500',
|
||||
'border-primary-500 border',
|
||||
'hover:bg-primary-50 active:bg-primary-100 disabled:bg-primary-100',
|
||||
isDarkBg &&
|
||||
'hover:bg-gray-900 active:bg-gray-800 disabled:bg-gray-800',
|
||||
],
|
||||
variant === 'ghost' && [
|
||||
'text-primary-500',
|
||||
'shadow-none',
|
||||
'hover:bg-primary-50 active:bg-primary-100 disabled:bg-primary-100',
|
||||
isDarkBg &&
|
||||
'hover:bg-gray-900 active:bg-gray-800 disabled:bg-gray-800',
|
||||
],
|
||||
variant === 'light' && [
|
||||
'bg-white text-gray-700',
|
||||
'border border-gray-300',
|
||||
'hover:text-dark hover:bg-gray-100',
|
||||
'active:bg-white/80 disabled:bg-gray-200',
|
||||
],
|
||||
variant === 'dark' && [
|
||||
'bg-gray-900 text-white',
|
||||
'border border-gray-600',
|
||||
'hover:bg-gray-800 active:bg-gray-700 disabled:bg-gray-700',
|
||||
],
|
||||
],
|
||||
//#endregion //*======== Variants ===========
|
||||
'disabled:cursor-not-allowed',
|
||||
isLoading &&
|
||||
'relative text-transparent transition-none hover:text-transparent disabled:cursor-wait',
|
||||
className
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{isLoading && (
|
||||
<div
|
||||
className={cn(
|
||||
'absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2',
|
||||
{
|
||||
'text-white': ['primary', 'dark'].includes(variant),
|
||||
'text-black': ['light'].includes(variant),
|
||||
'text-primary-500': ['outline', 'ghost'].includes(variant),
|
||||
}
|
||||
)}
|
||||
>
|
||||
<ImSpinner2 className='animate-spin' />
|
||||
</div>
|
||||
)}
|
||||
{Icon && <Icon size='1em' className={cn(classNames?.icon)} />}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default IconButton;
|
||||
52
src/components/buttons/TextButton.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const TextButtonVariant = ['primary', 'basic'] as const;
|
||||
|
||||
type TextButtonProps = {
|
||||
variant?: (typeof TextButtonVariant)[number];
|
||||
} & React.ComponentPropsWithRef<'button'>;
|
||||
|
||||
const TextButton = React.forwardRef<HTMLButtonElement, TextButtonProps>(
|
||||
(
|
||||
{
|
||||
children,
|
||||
className,
|
||||
variant = 'primary',
|
||||
disabled: buttonDisabled,
|
||||
...rest
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
type='button'
|
||||
disabled={buttonDisabled}
|
||||
className={cn(
|
||||
'button inline-flex items-center justify-center font-semibold',
|
||||
'focus-visible:ring-primary-500 focus:outline-none focus-visible:ring',
|
||||
'transition duration-100',
|
||||
//#region //*=========== Variant ===========
|
||||
variant === 'primary' && [
|
||||
'text-primary-500 hover:text-primary-600 active:text-primary-700',
|
||||
'disabled:text-primary-200',
|
||||
],
|
||||
variant === 'basic' && [
|
||||
'text-black hover:text-gray-600 active:text-gray-800',
|
||||
'disabled:text-gray-300',
|
||||
],
|
||||
//#endregion //*======== Variant ===========
|
||||
'disabled:cursor-not-allowed disabled:brightness-105 disabled:hover:underline',
|
||||
className
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default TextButton;
|
||||
64
src/components/links/ArrowLink.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
import UnderlineLink from '@/components/links/UnderlineLink';
|
||||
import { UnstyledLinkProps } from '@/components/links/UnstyledLink';
|
||||
|
||||
type ArrowLinkProps<C extends React.ElementType> = {
|
||||
as?: C;
|
||||
direction?: 'left' | 'right';
|
||||
} & UnstyledLinkProps &
|
||||
React.ComponentProps<C>;
|
||||
|
||||
export default function ArrowLink<C extends React.ElementType>({
|
||||
children,
|
||||
className,
|
||||
direction = 'right',
|
||||
as,
|
||||
...rest
|
||||
}: ArrowLinkProps<C>) {
|
||||
const Component = as || UnderlineLink;
|
||||
|
||||
return (
|
||||
<Component
|
||||
{...rest}
|
||||
className={cn(
|
||||
'group gap-[0.25em]',
|
||||
direction === 'left' && 'flex-row-reverse',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<span>{children}</span>
|
||||
<svg
|
||||
viewBox='0 0 16 16'
|
||||
height='1em'
|
||||
width='1em'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
className={cn(
|
||||
'relative',
|
||||
'transition-transform duration-200',
|
||||
direction === 'right' ? 'motion-safe:-translate-x-1' : 'rotate-180',
|
||||
'group-hover:translate-x-0'
|
||||
)}
|
||||
>
|
||||
<path
|
||||
fill='currentColor'
|
||||
d='M7.28033 3.21967C6.98744 2.92678 6.51256 2.92678 6.21967 3.21967C5.92678 3.51256 5.92678 3.98744 6.21967 4.28033L7.28033 3.21967ZM11 8L11.5303 8.53033C11.8232 8.23744 11.8232 7.76256 11.5303 7.46967L11 8ZM6.21967 11.7197C5.92678 12.0126 5.92678 12.4874 6.21967 12.7803C6.51256 13.0732 6.98744 13.0732 7.28033 12.7803L6.21967 11.7197ZM6.21967 4.28033L10.4697 8.53033L11.5303 7.46967L7.28033 3.21967L6.21967 4.28033ZM10.4697 7.46967L6.21967 11.7197L7.28033 12.7803L11.5303 8.53033L10.4697 7.46967Z'
|
||||
/>
|
||||
<path
|
||||
stroke='currentColor'
|
||||
d='M1.75 8H11'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
className={cn(
|
||||
'origin-left transition-all duration-200',
|
||||
'opacity-0 motion-safe:-translate-x-1',
|
||||
'group-hover:translate-x-0 group-hover:opacity-100'
|
||||
)}
|
||||
/>
|
||||
</svg>
|
||||
</Component>
|
||||
);
|
||||
}
|
||||
146
src/components/links/ButtonLink.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
import { LucideIcon } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
import { IconType } from 'react-icons';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
import UnstyledLink, {
|
||||
UnstyledLinkProps,
|
||||
} from '@/components/links/UnstyledLink';
|
||||
|
||||
const ButtonLinkVariant = [
|
||||
'primary',
|
||||
'outline',
|
||||
'ghost',
|
||||
'light',
|
||||
'dark',
|
||||
] as const;
|
||||
const ButtonLinkSize = ['sm', 'base'] as const;
|
||||
|
||||
type ButtonLinkProps = {
|
||||
isDarkBg?: boolean;
|
||||
variant?: (typeof ButtonLinkVariant)[number];
|
||||
size?: (typeof ButtonLinkSize)[number];
|
||||
leftIcon?: IconType | LucideIcon;
|
||||
rightIcon?: IconType | LucideIcon;
|
||||
classNames?: {
|
||||
leftIcon?: string;
|
||||
rightIcon?: string;
|
||||
};
|
||||
} & UnstyledLinkProps;
|
||||
|
||||
const ButtonLink = React.forwardRef<HTMLAnchorElement, ButtonLinkProps>(
|
||||
(
|
||||
{
|
||||
children,
|
||||
className,
|
||||
variant = 'primary',
|
||||
size = 'base',
|
||||
isDarkBg = false,
|
||||
leftIcon: LeftIcon,
|
||||
rightIcon: RightIcon,
|
||||
classNames,
|
||||
...rest
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
return (
|
||||
<UnstyledLink
|
||||
ref={ref}
|
||||
{...rest}
|
||||
className={cn(
|
||||
'inline-flex items-center rounded font-medium',
|
||||
'focus-visible:ring-primary-500 focus:outline-none focus-visible:ring',
|
||||
'shadow-sm',
|
||||
'transition-colors duration-75',
|
||||
//#region //*=========== Size ===========
|
||||
[
|
||||
size === 'base' && ['px-3 py-1.5', 'text-sm md:text-base'],
|
||||
size === 'sm' && ['px-2 py-1', 'text-xs md:text-sm'],
|
||||
],
|
||||
//#endregion //*======== Size ===========
|
||||
//#region //*=========== Variants ===========
|
||||
[
|
||||
variant === 'primary' && [
|
||||
'bg-primary-500 text-white',
|
||||
'border-primary-600 border',
|
||||
'hover:bg-primary-600 hover:text-white',
|
||||
'active:bg-primary-700',
|
||||
'disabled:bg-primary-700',
|
||||
],
|
||||
variant === 'outline' && [
|
||||
'text-primary-500',
|
||||
'border-primary-500 border',
|
||||
'hover:bg-primary-50 active:bg-primary-100 disabled:bg-primary-100',
|
||||
isDarkBg &&
|
||||
'hover:bg-gray-900 active:bg-gray-800 disabled:bg-gray-800',
|
||||
],
|
||||
variant === 'ghost' && [
|
||||
'text-primary-500',
|
||||
'shadow-none',
|
||||
'hover:bg-primary-50 active:bg-primary-100 disabled:bg-primary-100',
|
||||
isDarkBg &&
|
||||
'hover:bg-gray-900 active:bg-gray-800 disabled:bg-gray-800',
|
||||
],
|
||||
variant === 'light' && [
|
||||
'bg-white text-gray-700',
|
||||
'border border-gray-300',
|
||||
'hover:text-dark hover:bg-gray-100',
|
||||
'active:bg-white/80 disabled:bg-gray-200',
|
||||
],
|
||||
variant === 'dark' && [
|
||||
'bg-gray-900 text-white',
|
||||
'border border-gray-600',
|
||||
'hover:bg-gray-800 active:bg-gray-700 disabled:bg-gray-700',
|
||||
],
|
||||
],
|
||||
//#endregion //*======== Variants ===========
|
||||
'disabled:cursor-not-allowed',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{LeftIcon && (
|
||||
<div
|
||||
className={cn([
|
||||
size === 'base' && 'mr-1',
|
||||
size === 'sm' && 'mr-1.5',
|
||||
])}
|
||||
>
|
||||
<LeftIcon
|
||||
size='1em'
|
||||
className={cn(
|
||||
[
|
||||
size === 'base' && 'md:text-md text-md',
|
||||
size === 'sm' && 'md:text-md text-sm',
|
||||
],
|
||||
classNames?.leftIcon
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
{RightIcon && (
|
||||
<div
|
||||
className={cn([
|
||||
size === 'base' && 'ml-1',
|
||||
size === 'sm' && 'ml-1.5',
|
||||
])}
|
||||
>
|
||||
<RightIcon
|
||||
size='1em'
|
||||
className={cn(
|
||||
[
|
||||
size === 'base' && 'text-md md:text-md',
|
||||
size === 'sm' && 'md:text-md text-sm',
|
||||
],
|
||||
classNames?.rightIcon
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</UnstyledLink>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default ButtonLink;
|
||||
97
src/components/links/IconLink.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import { LucideIcon } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
import { IconType } from 'react-icons';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
import UnstyledLink, {
|
||||
UnstyledLinkProps,
|
||||
} from '@/components/links/UnstyledLink';
|
||||
|
||||
const IconLinkVariant = [
|
||||
'primary',
|
||||
'outline',
|
||||
'ghost',
|
||||
'light',
|
||||
'dark',
|
||||
] as const;
|
||||
|
||||
type IconLinkProps = {
|
||||
isDarkBg?: boolean;
|
||||
variant?: (typeof IconLinkVariant)[number];
|
||||
icon?: IconType | LucideIcon;
|
||||
classNames?: {
|
||||
icon?: string;
|
||||
};
|
||||
} & Omit<UnstyledLinkProps, 'children'>;
|
||||
|
||||
const IconLink = React.forwardRef<HTMLAnchorElement, IconLinkProps>(
|
||||
(
|
||||
{
|
||||
className,
|
||||
icon: Icon,
|
||||
variant = 'outline',
|
||||
isDarkBg = false,
|
||||
classNames,
|
||||
...rest
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
return (
|
||||
<UnstyledLink
|
||||
ref={ref}
|
||||
type='button'
|
||||
className={cn(
|
||||
'inline-flex items-center justify-center rounded font-medium',
|
||||
'focus-visible:ring-primary-500 focus:outline-none focus-visible:ring',
|
||||
'shadow-sm',
|
||||
'transition-colors duration-75',
|
||||
'min-h-[28px] min-w-[28px] p-1 md:min-h-[34px] md:min-w-[34px] md:p-2',
|
||||
//#region //*=========== Variants ===========
|
||||
[
|
||||
variant === 'primary' && [
|
||||
'bg-primary-500 text-white',
|
||||
'border-primary-600 border',
|
||||
'hover:bg-primary-600 hover:text-white',
|
||||
'active:bg-primary-700',
|
||||
'disabled:bg-primary-700',
|
||||
],
|
||||
variant === 'outline' && [
|
||||
'text-primary-500',
|
||||
'border-primary-500 border',
|
||||
'hover:bg-primary-50 active:bg-primary-100 disabled:bg-primary-100',
|
||||
isDarkBg &&
|
||||
'hover:bg-gray-900 active:bg-gray-800 disabled:bg-gray-800',
|
||||
],
|
||||
variant === 'ghost' && [
|
||||
'text-primary-500',
|
||||
'shadow-none',
|
||||
'hover:bg-primary-50 active:bg-primary-100 disabled:bg-primary-100',
|
||||
isDarkBg &&
|
||||
'hover:bg-gray-900 active:bg-gray-800 disabled:bg-gray-800',
|
||||
],
|
||||
variant === 'light' && [
|
||||
'bg-white text-gray-700',
|
||||
'border border-gray-300',
|
||||
'hover:text-dark hover:bg-gray-100',
|
||||
'active:bg-white/80 disabled:bg-gray-200',
|
||||
],
|
||||
variant === 'dark' && [
|
||||
'bg-gray-900 text-white',
|
||||
'border border-gray-600',
|
||||
'hover:bg-gray-800 active:bg-gray-700 disabled:bg-gray-700',
|
||||
],
|
||||
],
|
||||
//#endregion //*======== Variants ===========
|
||||
'disabled:cursor-not-allowed',
|
||||
className
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{Icon && <Icon size='1em' className={cn(classNames?.icon)} />}
|
||||
</UnstyledLink>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default IconLink;
|
||||
43
src/components/links/PrimaryLink.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
import UnstyledLink, {
|
||||
UnstyledLinkProps,
|
||||
} from '@/components/links/UnstyledLink';
|
||||
|
||||
const PrimaryLinkVariant = ['primary', 'basic'] as const;
|
||||
type PrimaryLinkProps = {
|
||||
variant?: (typeof PrimaryLinkVariant)[number];
|
||||
} & UnstyledLinkProps;
|
||||
|
||||
const PrimaryLink = React.forwardRef<HTMLAnchorElement, PrimaryLinkProps>(
|
||||
({ className, children, variant = 'primary', ...rest }, ref) => {
|
||||
return (
|
||||
<UnstyledLink
|
||||
ref={ref}
|
||||
{...rest}
|
||||
className={cn(
|
||||
'inline-flex items-center',
|
||||
'focus-visible:ring-primary-500 focus:outline-none focus-visible:rounded focus-visible:ring focus-visible:ring-offset-2',
|
||||
'font-medium',
|
||||
//#region //*=========== Variant ===========
|
||||
variant === 'primary' && [
|
||||
'text-primary-500 hover:text-primary-600 active:text-primary-700',
|
||||
'disabled:text-primary-200',
|
||||
],
|
||||
variant === 'basic' && [
|
||||
'text-black hover:text-gray-600 active:text-gray-800',
|
||||
'disabled:text-gray-300',
|
||||
],
|
||||
//#endregion //*======== Variant ===========
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</UnstyledLink>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default PrimaryLink;
|
||||
28
src/components/links/UnderlineLink.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
import UnstyledLink, {
|
||||
UnstyledLinkProps,
|
||||
} from '@/components/links/UnstyledLink';
|
||||
|
||||
const UnderlineLink = React.forwardRef<HTMLAnchorElement, UnstyledLinkProps>(
|
||||
({ children, className, ...rest }, ref) => {
|
||||
return (
|
||||
<UnstyledLink
|
||||
ref={ref}
|
||||
{...rest}
|
||||
className={cn(
|
||||
'animated-underline custom-link inline-flex items-center font-medium',
|
||||
'focus-visible:ring-primary-500 focus:outline-none focus-visible:rounded focus-visible:ring focus-visible:ring-offset-2',
|
||||
'border-dark border-b border-dotted hover:border-black/0',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</UnstyledLink>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default UnderlineLink;
|
||||
50
src/components/links/UnstyledLink.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import Link, { LinkProps } from 'next/link';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export type UnstyledLinkProps = {
|
||||
href: string;
|
||||
children: React.ReactNode;
|
||||
openNewTab?: boolean;
|
||||
className?: string;
|
||||
nextLinkProps?: Omit<LinkProps, 'href'>;
|
||||
} & React.ComponentPropsWithRef<'a'>;
|
||||
|
||||
const UnstyledLink = React.forwardRef<HTMLAnchorElement, UnstyledLinkProps>(
|
||||
({ children, href, openNewTab, className, nextLinkProps, ...rest }, ref) => {
|
||||
const isNewTab =
|
||||
openNewTab !== undefined
|
||||
? openNewTab
|
||||
: href && !href.startsWith('/') && !href.startsWith('#');
|
||||
|
||||
if (!isNewTab) {
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
ref={ref}
|
||||
className={className}
|
||||
{...rest}
|
||||
{...nextLinkProps}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
ref={ref}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
href={href}
|
||||
{...rest}
|
||||
className={cn('cursor-newtab', className)}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default UnstyledLink;
|
||||
7
src/constant/config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export const siteConfig = {
|
||||
title: 'Next.js + Tailwind CSS + TypeScript Starter',
|
||||
description:
|
||||
'A starter for Next.js, Tailwind CSS, and TypeScript with Absolute Import, Seo, Link component, pre-configured with Husky',
|
||||
/** Without additional '/' on the end, e.g. https://theodorusclarence.com */
|
||||
url: 'https://tsnext-tw.thcl.dev',
|
||||
};
|
||||
6
src/constant/env.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export const isProd = process.env.NODE_ENV === 'production';
|
||||
export const isLocal = process.env.NODE_ENV === 'development';
|
||||
|
||||
export const showLogger = isLocal
|
||||
? true
|
||||
: process.env.NEXT_PUBLIC_SHOW_LOGGER === 'true' ?? false;
|
||||
20
src/lib/__tests__/og.test.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { openGraph } from '@/lib/og';
|
||||
|
||||
describe('Open Graph function should work correctly', () => {
|
||||
it('should not return templateTitle when not specified', () => {
|
||||
const result = openGraph({
|
||||
description: 'Test description',
|
||||
siteName: 'Test site name',
|
||||
});
|
||||
expect(result).not.toContain('&templateTitle=');
|
||||
});
|
||||
|
||||
it('should return templateTitle when specified', () => {
|
||||
const result = openGraph({
|
||||
templateTitle: 'Test Template Title',
|
||||
description: 'Test description',
|
||||
siteName: 'Test site name',
|
||||
});
|
||||
expect(result).toContain('&templateTitle=Test%20Template%20Title');
|
||||
});
|
||||
});
|
||||
20
src/lib/env.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/* eslint-disable @typescript-eslint/no-namespace */
|
||||
/**
|
||||
* Configuration for type-safe environment variables.
|
||||
* Imported through src/app/page.tsx
|
||||
* @see https://x.com/mattpocockuk/status/1760991147793449396
|
||||
*/
|
||||
import { z } from 'zod';
|
||||
|
||||
const envVariables = z.object({
|
||||
NEXT_PUBLIC_SHOW_LOGGER: z.enum(['true', 'false']).optional(),
|
||||
});
|
||||
|
||||
envVariables.parse(process.env);
|
||||
|
||||
declare global {
|
||||
namespace NodeJS {
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
interface ProcessEnv extends z.infer<typeof envVariables> {}
|
||||
}
|
||||
}
|
||||
13
src/lib/helper.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export function getFromLocalStorage(key: string): string | null {
|
||||
if (typeof window !== 'undefined') {
|
||||
return window.localStorage.getItem(key);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getFromSessionStorage(key: string): string | null {
|
||||
if (typeof sessionStorage !== 'undefined') {
|
||||
return sessionStorage.getItem(key);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
19
src/lib/logger.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/* eslint-disable no-console */
|
||||
import { showLogger } from '@/constant/env';
|
||||
|
||||
/**
|
||||
* A logger function that will only logs on development
|
||||
* @param object - The object to log
|
||||
* @param comment - Autogenerated with `lg` snippet
|
||||
*/
|
||||
export default function logger(object: unknown, comment?: string): void {
|
||||
if (!showLogger) return;
|
||||
|
||||
console.log(
|
||||
'%c ============== INFO LOG \n',
|
||||
'color: #22D3EE',
|
||||
`${typeof window !== 'undefined' && window?.location.pathname}\n`,
|
||||
`=== ${comment ?? ''}\n`,
|
||||
object
|
||||
);
|
||||
}
|
||||
27
src/lib/og.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
type OpenGraphType = {
|
||||
siteName: string;
|
||||
description: string;
|
||||
templateTitle?: string;
|
||||
logo?: string;
|
||||
};
|
||||
// !STARTERCONF This OG is generated from https://github.com/theodorusclarence/og
|
||||
// Please clone them and self-host if your site is going to be visited by many people.
|
||||
// Then change the url and the default logo.
|
||||
export function openGraph({
|
||||
siteName,
|
||||
templateTitle,
|
||||
description,
|
||||
// !STARTERCONF Or, you can use my server with your own logo.
|
||||
logo = 'https://og.<your-domain>/images/logo.jpg',
|
||||
}: OpenGraphType): string {
|
||||
const ogLogo = encodeURIComponent(logo);
|
||||
const ogSiteName = encodeURIComponent(siteName.trim());
|
||||
const ogTemplateTitle = templateTitle
|
||||
? encodeURIComponent(templateTitle.trim())
|
||||
: undefined;
|
||||
const ogDesc = encodeURIComponent(description.trim());
|
||||
|
||||
return `https://og.<your-domain>/api/general?siteName=${ogSiteName}&description=${ogDesc}&logo=${ogLogo}${
|
||||
ogTemplateTitle ? `&templateTitle=${ogTemplateTitle}` : ''
|
||||
}`;
|
||||
}
|
||||
7
src/lib/utils.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import clsx, { ClassValue } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
/** Merge classes with tailwind-merge with clsx full feature */
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
550
src/styles/colors.css
Normal file
@@ -0,0 +1,550 @@
|
||||
/* //!STARTERCONF Remove this file after copying your desired color, this is a large file you should remove it. */
|
||||
|
||||
.slate {
|
||||
--tw-color-primary-50: 248 250 252;
|
||||
--tw-color-primary-100: 241 245 249;
|
||||
--tw-color-primary-200: 226 232 240;
|
||||
--tw-color-primary-300: 203 213 225;
|
||||
--tw-color-primary-400: 148 163 184;
|
||||
--tw-color-primary-500: 100 116 139;
|
||||
--tw-color-primary-600: 71 85 105;
|
||||
--tw-color-primary-700: 51 65 85;
|
||||
--tw-color-primary-800: 30 41 59;
|
||||
--tw-color-primary-900: 15 23 42;
|
||||
--tw-color-primary-950: 2 6 23;
|
||||
--color-primary-50: rgb(var(--tw-color-primary-50)); /* #f8fafc */
|
||||
--color-primary-100: rgb(var(--tw-color-primary-100)); /* #f1f5f9 */
|
||||
--color-primary-200: rgb(var(--tw-color-primary-200)); /* #e2e8f0 */
|
||||
--color-primary-300: rgb(var(--tw-color-primary-300)); /* #cbd5e1 */
|
||||
--color-primary-400: rgb(var(--tw-color-primary-400)); /* #94a3b8 */
|
||||
--color-primary-500: rgb(var(--tw-color-primary-500)); /* #64748b */
|
||||
--color-primary-600: rgb(var(--tw-color-primary-600)); /* #475569 */
|
||||
--color-primary-700: rgb(var(--tw-color-primary-700)); /* #334155 */
|
||||
--color-primary-800: rgb(var(--tw-color-primary-800)); /* #1e293b */
|
||||
--color-primary-900: rgb(var(--tw-color-primary-900)); /* #0f172a */
|
||||
--color-primary-950: rgb(var(--tw-color-primary-950)); /* #020617 */
|
||||
}
|
||||
|
||||
.gray {
|
||||
--tw-color-primary-50: 249 250 251;
|
||||
--tw-color-primary-100: 243 244 246;
|
||||
--tw-color-primary-200: 229 231 235;
|
||||
--tw-color-primary-300: 209 213 219;
|
||||
--tw-color-primary-400: 156 163 175;
|
||||
--tw-color-primary-500: 107 114 128;
|
||||
--tw-color-primary-600: 75 85 99;
|
||||
--tw-color-primary-700: 55 65 81;
|
||||
--tw-color-primary-800: 31 41 55;
|
||||
--tw-color-primary-900: 17 24 39;
|
||||
--tw-color-primary-950: 3 7 18;
|
||||
--color-primary-50: rgb(var(--tw-color-primary-50)); /* #f9fafb */
|
||||
--color-primary-100: rgb(var(--tw-color-primary-100)); /* #f3f4f6 */
|
||||
--color-primary-200: rgb(var(--tw-color-primary-200)); /* #e5e7eb */
|
||||
--color-primary-300: rgb(var(--tw-color-primary-300)); /* #d1d5db */
|
||||
--color-primary-400: rgb(var(--tw-color-primary-400)); /* #9ca3af */
|
||||
--color-primary-500: rgb(var(--tw-color-primary-500)); /* #6b7280 */
|
||||
--color-primary-600: rgb(var(--tw-color-primary-600)); /* #4b5563 */
|
||||
--color-primary-700: rgb(var(--tw-color-primary-700)); /* #374151 */
|
||||
--color-primary-800: rgb(var(--tw-color-primary-800)); /* #1f2937 */
|
||||
--color-primary-900: rgb(var(--tw-color-primary-900)); /* #111827 */
|
||||
--color-primary-950: rgb(var(--tw-color-primary-950)); /* #030712 */
|
||||
}
|
||||
|
||||
.zinc {
|
||||
--tw-color-primary-50: 250 250 250;
|
||||
--tw-color-primary-100: 244 244 245;
|
||||
--tw-color-primary-200: 228 228 231;
|
||||
--tw-color-primary-300: 212 212 216;
|
||||
--tw-color-primary-400: 161 161 170;
|
||||
--tw-color-primary-500: 113 113 122;
|
||||
--tw-color-primary-600: 82 82 91;
|
||||
--tw-color-primary-700: 63 63 70;
|
||||
--tw-color-primary-800: 39 39 42;
|
||||
--tw-color-primary-900: 24 24 27;
|
||||
--tw-color-primary-950: 9 9 11;
|
||||
--color-primary-50: rgb(var(--tw-color-primary-50)); /* #fafafa */
|
||||
--color-primary-100: rgb(var(--tw-color-primary-100)); /* #f4f4f5 */
|
||||
--color-primary-200: rgb(var(--tw-color-primary-200)); /* #e4e4e7 */
|
||||
--color-primary-300: rgb(var(--tw-color-primary-300)); /* #d4d4d8 */
|
||||
--color-primary-400: rgb(var(--tw-color-primary-400)); /* #a1a1aa */
|
||||
--color-primary-500: rgb(var(--tw-color-primary-500)); /* #71717a */
|
||||
--color-primary-600: rgb(var(--tw-color-primary-600)); /* #52525b */
|
||||
--color-primary-700: rgb(var(--tw-color-primary-700)); /* #3f3f46 */
|
||||
--color-primary-800: rgb(var(--tw-color-primary-800)); /* #27272a */
|
||||
--color-primary-900: rgb(var(--tw-color-primary-900)); /* #18181b */
|
||||
--color-primary-950: rgb(var(--tw-color-primary-950)); /* #09090b */
|
||||
}
|
||||
|
||||
.neutral {
|
||||
--tw-color-primary-50: 250 250 250;
|
||||
--tw-color-primary-100: 245 245 245;
|
||||
--tw-color-primary-200: 229 229 229;
|
||||
--tw-color-primary-300: 212 212 212;
|
||||
--tw-color-primary-400: 163 163 163;
|
||||
--tw-color-primary-500: 115 115 115;
|
||||
--tw-color-primary-600: 82 82 82;
|
||||
--tw-color-primary-700: 64 64 64;
|
||||
--tw-color-primary-800: 38 38 38;
|
||||
--tw-color-primary-900: 23 23 23;
|
||||
--tw-color-primary-950: 10 10 10;
|
||||
--color-primary-50: rgb(var(--tw-color-primary-50)); /* #fafafa */
|
||||
--color-primary-100: rgb(var(--tw-color-primary-100)); /* #f5f5f5 */
|
||||
--color-primary-200: rgb(var(--tw-color-primary-200)); /* #e5e5e5 */
|
||||
--color-primary-300: rgb(var(--tw-color-primary-300)); /* #d4d4d4 */
|
||||
--color-primary-400: rgb(var(--tw-color-primary-400)); /* #a3a3a3 */
|
||||
--color-primary-500: rgb(var(--tw-color-primary-500)); /* #737373 */
|
||||
--color-primary-600: rgb(var(--tw-color-primary-600)); /* #525252 */
|
||||
--color-primary-700: rgb(var(--tw-color-primary-700)); /* #404040 */
|
||||
--color-primary-800: rgb(var(--tw-color-primary-800)); /* #262626 */
|
||||
--color-primary-900: rgb(var(--tw-color-primary-900)); /* #171717 */
|
||||
--color-primary-950: rgb(var(--tw-color-primary-950)); /* #0a0a0a */
|
||||
}
|
||||
|
||||
.stone {
|
||||
--tw-color-primary-50: 250 250 249;
|
||||
--tw-color-primary-100: 245 245 244;
|
||||
--tw-color-primary-200: 231 229 228;
|
||||
--tw-color-primary-300: 214 211 209;
|
||||
--tw-color-primary-400: 168 162 158;
|
||||
--tw-color-primary-500: 120 113 108;
|
||||
--tw-color-primary-600: 87 83 78;
|
||||
--tw-color-primary-700: 68 64 60;
|
||||
--tw-color-primary-800: 41 37 36;
|
||||
--tw-color-primary-900: 28 25 23;
|
||||
--tw-color-primary-950: 12 10 9;
|
||||
--color-primary-50: rgb(var(--tw-color-primary-50)); /* #fafaf9 */
|
||||
--color-primary-100: rgb(var(--tw-color-primary-100)); /* #f5f5f4 */
|
||||
--color-primary-200: rgb(var(--tw-color-primary-200)); /* #e7e5e4 */
|
||||
--color-primary-300: rgb(var(--tw-color-primary-300)); /* #d6d3d1 */
|
||||
--color-primary-400: rgb(var(--tw-color-primary-400)); /* #a8a29e */
|
||||
--color-primary-500: rgb(var(--tw-color-primary-500)); /* #78716c */
|
||||
--color-primary-600: rgb(var(--tw-color-primary-600)); /* #57534e */
|
||||
--color-primary-700: rgb(var(--tw-color-primary-700)); /* #44403c */
|
||||
--color-primary-800: rgb(var(--tw-color-primary-800)); /* #292524 */
|
||||
--color-primary-900: rgb(var(--tw-color-primary-900)); /* #1c1917 */
|
||||
--color-primary-950: rgb(var(--tw-color-primary-950)); /* #0c0a09 */
|
||||
}
|
||||
|
||||
.red {
|
||||
--tw-color-primary-50: 254 242 242;
|
||||
--tw-color-primary-100: 254 226 226;
|
||||
--tw-color-primary-200: 254 202 202;
|
||||
--tw-color-primary-300: 252 165 165;
|
||||
--tw-color-primary-400: 248 113 113;
|
||||
--tw-color-primary-500: 239 68 68;
|
||||
--tw-color-primary-600: 220 38 38;
|
||||
--tw-color-primary-700: 185 28 28;
|
||||
--tw-color-primary-800: 153 27 27;
|
||||
--tw-color-primary-900: 127 29 29;
|
||||
--tw-color-primary-950: 69 10 10;
|
||||
--color-primary-50: rgb(var(--tw-color-primary-50)); /* #fef2f2 */
|
||||
--color-primary-100: rgb(var(--tw-color-primary-100)); /* #fee2e2 */
|
||||
--color-primary-200: rgb(var(--tw-color-primary-200)); /* #fecaca */
|
||||
--color-primary-300: rgb(var(--tw-color-primary-300)); /* #fca5a5 */
|
||||
--color-primary-400: rgb(var(--tw-color-primary-400)); /* #f87171 */
|
||||
--color-primary-500: rgb(var(--tw-color-primary-500)); /* #ef4444 */
|
||||
--color-primary-600: rgb(var(--tw-color-primary-600)); /* #dc2626 */
|
||||
--color-primary-700: rgb(var(--tw-color-primary-700)); /* #b91c1c */
|
||||
--color-primary-800: rgb(var(--tw-color-primary-800)); /* #991b1b */
|
||||
--color-primary-900: rgb(var(--tw-color-primary-900)); /* #7f1d1d */
|
||||
--color-primary-950: rgb(var(--tw-color-primary-950)); /* #450a0a */
|
||||
}
|
||||
|
||||
.orange {
|
||||
--tw-color-primary-50: 255 247 237;
|
||||
--tw-color-primary-100: 255 237 213;
|
||||
--tw-color-primary-200: 254 215 170;
|
||||
--tw-color-primary-300: 253 186 116;
|
||||
--tw-color-primary-400: 251 146 60;
|
||||
--tw-color-primary-500: 249 115 22;
|
||||
--tw-color-primary-600: 234 88 12;
|
||||
--tw-color-primary-700: 194 65 12;
|
||||
--tw-color-primary-800: 154 52 18;
|
||||
--tw-color-primary-900: 124 45 18;
|
||||
--tw-color-primary-950: 67 20 7;
|
||||
--color-primary-50: rgb(var(--tw-color-primary-50)); /* #fff7ed */
|
||||
--color-primary-100: rgb(var(--tw-color-primary-100)); /* #ffedd5 */
|
||||
--color-primary-200: rgb(var(--tw-color-primary-200)); /* #fed7aa */
|
||||
--color-primary-300: rgb(var(--tw-color-primary-300)); /* #fdba74 */
|
||||
--color-primary-400: rgb(var(--tw-color-primary-400)); /* #fb923c */
|
||||
--color-primary-500: rgb(var(--tw-color-primary-500)); /* #f97316 */
|
||||
--color-primary-600: rgb(var(--tw-color-primary-600)); /* #ea580c */
|
||||
--color-primary-700: rgb(var(--tw-color-primary-700)); /* #c2410c */
|
||||
--color-primary-800: rgb(var(--tw-color-primary-800)); /* #9a3412 */
|
||||
--color-primary-900: rgb(var(--tw-color-primary-900)); /* #7c2d12 */
|
||||
--color-primary-950: rgb(var(--tw-color-primary-950)); /* #431407 */
|
||||
}
|
||||
|
||||
.amber {
|
||||
--tw-color-primary-50: 255 251 235;
|
||||
--tw-color-primary-100: 254 243 199;
|
||||
--tw-color-primary-200: 253 230 138;
|
||||
--tw-color-primary-300: 252 211 77;
|
||||
--tw-color-primary-400: 251 191 36;
|
||||
--tw-color-primary-500: 245 158 11;
|
||||
--tw-color-primary-600: 217 119 6;
|
||||
--tw-color-primary-700: 180 83 9;
|
||||
--tw-color-primary-800: 146 64 14;
|
||||
--tw-color-primary-900: 120 53 15;
|
||||
--tw-color-primary-950: 69 26 3;
|
||||
--color-primary-50: rgb(var(--tw-color-primary-50)); /* #fffbeb */
|
||||
--color-primary-100: rgb(var(--tw-color-primary-100)); /* #fef3c7 */
|
||||
--color-primary-200: rgb(var(--tw-color-primary-200)); /* #fde68a */
|
||||
--color-primary-300: rgb(var(--tw-color-primary-300)); /* #fcd34d */
|
||||
--color-primary-400: rgb(var(--tw-color-primary-400)); /* #fbbf24 */
|
||||
--color-primary-500: rgb(var(--tw-color-primary-500)); /* #f59e0b */
|
||||
--color-primary-600: rgb(var(--tw-color-primary-600)); /* #d97706 */
|
||||
--color-primary-700: rgb(var(--tw-color-primary-700)); /* #b45309 */
|
||||
--color-primary-800: rgb(var(--tw-color-primary-800)); /* #92400e */
|
||||
--color-primary-900: rgb(var(--tw-color-primary-900)); /* #78350f */
|
||||
--color-primary-950: rgb(var(--tw-color-primary-950)); /* #451a03 */
|
||||
}
|
||||
|
||||
.yellow {
|
||||
--tw-color-primary-50: 254 252 232;
|
||||
--tw-color-primary-100: 254 249 195;
|
||||
--tw-color-primary-200: 254 240 138;
|
||||
--tw-color-primary-300: 253 224 71;
|
||||
--tw-color-primary-400: 250 204 21;
|
||||
--tw-color-primary-500: 234 179 8;
|
||||
--tw-color-primary-600: 202 138 4;
|
||||
--tw-color-primary-700: 161 98 7;
|
||||
--tw-color-primary-800: 133 77 14;
|
||||
--tw-color-primary-900: 113 63 18;
|
||||
--tw-color-primary-950: 66 32 6;
|
||||
--color-primary-50: rgb(var(--tw-color-primary-50)); /* #fefce8 */
|
||||
--color-primary-100: rgb(var(--tw-color-primary-100)); /* #fef9c3 */
|
||||
--color-primary-200: rgb(var(--tw-color-primary-200)); /* #fef08a */
|
||||
--color-primary-300: rgb(var(--tw-color-primary-300)); /* #fde047 */
|
||||
--color-primary-400: rgb(var(--tw-color-primary-400)); /* #facc15 */
|
||||
--color-primary-500: rgb(var(--tw-color-primary-500)); /* #eab308 */
|
||||
--color-primary-600: rgb(var(--tw-color-primary-600)); /* #ca8a04 */
|
||||
--color-primary-700: rgb(var(--tw-color-primary-700)); /* #a16207 */
|
||||
--color-primary-800: rgb(var(--tw-color-primary-800)); /* #854d0e */
|
||||
--color-primary-900: rgb(var(--tw-color-primary-900)); /* #713f12 */
|
||||
--color-primary-950: rgb(var(--tw-color-primary-950)); /* #422006 */
|
||||
}
|
||||
.lime {
|
||||
--tw-color-primary-50: 247 254 231;
|
||||
--tw-color-primary-100: 236 252 203;
|
||||
--tw-color-primary-200: 217 249 157;
|
||||
--tw-color-primary-300: 190 242 100;
|
||||
--tw-color-primary-400: 163 230 53;
|
||||
--tw-color-primary-500: 132 204 22;
|
||||
--tw-color-primary-600: 101 163 13;
|
||||
--tw-color-primary-700: 77 124 15;
|
||||
--tw-color-primary-800: 63 98 18;
|
||||
--tw-color-primary-900: 54 83 20;
|
||||
--tw-color-primary-950: 26 46 5;
|
||||
--color-primary-50: rgb(var(--tw-color-primary-50)); /* #f7fee7 */
|
||||
--color-primary-100: rgb(var(--tw-color-primary-100)); /* #ecfccb */
|
||||
--color-primary-200: rgb(var(--tw-color-primary-200)); /* #d9f99d */
|
||||
--color-primary-300: rgb(var(--tw-color-primary-300)); /* #bef264 */
|
||||
--color-primary-400: rgb(var(--tw-color-primary-400)); /* #a3e635 */
|
||||
--color-primary-500: rgb(var(--tw-color-primary-500)); /* #84cc16 */
|
||||
--color-primary-600: rgb(var(--tw-color-primary-600)); /* #65a30d */
|
||||
--color-primary-700: rgb(var(--tw-color-primary-700)); /* #4d7c0f */
|
||||
--color-primary-800: rgb(var(--tw-color-primary-800)); /* #3f6212 */
|
||||
--color-primary-900: rgb(var(--tw-color-primary-900)); /* #365314 */
|
||||
--color-primary-950: rgb(var(--tw-color-primary-950)); /* #1a2e05 */
|
||||
}
|
||||
|
||||
.green {
|
||||
--tw-color-primary-50: 240 253 244;
|
||||
--tw-color-primary-100: 220 252 231;
|
||||
--tw-color-primary-200: 187 247 208;
|
||||
--tw-color-primary-300: 134 239 172;
|
||||
--tw-color-primary-400: 74 222 128;
|
||||
--tw-color-primary-500: 34 197 94;
|
||||
--tw-color-primary-600: 22 163 74;
|
||||
--tw-color-primary-700: 21 128 61;
|
||||
--tw-color-primary-800: 22 101 52;
|
||||
--tw-color-primary-900: 20 83 45;
|
||||
--tw-color-primary-950: 5 46 22;
|
||||
--color-primary-50: rgb(var(--tw-color-primary-50)); /* #f0fdf4 */
|
||||
--color-primary-100: rgb(var(--tw-color-primary-100)); /* #dcfce7 */
|
||||
--color-primary-200: rgb(var(--tw-color-primary-200)); /* #bbf7d0 */
|
||||
--color-primary-300: rgb(var(--tw-color-primary-300)); /* #86efac */
|
||||
--color-primary-400: rgb(var(--tw-color-primary-400)); /* #4ade80 */
|
||||
--color-primary-500: rgb(var(--tw-color-primary-500)); /* #22c55e */
|
||||
--color-primary-600: rgb(var(--tw-color-primary-600)); /* #16a34a */
|
||||
--color-primary-700: rgb(var(--tw-color-primary-700)); /* #15803d */
|
||||
--color-primary-800: rgb(var(--tw-color-primary-800)); /* #166534 */
|
||||
--color-primary-900: rgb(var(--tw-color-primary-900)); /* #14532d */
|
||||
--color-primary-950: rgb(var(--tw-color-primary-950)); /* #052e16 */
|
||||
}
|
||||
|
||||
.emerald {
|
||||
--tw-color-primary-50: 236 253 245;
|
||||
--tw-color-primary-100: 209 250 229;
|
||||
--tw-color-primary-200: 167 243 208;
|
||||
--tw-color-primary-300: 110 231 183;
|
||||
--tw-color-primary-400: 52 211 153;
|
||||
--tw-color-primary-500: 16 185 129;
|
||||
--tw-color-primary-600: 5 150 105;
|
||||
--tw-color-primary-700: 4 120 87;
|
||||
--tw-color-primary-800: 6 95 70;
|
||||
--tw-color-primary-900: 6 78 59;
|
||||
--tw-color-primary-950: 2 44 34;
|
||||
--color-primary-50: rgb(var(--tw-color-primary-50)); /* #ecfdf5 */
|
||||
--color-primary-100: rgb(var(--tw-color-primary-100)); /* #d1fae5 */
|
||||
--color-primary-200: rgb(var(--tw-color-primary-200)); /* #a7f3d0 */
|
||||
--color-primary-300: rgb(var(--tw-color-primary-300)); /* #6ee7b7 */
|
||||
--color-primary-400: rgb(var(--tw-color-primary-400)); /* #34d399 */
|
||||
--color-primary-500: rgb(var(--tw-color-primary-500)); /* #10b981 */
|
||||
--color-primary-600: rgb(var(--tw-color-primary-600)); /* #059669 */
|
||||
--color-primary-700: rgb(var(--tw-color-primary-700)); /* #047857 */
|
||||
--color-primary-800: rgb(var(--tw-color-primary-800)); /* #065f46 */
|
||||
--color-primary-900: rgb(var(--tw-color-primary-900)); /* #064e3b */
|
||||
--color-primary-950: rgb(var(--tw-color-primary-950)); /* #022c22 */
|
||||
}
|
||||
|
||||
.teal {
|
||||
--tw-color-primary-50: 240 253 250;
|
||||
--tw-color-primary-100: 204 251 241;
|
||||
--tw-color-primary-200: 153 246 228;
|
||||
--tw-color-primary-300: 94 234 212;
|
||||
--tw-color-primary-400: 45 212 191;
|
||||
--tw-color-primary-500: 20 184 166;
|
||||
--tw-color-primary-600: 13 148 136;
|
||||
--tw-color-primary-700: 15 118 110;
|
||||
--tw-color-primary-800: 17 94 89;
|
||||
--tw-color-primary-900: 19 78 74;
|
||||
--tw-color-primary-950: 4 47 46;
|
||||
--color-primary-50: rgb(var(--tw-color-primary-50)); /* #f0fdfa */
|
||||
--color-primary-100: rgb(var(--tw-color-primary-100)); /* #ccfbf1 */
|
||||
--color-primary-200: rgb(var(--tw-color-primary-200)); /* #99f6e4 */
|
||||
--color-primary-300: rgb(var(--tw-color-primary-300)); /* #5eead4 */
|
||||
--color-primary-400: rgb(var(--tw-color-primary-400)); /* #2dd4bf */
|
||||
--color-primary-500: rgb(var(--tw-color-primary-500)); /* #14b8a6 */
|
||||
--color-primary-600: rgb(var(--tw-color-primary-600)); /* #0d9488 */
|
||||
--color-primary-700: rgb(var(--tw-color-primary-700)); /* #0f766e */
|
||||
--color-primary-800: rgb(var(--tw-color-primary-800)); /* #115e59 */
|
||||
--color-primary-900: rgb(var(--tw-color-primary-900)); /* #134e4a */
|
||||
--color-primary-950: rgb(var(--tw-color-primary-950)); /* #042f2e */
|
||||
}
|
||||
|
||||
.cyan {
|
||||
--tw-color-primary-50: 236 254 255;
|
||||
--tw-color-primary-100: 207 250 254;
|
||||
--tw-color-primary-200: 165 243 252;
|
||||
--tw-color-primary-300: 103 232 249;
|
||||
--tw-color-primary-400: 34 211 238;
|
||||
--tw-color-primary-500: 6 182 212;
|
||||
--tw-color-primary-600: 8 145 178;
|
||||
--tw-color-primary-700: 14 116 144;
|
||||
--tw-color-primary-800: 21 94 117;
|
||||
--tw-color-primary-900: 22 78 99;
|
||||
--tw-color-primary-950: 8 51 68;
|
||||
--color-primary-50: rgb(var(--tw-color-primary-50)); /* #ecfeff */
|
||||
--color-primary-100: rgb(var(--tw-color-primary-100)); /* #cffafe */
|
||||
--color-primary-200: rgb(var(--tw-color-primary-200)); /* #a5f3fc */
|
||||
--color-primary-300: rgb(var(--tw-color-primary-300)); /* #67e8f9 */
|
||||
--color-primary-400: rgb(var(--tw-color-primary-400)); /* #22d3ee */
|
||||
--color-primary-500: rgb(var(--tw-color-primary-500)); /* #06b6d4 */
|
||||
--color-primary-600: rgb(var(--tw-color-primary-600)); /* #0891b2 */
|
||||
--color-primary-700: rgb(var(--tw-color-primary-700)); /* #0e7490 */
|
||||
--color-primary-800: rgb(var(--tw-color-primary-800)); /* #155e75 */
|
||||
--color-primary-900: rgb(var(--tw-color-primary-900)); /* #164e63 */
|
||||
--color-primary-950: rgb(var(--tw-color-primary-950)); /* #083344 */
|
||||
}
|
||||
|
||||
.sky {
|
||||
--tw-color-primary-50: 240 249 255;
|
||||
--tw-color-primary-100: 224 242 254;
|
||||
--tw-color-primary-200: 186 230 253;
|
||||
--tw-color-primary-300: 125 211 252;
|
||||
--tw-color-primary-400: 56 189 248;
|
||||
--tw-color-primary-500: 14 165 233;
|
||||
--tw-color-primary-600: 2 132 199;
|
||||
--tw-color-primary-700: 3 105 161;
|
||||
--tw-color-primary-800: 7 89 133;
|
||||
--tw-color-primary-900: 12 74 110;
|
||||
--tw-color-primary-950: 8 47 73;
|
||||
--color-primary-50: rgb(var(--tw-color-primary-50)); /* #f0f9ff */
|
||||
--color-primary-100: rgb(var(--tw-color-primary-100)); /* #e0f2fe */
|
||||
--color-primary-200: rgb(var(--tw-color-primary-200)); /* #bae6fd */
|
||||
--color-primary-300: rgb(var(--tw-color-primary-300)); /* #7dd3fc */
|
||||
--color-primary-400: rgb(var(--tw-color-primary-400)); /* #38bdf8 */
|
||||
--color-primary-500: rgb(var(--tw-color-primary-500)); /* #0ea5e9 */
|
||||
--color-primary-600: rgb(var(--tw-color-primary-600)); /* #0284c7 */
|
||||
--color-primary-700: rgb(var(--tw-color-primary-700)); /* #0369a1 */
|
||||
--color-primary-800: rgb(var(--tw-color-primary-800)); /* #075985 */
|
||||
--color-primary-900: rgb(var(--tw-color-primary-900)); /* #0c4a6e */
|
||||
--color-primary-950: rgb(var(--tw-color-primary-950)); /* #082f49 */
|
||||
}
|
||||
|
||||
.blue {
|
||||
--tw-color-primary-50: 239 246 255;
|
||||
--tw-color-primary-100: 219 234 254;
|
||||
--tw-color-primary-200: 191 219 254;
|
||||
--tw-color-primary-300: 147 197 253;
|
||||
--tw-color-primary-400: 96 165 250;
|
||||
--tw-color-primary-500: 59 130 246;
|
||||
--tw-color-primary-600: 37 99 235;
|
||||
--tw-color-primary-700: 29 78 216;
|
||||
--tw-color-primary-800: 30 64 175;
|
||||
--tw-color-primary-900: 30 58 138;
|
||||
--tw-color-primary-950: 23 37 84;
|
||||
--color-primary-50: rgb(var(--tw-color-primary-50)); /* #eff6ff */
|
||||
--color-primary-100: rgb(var(--tw-color-primary-100)); /* #dbeafe */
|
||||
--color-primary-200: rgb(var(--tw-color-primary-200)); /* #bfdbfe */
|
||||
--color-primary-300: rgb(var(--tw-color-primary-300)); /* #93c5fd */
|
||||
--color-primary-400: rgb(var(--tw-color-primary-400)); /* #60a5fa */
|
||||
--color-primary-500: rgb(var(--tw-color-primary-500)); /* #3b82f6 */
|
||||
--color-primary-600: rgb(var(--tw-color-primary-600)); /* #2563eb */
|
||||
--color-primary-700: rgb(var(--tw-color-primary-700)); /* #1d4ed8 */
|
||||
--color-primary-800: rgb(var(--tw-color-primary-800)); /* #1e40af */
|
||||
--color-primary-900: rgb(var(--tw-color-primary-900)); /* #1e3a8a */
|
||||
--color-primary-950: rgb(var(--tw-color-primary-950)); /* #172554 */
|
||||
}
|
||||
|
||||
.indigo {
|
||||
--tw-color-primary-50: 238 242 255;
|
||||
--tw-color-primary-100: 224 231 255;
|
||||
--tw-color-primary-200: 199 210 254;
|
||||
--tw-color-primary-300: 165 180 252;
|
||||
--tw-color-primary-400: 129 140 248;
|
||||
--tw-color-primary-500: 99 102 241;
|
||||
--tw-color-primary-600: 79 70 229;
|
||||
--tw-color-primary-700: 67 56 202;
|
||||
--tw-color-primary-800: 55 48 163;
|
||||
--tw-color-primary-900: 49 46 129;
|
||||
--tw-color-primary-950: 30 27 75;
|
||||
--color-primary-50: rgb(var(--tw-color-primary-50)); /* #eef2ff */
|
||||
--color-primary-100: rgb(var(--tw-color-primary-100)); /* #e0e7ff */
|
||||
--color-primary-200: rgb(var(--tw-color-primary-200)); /* #c7d2fe */
|
||||
--color-primary-300: rgb(var(--tw-color-primary-300)); /* #a5b4fc */
|
||||
--color-primary-400: rgb(var(--tw-color-primary-400)); /* #818cf8 */
|
||||
--color-primary-500: rgb(var(--tw-color-primary-500)); /* #6366f1 */
|
||||
--color-primary-600: rgb(var(--tw-color-primary-600)); /* #4f46e5 */
|
||||
--color-primary-700: rgb(var(--tw-color-primary-700)); /* #4338ca */
|
||||
--color-primary-800: rgb(var(--tw-color-primary-800)); /* #3730a3 */
|
||||
--color-primary-900: rgb(var(--tw-color-primary-900)); /* #312e81 */
|
||||
--color-primary-950: rgb(var(--tw-color-primary-950)); /* #1e1b4b */
|
||||
}
|
||||
|
||||
.violet {
|
||||
--tw-color-primary-50: 245 243 255;
|
||||
--tw-color-primary-100: 237 233 254;
|
||||
--tw-color-primary-200: 221 214 254;
|
||||
--tw-color-primary-300: 196 181 253;
|
||||
--tw-color-primary-400: 167 139 250;
|
||||
--tw-color-primary-500: 139 92 246;
|
||||
--tw-color-primary-600: 124 58 237;
|
||||
--tw-color-primary-700: 109 40 217;
|
||||
--tw-color-primary-800: 91 33 182;
|
||||
--tw-color-primary-900: 76 29 149;
|
||||
--tw-color-primary-950: 46 16 101;
|
||||
--color-primary-50: rgb(var(--tw-color-primary-50)); /* #f5f3ff */
|
||||
--color-primary-100: rgb(var(--tw-color-primary-100)); /* #ede9fe */
|
||||
--color-primary-200: rgb(var(--tw-color-primary-200)); /* #ddd6fe */
|
||||
--color-primary-300: rgb(var(--tw-color-primary-300)); /* #c4b5fd */
|
||||
--color-primary-400: rgb(var(--tw-color-primary-400)); /* #a78bfa */
|
||||
--color-primary-500: rgb(var(--tw-color-primary-500)); /* #8b5cf6 */
|
||||
--color-primary-600: rgb(var(--tw-color-primary-600)); /* #7c3aed */
|
||||
--color-primary-700: rgb(var(--tw-color-primary-700)); /* #6d28d9 */
|
||||
--color-primary-800: rgb(var(--tw-color-primary-800)); /* #5b21b6 */
|
||||
--color-primary-900: rgb(var(--tw-color-primary-900)); /* #4c1d95 */
|
||||
--color-primary-950: rgb(var(--tw-color-primary-950)); /* #2e1065 */
|
||||
}
|
||||
|
||||
.purple {
|
||||
--tw-color-primary-50: 250 245 255;
|
||||
--tw-color-primary-100: 243 232 255;
|
||||
--tw-color-primary-200: 233 213 255;
|
||||
--tw-color-primary-300: 216 180 254;
|
||||
--tw-color-primary-400: 192 132 252;
|
||||
--tw-color-primary-500: 168 85 247;
|
||||
--tw-color-primary-600: 147 51 234;
|
||||
--tw-color-primary-700: 126 34 206;
|
||||
--tw-color-primary-800: 107 33 168;
|
||||
--tw-color-primary-900: 88 28 135;
|
||||
--tw-color-primary-950: 59 7 100;
|
||||
--color-primary-50: rgb(var(--tw-color-primary-50)); /* #faf5ff */
|
||||
--color-primary-100: rgb(var(--tw-color-primary-100)); /* #f3e8ff */
|
||||
--color-primary-200: rgb(var(--tw-color-primary-200)); /* #e9d5ff */
|
||||
--color-primary-300: rgb(var(--tw-color-primary-300)); /* #d8b4fe */
|
||||
--color-primary-400: rgb(var(--tw-color-primary-400)); /* #c084fc */
|
||||
--color-primary-500: rgb(var(--tw-color-primary-500)); /* #a855f7 */
|
||||
--color-primary-600: rgb(var(--tw-color-primary-600)); /* #9333ea */
|
||||
--color-primary-700: rgb(var(--tw-color-primary-700)); /* #7e22ce */
|
||||
--color-primary-800: rgb(var(--tw-color-primary-800)); /* #6b21a8 */
|
||||
--color-primary-900: rgb(var(--tw-color-primary-900)); /* #581c87 */
|
||||
--color-primary-950: rgb(var(--tw-color-primary-950)); /* #3b0764 */
|
||||
}
|
||||
|
||||
.fuchsia {
|
||||
--tw-color-primary-50: 253 244 255;
|
||||
--tw-color-primary-100: 250 232 255;
|
||||
--tw-color-primary-200: 245 208 254;
|
||||
--tw-color-primary-300: 240 171 252;
|
||||
--tw-color-primary-400: 232 121 249;
|
||||
--tw-color-primary-500: 217 70 239;
|
||||
--tw-color-primary-600: 192 38 211;
|
||||
--tw-color-primary-700: 162 28 175;
|
||||
--tw-color-primary-800: 134 25 143;
|
||||
--tw-color-primary-900: 112 26 117;
|
||||
--tw-color-primary-950: 74 4 78;
|
||||
--color-primary-50: rgb(var(--tw-color-primary-50)); /* #fdf4ff */
|
||||
--color-primary-100: rgb(var(--tw-color-primary-100)); /* #fae8ff */
|
||||
--color-primary-200: rgb(var(--tw-color-primary-200)); /* #f5d0fe */
|
||||
--color-primary-300: rgb(var(--tw-color-primary-300)); /* #f0abfc */
|
||||
--color-primary-400: rgb(var(--tw-color-primary-400)); /* #e879f9 */
|
||||
--color-primary-500: rgb(var(--tw-color-primary-500)); /* #d946ef */
|
||||
--color-primary-600: rgb(var(--tw-color-primary-600)); /* #c026d3 */
|
||||
--color-primary-700: rgb(var(--tw-color-primary-700)); /* #a21caf */
|
||||
--color-primary-800: rgb(var(--tw-color-primary-800)); /* #86198f */
|
||||
--color-primary-900: rgb(var(--tw-color-primary-900)); /* #701a75 */
|
||||
--color-primary-950: rgb(var(--tw-color-primary-950)); /* #4a044e */
|
||||
}
|
||||
|
||||
.pink {
|
||||
--tw-color-primary-50: 253 242 248;
|
||||
--tw-color-primary-100: 252 231 243;
|
||||
--tw-color-primary-200: 251 207 232;
|
||||
--tw-color-primary-300: 249 168 212;
|
||||
--tw-color-primary-400: 244 114 182;
|
||||
--tw-color-primary-500: 236 72 153;
|
||||
--tw-color-primary-600: 219 39 119;
|
||||
--tw-color-primary-700: 190 24 93;
|
||||
--tw-color-primary-800: 157 23 77;
|
||||
--tw-color-primary-900: 131 24 67;
|
||||
--tw-color-primary-950: 80 4 36;
|
||||
--color-primary-50: rgb(var(--tw-color-primary-50)); /* #fdf2f8 */
|
||||
--color-primary-100: rgb(var(--tw-color-primary-100)); /* #fce7f3 */
|
||||
--color-primary-200: rgb(var(--tw-color-primary-200)); /* #fbcfe8 */
|
||||
--color-primary-300: rgb(var(--tw-color-primary-300)); /* #f9a8d4 */
|
||||
--color-primary-400: rgb(var(--tw-color-primary-400)); /* #f472b6 */
|
||||
--color-primary-500: rgb(var(--tw-color-primary-500)); /* #ec4899 */
|
||||
--color-primary-600: rgb(var(--tw-color-primary-600)); /* #db2777 */
|
||||
--color-primary-700: rgb(var(--tw-color-primary-700)); /* #be185d */
|
||||
--color-primary-800: rgb(var(--tw-color-primary-800)); /* #9d174d */
|
||||
--color-primary-900: rgb(var(--tw-color-primary-900)); /* #831843 */
|
||||
--color-primary-950: rgb(var(--tw-color-primary-950)); /* #500724 */
|
||||
}
|
||||
|
||||
.rose {
|
||||
--tw-color-primary-50: 255 241 242;
|
||||
--tw-color-primary-100: 255 228 230;
|
||||
--tw-color-primary-200: 254 205 211;
|
||||
--tw-color-primary-300: 253 164 175;
|
||||
--tw-color-primary-400: 251 113 133;
|
||||
--tw-color-primary-500: 244 63 94;
|
||||
--tw-color-primary-600: 225 29 72;
|
||||
--tw-color-primary-700: 190 18 60;
|
||||
--tw-color-primary-800: 159 18 57;
|
||||
--tw-color-primary-900: 136 19 55;
|
||||
--tw-color-primary-950: 76 5 25;
|
||||
--color-primary-50: rgb(var(--tw-color-primary-50)); /* #fff1f2 */
|
||||
--color-primary-100: rgb(var(--tw-color-primary-100)); /* #ffe4e6 */
|
||||
--color-primary-200: rgb(var(--tw-color-primary-200)); /* #fecdd3 */
|
||||
--color-primary-300: rgb(var(--tw-color-primary-300)); /* #fda4af */
|
||||
--color-primary-400: rgb(var(--tw-color-primary-400)); /* #fb7185 */
|
||||
--color-primary-500: rgb(var(--tw-color-primary-500)); /* #f43f5e */
|
||||
--color-primary-600: rgb(var(--tw-color-primary-600)); /* #e11d48 */
|
||||
--color-primary-700: rgb(var(--tw-color-primary-700)); /* #be123c */
|
||||
--color-primary-800: rgb(var(--tw-color-primary-800)); /* #9f1239 */
|
||||
--color-primary-900: rgb(var(--tw-color-primary-900)); /* #881337 */
|
||||
--color-primary-950: rgb(var(--tw-color-primary-950)); /* #4c0519 */
|
||||
}
|
||||
118
src/styles/globals.css
Normal file
@@ -0,0 +1,118 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
/* #region /**=========== Primary Color =========== */
|
||||
/* !STARTERCONF Customize these variable, copy and paste from /styles/colors.css for list of colors */
|
||||
--tw-color-primary-50: 240 249 255;
|
||||
--tw-color-primary-100: 224 242 254;
|
||||
--tw-color-primary-200: 186 230 253;
|
||||
--tw-color-primary-300: 125 211 252;
|
||||
--tw-color-primary-400: 56 189 248;
|
||||
--tw-color-primary-500: 14 165 233;
|
||||
--tw-color-primary-600: 2 132 199;
|
||||
--tw-color-primary-700: 3 105 161;
|
||||
--tw-color-primary-800: 7 89 133;
|
||||
--tw-color-primary-900: 12 74 110;
|
||||
--color-primary-50: rgb(var(--tw-color-primary-50)); /* #f0f9ff */
|
||||
--color-primary-100: rgb(var(--tw-color-primary-100)); /* #e0f2fe */
|
||||
--color-primary-200: rgb(var(--tw-color-primary-200)); /* #bae6fd */
|
||||
--color-primary-300: rgb(var(--tw-color-primary-300)); /* #7dd3fc */
|
||||
--color-primary-400: rgb(var(--tw-color-primary-400)); /* #38bdf8 */
|
||||
--color-primary-500: rgb(var(--tw-color-primary-500)); /* #0ea5e9 */
|
||||
--color-primary-600: rgb(var(--tw-color-primary-600)); /* #0284c7 */
|
||||
--color-primary-700: rgb(var(--tw-color-primary-700)); /* #0369a1 */
|
||||
--color-primary-800: rgb(var(--tw-color-primary-800)); /* #075985 */
|
||||
--color-primary-900: rgb(var(--tw-color-primary-900)); /* #0c4a6e */
|
||||
/* #endregion /**======== Primary Color =========== */
|
||||
}
|
||||
|
||||
@layer base {
|
||||
/* inter var - latin */
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 100 900;
|
||||
font-display: block;
|
||||
src: url('/fonts/inter-var-latin.woff2') format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
|
||||
U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212,
|
||||
U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
.cursor-newtab {
|
||||
cursor: url('/images/new-tab.png') 10 10, pointer;
|
||||
}
|
||||
|
||||
/* #region /**=========== Typography =========== */
|
||||
.h0 {
|
||||
@apply font-primary text-3xl font-bold md:text-5xl;
|
||||
}
|
||||
|
||||
h1,
|
||||
.h1 {
|
||||
@apply font-primary text-2xl font-bold md:text-4xl;
|
||||
}
|
||||
|
||||
h2,
|
||||
.h2 {
|
||||
@apply font-primary text-xl font-bold md:text-3xl;
|
||||
}
|
||||
|
||||
h3,
|
||||
.h3 {
|
||||
@apply font-primary text-lg font-bold md:text-2xl;
|
||||
}
|
||||
|
||||
h4,
|
||||
.h4 {
|
||||
@apply font-primary text-base font-bold md:text-lg;
|
||||
}
|
||||
|
||||
body,
|
||||
.p {
|
||||
@apply font-primary text-sm md:text-base;
|
||||
}
|
||||
/* #endregion /**======== Typography =========== */
|
||||
|
||||
.layout {
|
||||
/* 1100px */
|
||||
max-width: 68.75rem;
|
||||
@apply mx-auto w-11/12;
|
||||
}
|
||||
|
||||
.bg-dark a.custom-link {
|
||||
@apply border-gray-200 hover:border-gray-200/0;
|
||||
}
|
||||
|
||||
/* Class to adjust with sticky footer */
|
||||
.min-h-main {
|
||||
@apply min-h-[calc(100vh-56px)];
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.animated-underline {
|
||||
background-image: linear-gradient(#33333300, #33333300),
|
||||
linear-gradient(
|
||||
to right,
|
||||
var(--color-primary-400),
|
||||
var(--color-primary-500)
|
||||
);
|
||||
background-size: 100% 2px, 0 2px;
|
||||
background-position: 100% 100%, 0 100%;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.animated-underline {
|
||||
transition: 0.3s ease;
|
||||
transition-property: background-size, color, background-color,
|
||||
border-color;
|
||||
}
|
||||
}
|
||||
.animated-underline:hover,
|
||||
.animated-underline:focus-visible {
|
||||
background-size: 0 2px, 100% 2px;
|
||||
}
|
||||
}
|
||||
56
tailwind.config.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import type { Config } from 'tailwindcss';
|
||||
import defaultTheme from 'tailwindcss/defaultTheme';
|
||||
|
||||
export default {
|
||||
content: ['./src/**/*.{js,jsx,ts,tsx}'],
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
primary: ['Inter', ...defaultTheme.fontFamily.sans],
|
||||
},
|
||||
colors: {
|
||||
primary: {
|
||||
// Customize it on globals.css :root
|
||||
50: 'rgb(var(--tw-color-primary-50) / <alpha-value>)',
|
||||
100: 'rgb(var(--tw-color-primary-100) / <alpha-value>)',
|
||||
200: 'rgb(var(--tw-color-primary-200) / <alpha-value>)',
|
||||
300: 'rgb(var(--tw-color-primary-300) / <alpha-value>)',
|
||||
400: 'rgb(var(--tw-color-primary-400) / <alpha-value>)',
|
||||
500: 'rgb(var(--tw-color-primary-500) / <alpha-value>)',
|
||||
600: 'rgb(var(--tw-color-primary-600) / <alpha-value>)',
|
||||
700: 'rgb(var(--tw-color-primary-700) / <alpha-value>)',
|
||||
800: 'rgb(var(--tw-color-primary-800) / <alpha-value>)',
|
||||
900: 'rgb(var(--tw-color-primary-900) / <alpha-value>)',
|
||||
950: 'rgb(var(--tw-color-primary-950) / <alpha-value>)',
|
||||
},
|
||||
dark: '#222222',
|
||||
},
|
||||
keyframes: {
|
||||
flicker: {
|
||||
'0%, 19.999%, 22%, 62.999%, 64%, 64.999%, 70%, 100%': {
|
||||
opacity: '0.99',
|
||||
filter:
|
||||
'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))',
|
||||
},
|
||||
'20%, 21.999%, 63%, 63.999%, 65%, 69.999%': {
|
||||
opacity: '0.4',
|
||||
filter: 'none',
|
||||
},
|
||||
},
|
||||
shimmer: {
|
||||
'0%': {
|
||||
backgroundPosition: '-700px 0',
|
||||
},
|
||||
'100%': {
|
||||
backgroundPosition: '700px 0',
|
||||
},
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
flicker: 'flicker 3s linear infinite',
|
||||
shimmer: 'shimmer 1.3s linear infinite',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require('@tailwindcss/forms')],
|
||||
} satisfies Config;
|
||||
31
tsconfig.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"~/*": ["./public/*"]
|
||||
},
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
]
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"],
|
||||
"moduleResolution": ["node_modules", ".next", "node"]
|
||||
}
|
||||
13
vercel.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"headers": [
|
||||
{
|
||||
"source": "/fonts/inter-var-latin.woff2",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Cache-Control",
|
||||
"value": "public, max-age=31536000, immutable"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||