Initial commit from Create Next App

This commit is contained in:
shinya
2025-06-17 13:15:54 +08:00
commit 2e989e5e9b
77 changed files with 13025 additions and 0 deletions

6
.env.example Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx --no-install commitlint --edit "$1"

4
.husky/post-merge Executable file
View File

@@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
pnpm install

4
.husky/pre-commit Executable file
View File

@@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx lint-staged

1
.npmrc Normal file
View File

@@ -0,0 +1 @@
enable-pre-post-scripts=true

1
.nvmrc Normal file
View File

@@ -0,0 +1 @@
v20.10.0

41
.prettierignore Normal file
View 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
View 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
View File

@@ -0,0 +1,10 @@
{
"Region CSS": {
"prefix": "regc",
"body": [
"/* #region /**=========== ${1} =========== */",
"$0",
"/* #endregion /**======== ${1} =========== */"
]
}
}

9
.vscode/extensions.json vendored Normal file
View 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
View 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
View 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
View 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`
![Button Variants](https://user-images.githubusercontent.com/55318172/144385213-632b3e1f-9a0e-4184-82e0-7905ee3318b4.gif)
- #### 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>
```
![Arrow Link Feature](https://user-images.githubusercontent.com/55318172/144385991-f3521d52-e0a8-49c5-8e87-409231fdd5b6.gif)
- #### Change default theme to white
| ![Home Page](https://user-images.githubusercontent.com/55318172/144386763-00e6c3fd-ee2e-4c9e-87f8-18b036bdc2e1.png) | ![404](https://user-images.githubusercontent.com/55318172/144386764-0e4b4fb0-35a8-4725-a795-f998b06543a1.png) |
| - | - |
### 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
| ![Create Branch Actions](https://user-images.githubusercontent.com/55318172/144379834-8c3e4d4f-d584-4253-9ad8-b9f1d468ed01.gif) <br> Auto Create Branch | ![Auto Resolve](https://user-images.githubusercontent.com/55318172/144382044-0132e755-9cd5-4805-a756-4086f67b3282.gif) <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
![customlink](https://user-images.githubusercontent.com/55318172/129183546-4e8c2059-0493-4459-a1e9-755fbd32fe39.gif)
- #### 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
![404](https://user-images.githubusercontent.com/55318172/129184274-d90631f2-6688-4ed2-bef2-a4d018a4863c.gif)
- #### 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
| ![image](https://user-images.githubusercontent.com/55318172/137617070-806a0509-84bd-4cae-a900-2ab17e418d8d.png) | ![image](https://user-images.githubusercontent.com/55318172/137617090-c24f684a-bfe5-41b6-8ba9-fa99bae5cadf.png) |
| --------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
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
View 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>
[![GitHub Repo stars](https://img.shields.io/github/stars/theodorusclarence/ts-nextjs-tailwind-starter)](https://github.com/theodorusclarence/ts-nextjs-tailwind-starter/stargazers)
[![Depfu](https://badges.depfu.com/badges/fc6e730632ab9dacaf7df478a08684a7/overview.svg)](https://depfu.com/github/theodorusclarence/ts-nextjs-tailwind-starter?project_id=30160)
[![Last Update](https://img.shields.io/badge/deps%20update-every%20sunday-blue.svg)](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 😄
![Use as template](https://user-images.githubusercontent.com/55318172/129183039-1a61e68d-dd90-4548-9489-7b3ccbb35810.png)
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
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https%3A%2F%2Fgithub.com%2Ftheodorusclarence%2Fts-nextjs-tailwind-starter)
### 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
View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

6
postcss.config.js Normal file
View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 937 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
public/favicon/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View 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"
}

Binary file not shown.

BIN
public/images/new-tab.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 565 B

BIN
public/images/og.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

1
public/svg/Logo.svg Normal file
View 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
View 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
View 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;

View 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();
});
});

View File

@@ -0,0 +1,5 @@
import { NextResponse } from 'next/server';
export async function GET() {
return NextResponse.json({ hello: 'Next.js' });
}

View 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
View 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
View 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
View 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
View 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
View 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>
);
}

View 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>
);
}

View 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}
/>
);
}

View 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;

View 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;

View 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;

View 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>
);
}

View 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;

View 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;

View 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;

View 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;

View 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
View 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
View 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;

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,13 @@
{
"headers": [
{
"source": "/fonts/inter-var-latin.woff2",
"headers": [
{
"key": "Cache-Control",
"value": "public, max-age=31536000, immutable"
}
]
}
]
}