├── .cspell.json
├── .editorconfig
├── .gitignore
├── .npmrc
├── .nvmrc
├── .prettierignore
├── .vscode
├── extensions.json
└── settings.json
├── LICENSE
├── README.md
├── commitlint.config.ts
├── eslint.config.mjs
├── knip.config.ts
├── next.config.ts
├── package.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── postcss.config.mjs
├── prettier.config.js
├── public
└── favicon
│ ├── android-chrome-192x192.png
│ ├── android-chrome-512x512.png
│ ├── apple-touch-icon.png
│ ├── favicon-16x16.png
│ ├── favicon-32x32.png
│ ├── favicon.ico
│ └── site.webmanifest
├── src
├── app
│ ├── analytics.tsx
│ ├── grid.svg
│ ├── layout.tsx
│ ├── page.tsx
│ ├── primary-links.tsx
│ └── sparkle-button.tsx
├── components
│ ├── footer.tsx
│ └── spotlight.tsx
└── styles
│ └── globals.css
├── tailwind.config.ts
└── tsconfig.json
/.cspell.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json",
3 | "dictionaries": ["software-terms", "npm", "fullstack"],
4 | "enableFiletypes": ["tailwindcss"],
5 | "files": ["**", ".vscode/**", ".github/**"],
6 | "ignorePaths": ["pnpm-lock.yaml"],
7 | "useGitignore": true,
8 | "version": "0.2",
9 | "words": [
10 | "bradlc",
11 | "dbaeumer",
12 | "devdotto",
13 | "honghong",
14 | "lightningcss",
15 | "stackoverflow",
16 | "tszhong0411"
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | end_of_line = lf
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
11 | [*.md]
12 | trim_trailing_whitespace = false
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 | .pnpm-debug.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | engine-strict=true
2 | public-hoist-pattern[]=*prettier*
3 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | v22.13.1
2 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | pnpm-lock.yaml
2 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "editorconfig.editorconfig",
4 | "dbaeumer.vscode-eslint",
5 | "bradlc.vscode-tailwindcss",
6 | "streetsidesoftware.code-spell-checker"
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "cSpell.diagnosticLevel": "Warning",
3 | "cSpell.enabled": true,
4 |
5 | "editor.codeActionsOnSave": {
6 | "source.fixAll.eslint": "explicit",
7 | "source.organizeImports": "never"
8 | },
9 |
10 | "editor.formatOnPaste": true,
11 | "editor.formatOnSave": true,
12 |
13 | "eslint.runtime": "node",
14 | "eslint.useFlatConfig": true,
15 | "eslint.workingDirectories": [{ "mode": "auto" }],
16 |
17 | "files.associations": {
18 | "*.css": "tailwindcss",
19 | ".env*": "properties"
20 | },
21 |
22 | "tailwindCSS.experimental.classRegex": [
23 | ["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"],
24 | ["cn\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
25 | ],
26 |
27 | "typescript.enablePromptUseWorkspaceTsdk": true,
28 | "typescript.preferences.preferTypeOnlyAutoImports": true,
29 | "typescript.tsdk": "node_modules/typescript/lib"
30 | }
31 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 tszhong0411
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ## Tech Stack
6 |
7 | | Name | Link |
8 | | ---------- | --------------------------------------------------------- |
9 | | Framework | [Next.js](https://nextjs.org/) |
10 | | Deployment | [Vercel](https://vercel.com) |
11 | | Styling | [Tailwindcss](https://tailwindcss.com) |
12 | | Icons | [react-icons](https://react-icons.github.io/react-icons/) |
13 |
14 | ## Getting Started
15 |
16 | Follow these steps to run the project locally on your machine:
17 |
18 | 1. Clone the repository
19 |
20 | ```bash
21 | git clone https://github.com/tszhong0411/links.git
22 | ```
23 |
24 | 2. Navigate to the project directory
25 |
26 | ```bash
27 | cd links
28 | ```
29 |
30 | 3. Install dependencies
31 |
32 | ```bash
33 | pnpm install
34 | ```
35 |
36 | 4. Run the development server
37 |
38 | ```bash
39 | pnpm dev
40 | ```
41 |
42 |
43 |
44 | Made with ❤️ in Hong Kong
45 |
46 |
--------------------------------------------------------------------------------
/commitlint.config.ts:
--------------------------------------------------------------------------------
1 | import type { UserConfig } from 'cz-git'
2 |
3 | const config: UserConfig = {
4 | extends: ['@commitlint/config-conventional']
5 | }
6 |
7 | export default config
8 |
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import tszhong0411 from '@tszhong0411/eslint-config'
2 |
3 | export default tszhong0411({
4 | project: './tsconfig.json',
5 | tsconfigRootDir: import.meta.dirname,
6 | react: true,
7 | next: true
8 | })
9 |
--------------------------------------------------------------------------------
/knip.config.ts:
--------------------------------------------------------------------------------
1 | import { type KnipConfig } from 'knip'
2 |
3 | const config: KnipConfig = {
4 | ignoreBinaries: ['only-allow'],
5 | ignoreDependencies: ['prettier-plugin-*']
6 | }
7 |
8 | export default config
9 |
--------------------------------------------------------------------------------
/next.config.ts:
--------------------------------------------------------------------------------
1 | import type { NextConfig } from 'next'
2 |
3 | const nextConfig: NextConfig = {
4 | images: {
5 | remotePatterns: [
6 | {
7 | protocol: 'https',
8 | hostname: 'honghong.me'
9 | }
10 | ]
11 | }
12 | }
13 |
14 | export default nextConfig
15 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "name": "links",
4 | "version": "0.0.0",
5 | "description": "tszhong0411's social media links",
6 | "license": "MIT",
7 | "author": "tszhong0411 (https://github.com/tszhong0411/)",
8 | "repository": {
9 | "type": "git",
10 | "url": "git+https://github.com/tszhong0411/links.git"
11 | },
12 | "type": "module",
13 | "scripts": {
14 | "build": "next build",
15 | "check": "pnpm lint && pnpm type-check && pnpm format:check && pnpm check:spelling && pnpm check:knip",
16 | "check:knip": "knip",
17 | "check:spelling": "cspell --show-context --show-suggestions",
18 | "clean": "rm -rf .next",
19 | "dev": "next dev --turbo",
20 | "format:check": "prettier --cache --check --ignore-path .gitignore --ignore-path .prettierignore .",
21 | "format:write": "prettier --cache --write --list-different --ignore-path .gitignore --ignore-path .prettierignore .",
22 | "preinstall": "npx only-allow pnpm",
23 | "lint": "eslint . --max-warnings 0",
24 | "lint:fix": "eslint --fix .",
25 | "prepare": "simple-git-hooks",
26 | "start": "next start",
27 | "type-check": "tsc --noEmit"
28 | },
29 | "config": {
30 | "commitizen": {
31 | "path": "./node_modules/cz-git"
32 | }
33 | },
34 | "dependencies": {
35 | "@tszhong0411/utils": "^0.0.22",
36 | "geist": "^1.3.1",
37 | "next": "15.2.4",
38 | "react": "19.1.0",
39 | "react-dom": "19.1.0",
40 | "react-icons": "^5.5.0"
41 | },
42 | "devDependencies": {
43 | "@commitlint/cli": "19.8.0",
44 | "@commitlint/config-conventional": "19.8.0",
45 | "@tszhong0411/eslint-config": "^0.1.36",
46 | "@tszhong0411/prettier-config": "^0.0.15",
47 | "@tszhong0411/tsconfig": "^0.0.13",
48 | "@types/node": "22.14.0",
49 | "@types/react": "19.1.0",
50 | "@types/react-dom": "19.1.1",
51 | "cspell": "^8.18.1",
52 | "cz-git": "^1.11.1",
53 | "eslint": "9.23.0",
54 | "knip": "^5.46.5",
55 | "lint-staged": "^15.5.0",
56 | "postcss": "^8.5.3",
57 | "postcss-lightningcss": "^1.0.1",
58 | "postcss-load-config": "^6.0.1",
59 | "prettier": "^3.5.3",
60 | "simple-git-hooks": "^2.12.1",
61 | "tailwindcss": "^3.4.17",
62 | "typescript": "5.8.2"
63 | },
64 | "lint-staged": {
65 | "*.{cjs,mjs,js,jsx,cts,mts,ts,tsx,json}": "eslint --fix",
66 | "**/*": "prettier --write --ignore-unknown"
67 | },
68 | "packageManager": "pnpm@10.7.1",
69 | "simple-git-hooks": {
70 | "pre-commit": "pnpm lint-staged",
71 | "commit-msg": "pnpm commitlint --edit $1"
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | onlyBuiltDependencies:
2 | - sharp
3 | - simple-git-hooks
4 |
--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | export default {
3 | plugins: {
4 | tailwindcss: {},
5 | 'postcss-lightningcss': {
6 | browsers: '>= .25%'
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/prettier.config.js:
--------------------------------------------------------------------------------
1 | import tszhong0411 from '@tszhong0411/prettier-config'
2 |
3 | export default tszhong0411()
4 |
--------------------------------------------------------------------------------
/public/favicon/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tszhong0411/links/6cbbaa8f2b35f03c637d945962faa0a3339e0fa7/public/favicon/android-chrome-192x192.png
--------------------------------------------------------------------------------
/public/favicon/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tszhong0411/links/6cbbaa8f2b35f03c637d945962faa0a3339e0fa7/public/favicon/android-chrome-512x512.png
--------------------------------------------------------------------------------
/public/favicon/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tszhong0411/links/6cbbaa8f2b35f03c637d945962faa0a3339e0fa7/public/favicon/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/favicon/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tszhong0411/links/6cbbaa8f2b35f03c637d945962faa0a3339e0fa7/public/favicon/favicon-16x16.png
--------------------------------------------------------------------------------
/public/favicon/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tszhong0411/links/6cbbaa8f2b35f03c637d945962faa0a3339e0fa7/public/favicon/favicon-32x32.png
--------------------------------------------------------------------------------
/public/favicon/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tszhong0411/links/6cbbaa8f2b35f03c637d945962faa0a3339e0fa7/public/favicon/favicon.ico
--------------------------------------------------------------------------------
/public/favicon/site.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "background_color": "#000000",
3 | "display": "standalone",
4 | "icons": [
5 | {
6 | "sizes": "192x192",
7 | "src": "/favicon/android-chrome-192x192.png",
8 | "type": "image/png"
9 | },
10 | {
11 | "sizes": "512x512",
12 | "src": "/favicon/android-chrome-512x512.png",
13 | "type": "image/png"
14 | }
15 | ],
16 | "name": "Links",
17 | "short_name": "Links",
18 | "theme_color": "#000000"
19 | }
20 |
--------------------------------------------------------------------------------
/src/app/analytics.tsx:
--------------------------------------------------------------------------------
1 | import Script from 'next/script'
2 |
3 | const Analytics = () => {
4 | if (process.env.NODE_ENV !== 'production') return null
5 |
6 | return (
7 |
12 | )
13 | }
14 |
15 | export default Analytics
16 |
--------------------------------------------------------------------------------
/src/app/grid.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata, Viewport } from 'next'
2 |
3 | import '@/styles/globals.css'
4 |
5 | import { GeistSans } from 'geist/font/sans'
6 |
7 | import Footer from '@/components/footer'
8 |
9 | import Analytics from './analytics'
10 | import grid from './grid.svg'
11 |
12 | type RootLayoutProps = {
13 | children: React.ReactNode
14 | }
15 |
16 | const SITE_URL =
17 | process.env.NODE_ENV === 'production' ? 'https://links.honghong.me' : 'http://localhost:3000'
18 | const SITE_TITLE = 'Links | Nelson Lai - A Full Stack Engineer'
19 | const SITE_DESCRIPTION =
20 | 'Connect with me on all my social media profiles through Links. Discover new content and stay updated with my latest posts!'
21 |
22 | export const metadata: Metadata = {
23 | metadataBase: new URL(SITE_URL),
24 | title: SITE_TITLE,
25 | description: SITE_DESCRIPTION,
26 | robots: {
27 | index: true,
28 | follow: true,
29 | googleBot: {
30 | index: true,
31 | follow: true,
32 | 'max-video-preview': -1,
33 | 'max-image-preview': 'large',
34 | 'max-snippet': -1
35 | }
36 | },
37 | manifest: '/favicon/site.webmanifest',
38 | twitter: {
39 | title: 'Nelson Lai',
40 | card: 'summary_large_image',
41 | site: '@tszhong0411',
42 | creator: '@tszhong0411',
43 | images: [
44 | {
45 | url: 'https://honghong.me/images/projects/links/cover.png',
46 | width: 1280,
47 | height: 832,
48 | alt: SITE_DESCRIPTION
49 | }
50 | ]
51 | },
52 | alternates: {
53 | canonical: SITE_URL
54 | },
55 | keywords: ['tszhong0411', 'tszhong0411 social media', 'tszhong0411 links', 'links'],
56 | creator: 'tszhong0411',
57 | openGraph: {
58 | url: SITE_URL,
59 | type: 'website',
60 | title: SITE_TITLE,
61 | siteName: SITE_TITLE,
62 | description: SITE_DESCRIPTION,
63 | locale: 'en-US',
64 | images: [
65 | {
66 | url: 'https://honghong.me/images/projects/links/cover.png',
67 | width: 1280,
68 | height: 832,
69 | alt: SITE_DESCRIPTION,
70 | type: 'image/png'
71 | }
72 | ]
73 | },
74 | icons: {
75 | icon: '/favicon/favicon.ico',
76 | shortcut: '/favicon/favicon.ico',
77 | apple: [
78 | {
79 | url: '/favicon/apple-touch-icon.png',
80 | sizes: '180x180',
81 | type: 'image/png'
82 | }
83 | ],
84 | other: [
85 | {
86 | rel: 'icon',
87 | type: 'image/png',
88 | sizes: '16x16',
89 | url: '/favicon/favicon-16x16.png'
90 | },
91 | {
92 | rel: 'icon',
93 | type: 'image/png',
94 | sizes: '32x32',
95 | url: '/favicon/favicon-32x32.png'
96 | }
97 | ]
98 | }
99 | }
100 |
101 | export const viewport: Viewport = {
102 | themeColor: {
103 | color: '#000000'
104 | }
105 | }
106 |
107 | const RootLayout = (props: RootLayoutProps) => {
108 | const { children } = props
109 |
110 | return (
111 |
112 |
113 |
119 |
125 |
126 | {children}
127 |
128 |
129 |
130 |
131 |
132 | )
133 | }
134 |
135 | export default RootLayout
136 |
--------------------------------------------------------------------------------
/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | import Image from 'next/image'
2 | import { FiBook } from 'react-icons/fi'
3 | import {
4 | SiDevdotto,
5 | SiDiscord,
6 | SiFacebook,
7 | SiGithub,
8 | SiInstagram,
9 | SiStackoverflow,
10 | SiSteam,
11 | SiX,
12 | SiYoutube
13 | } from 'react-icons/si'
14 |
15 | import Spotlight from '@/components/spotlight'
16 |
17 | import PrimaryLinks from './primary-links'
18 |
19 | const links = [
20 | {
21 | icon: ,
22 | title: 'Blog',
23 | url: 'https://honghong.me/blog'
24 | },
25 | {
26 | icon: ,
27 | title: 'YouTube',
28 | url: 'https://www.youtube.com/@tszhong0411'
29 | },
30 | {
31 | icon: ,
32 | title: 'Facebook',
33 | url: 'https://www.facebook.com/tszhong0411/'
34 | },
35 | {
36 | icon: ,
37 | title: 'Steam',
38 | url: 'https://steamcommunity.com/profiles/76561199157324617/'
39 | },
40 | {
41 | icon: ,
42 | title: 'Instagram',
43 | url: 'https://instagram.com/tszhong0411/'
44 | },
45 | {
46 | icon: ,
47 | title: 'GitHub',
48 | url: 'https://github.com/tszhong0411'
49 | },
50 | {
51 | icon: ,
52 | title: 'Discord',
53 | url: 'https://discordapp.com/users/886269624608522240'
54 | },
55 | {
56 | icon: ,
57 | title: 'X',
58 | url: 'https://x.com/tszhong0411'
59 | },
60 | {
61 | icon: ,
62 | title: 'Stack overflow',
63 | url: 'https://stackoverflow.com/users/15166428'
64 | },
65 | {
66 | icon: ,
67 | title: 'Dev.to',
68 | url: 'https://dev.to/tszhong0411'
69 | }
70 | ]
71 |
72 | const HomePage = () => {
73 | return (
74 | <>
75 |
76 |
77 |
85 |
Nelson
86 |
Full Stack Engineer
87 |
88 |
89 |
107 | >
108 | )
109 | }
110 |
111 | export default HomePage
112 |
--------------------------------------------------------------------------------
/src/app/primary-links.tsx:
--------------------------------------------------------------------------------
1 | import SparkleButton from './sparkle-button'
2 |
3 | const PrimaryLinks = () => {
4 | return (
5 |
27 | )
28 | }
29 |
30 | export default PrimaryLinks
31 |
--------------------------------------------------------------------------------
/src/app/sparkle-button.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@tszhong0411/utils'
2 |
3 | type SparkleButtonProps = {
4 | children: React.ReactNode
5 | className?: string
6 | }
7 |
8 | type TextProps = {
9 | children: React.ReactNode
10 | }
11 |
12 | type ParticleProps = React.SVGAttributes
13 |
14 | const Particle = (props: ParticleProps) => (
15 |
30 | )
31 |
32 | const SparkleButton = (props: SparkleButtonProps) => {
33 | const { children, className } = props
34 |
35 | return (
36 |
37 | {children}
38 |
42 | {[...Array.from({ length: 20 }).keys()].map((i) => {
43 | const RANDOM = (min: number, max: number) =>
44 | Math.floor(Math.random() * (max - min + 1) + min)
45 |
46 | return (
47 | 0.5 ? RANDOM(300, 800) * -1 : RANDOM(300, 800)
58 | }%`,
59 | '--origin-y': `${
60 | Math.random() > 0.5 ? RANDOM(300, 800) * -1 : RANDOM(300, 800)
61 | }%`,
62 | '--size': `${RANDOM(40, 90) / 100}`
63 | } as React.CSSProperties
64 | }
65 | />
66 | )
67 | })}
68 |
69 |
70 | )
71 | }
72 |
73 | const Spark = () => (
74 |
75 | )
76 |
77 | const Backdrop = () => (
78 |
79 | )
80 |
81 | const Text = (props: TextProps) => {
82 | const { children } = props
83 |
84 | return (
85 |
86 | {children}
87 |
88 | )
89 | }
90 |
91 | const ClassName = cn(
92 | 'peer relative flex scale-[calc(1+var(--active)*0.1)] items-center gap-[0.25em] whitespace-nowrap rounded-[100px] px-6 py-4 font-medium transition-[shadow_var(--transition,scale_var(--transition),background_var(--transition))] [--active:0] [--cut:0.1em] [background:--bg]',
93 | 'before:absolute before:inset-[-0.25em] before:-z-10 before:rounded-[100px] before:border-[0.25em] before:border-solid before:border-[hsl(0_0%_20.08%/0.5)] before:opacity-[var(--active,0)] before:transition-[opacity_var(--transition)]',
94 | '[--bg:radial-gradient(80%_100%_at_center_120%,hsl(0_0%_20%/var(--active)),transparent),hsl(260_0%_12%)]',
95 | 'shadow-[0_0_calc(var(--active)*6em)_calc(var(--active)*3em)_hsl(0_0%_13.08%/75%),0_0.05em_0_0_hsl(0_0%_calc((var(--active)*20%)+30%))_inset,0_-0.05em_0_0_hsl(0_0%_calc(var(--active)*30%))_inset]',
96 | 'hover:[--active:1] hover:[--play-state:running]',
97 | 'active:scale-100'
98 | )
99 |
100 | SparkleButton.Spark = Spark
101 | SparkleButton.Backdrop = Backdrop
102 | SparkleButton.Text = Text
103 | SparkleButton.ClassName = ClassName
104 |
105 | export default SparkleButton
106 |
--------------------------------------------------------------------------------
/src/components/footer.tsx:
--------------------------------------------------------------------------------
1 | const Footer = () => {
2 | return (
3 |
6 | )
7 | }
8 |
9 | export default Footer
10 |
--------------------------------------------------------------------------------
/src/components/spotlight.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@tszhong0411/utils'
2 |
3 | type SpotlightProps = {
4 | className?: string
5 | }
6 |
7 | const Spotlight = (props: SpotlightProps) => {
8 | const { className } = props
9 |
10 | return (
11 |
48 | )
49 | }
50 |
51 | export default Spotlight
52 |
--------------------------------------------------------------------------------
/src/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from 'tailwindcss'
2 |
3 | const config: Config = {
4 | content: ['./src/**/*.{js,ts,jsx,tsx}'],
5 | theme: {
6 | extend: {
7 | fontFamily: {
8 | sans: ['var(--font-geist-sans)']
9 | },
10 | animation: {
11 | spotlight: 'spotlight 2s ease .75s 1 forwards',
12 | 'float-out':
13 | 'float-out calc(var(--duration, 1) * 1s) calc(var(--delay) * -1s) infinite linear',
14 | rotate: 'rotate var(--spark) linear infinite both',
15 | flip: 'flip calc(var(--spark) * 2) infinite steps(2, end)'
16 | },
17 | keyframes: {
18 | spotlight: {
19 | '0%': {
20 | opacity: '0',
21 | transform: 'translate(-72%, -62%) scale(0.5)'
22 | },
23 | '100%': {
24 | opacity: '1',
25 | transform: 'translate(-50%,-40%) scale(1)'
26 | }
27 | },
28 | 'float-out': {
29 | to: {
30 | rotate: '360deg'
31 | }
32 | },
33 | rotate: {
34 | to: {
35 | transform: 'rotate(90deg)'
36 | }
37 | },
38 | flip: {
39 | to: {
40 | rotate: '360deg'
41 | }
42 | }
43 | }
44 | }
45 | },
46 | plugins: []
47 | }
48 |
49 | export default config
50 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "compilerOptions": {
4 | "baseUrl": ".",
5 | "jsx": "preserve",
6 | "paths": {
7 | "@/*": ["src/*"]
8 | }
9 | },
10 | "exclude": ["node_modules"],
11 | "extends": "@tszhong0411/tsconfig/nextjs.json",
12 | "include": [
13 | "next-env.d.ts",
14 | "**/*.ts",
15 | "**/*.tsx",
16 | ".next/types/**/*.ts",
17 | "postcss.config.mjs",
18 | "eslint.config.mjs",
19 | "prettier.config.js"
20 | ]
21 | }
22 |
--------------------------------------------------------------------------------