├── .eslintrc.json
├── .gitignore
├── .prettierrc.json
├── LICENSE
├── README.md
├── app
├── (generator)
│ └── page.tsx
├── api
│ └── route.ts
├── favicon.ico
├── layout.tsx
└── providers.tsx
├── bun.lockb
├── components.json
├── next.config.mjs
├── package.json
├── postcss.config.mjs
├── public
└── logo.svg
├── script
└── generate-icons.mjs
├── src
├── assets
│ ├── items.ts
│ ├── orbits.ts
│ ├── tech-orbit.ts
│ └── text.ts
├── atoms
│ ├── duration.ts
│ ├── selected-icons.ts
│ ├── size.ts
│ └── title.ts
├── components
│ ├── add-icon-button.tsx
│ ├── duration-slider.tsx
│ ├── icon-select-section.tsx
│ ├── icons-list.tsx
│ ├── markdown-syntax-section.tsx
│ ├── preview-section.tsx
│ ├── remove-icon-button.tsx
│ ├── search-icons.tsx
│ ├── selected-icons.tsx
│ ├── size-slider.tsx
│ ├── slider-section.tsx
│ ├── title-input-section.tsx
│ └── ui
│ │ ├── button.tsx
│ │ ├── input.tsx
│ │ └── slider.tsx
├── libs
│ └── utils.ts
├── styles
│ └── globals.css
└── types
│ └── icons.ts
├── tailwind.config.ts
└── tsconfig.json
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["next/core-web-vitals", "prettier"]
3 | }
4 |
--------------------------------------------------------------------------------
/.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 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
38 | # etc
39 | /src/assets/icons.ts
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "semi": false,
3 | "singleQuote": true,
4 | "plugins": ["prettier-plugin-tailwindcss"]
5 | }
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 조성원
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 | # tech-orbit
2 |
3 | "Show off your tech stack in style with tech-orbit!"
4 |
5 | ## Profile README Demo
6 |
7 | ### Markdown
8 |
9 | ```md
10 | 
11 | ```
12 |
13 | ### Demo
14 |
15 | 
16 |
17 | ## Repository README Demo
18 |
19 | ### Markdown
20 |
21 | ```md
22 |
23 | ```
24 |
25 | ### Demo
26 |
27 |
28 |
--------------------------------------------------------------------------------
/app/(generator)/page.tsx:
--------------------------------------------------------------------------------
1 | import Image from 'next/image'
2 |
3 | import { MarkdownSyntaxSection } from '@/components/markdown-syntax-section'
4 | import { PreviewSection } from '@/components/preview-section'
5 | import { SliderSection } from '@/components/slider-section'
6 | import { TitleInputSection } from '@/components/title-input-section'
7 | import { IconSelectSection } from '@/components/icon-select-section'
8 |
9 | export default function Home() {
10 | return (
11 |
12 |
13 |
21 |
TECH-ORBIT
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | )
32 | }
33 |
--------------------------------------------------------------------------------
/app/api/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from 'next/server'
2 |
3 | import { techOrbit } from '@/assets/tech-orbit'
4 |
5 | export async function GET(req: NextRequest) {
6 | const { searchParams } = req.nextUrl
7 | const title = searchParams.get('title') || ''
8 | const tech = searchParams.get('tech') || ''
9 | const size = searchParams.get('size') || '500'
10 | const duration = searchParams.get('duration') || '20'
11 |
12 | const svg = techOrbit(
13 | Number(size),
14 | Number(duration),
15 | tech
16 | .toLowerCase()
17 | .split(',')
18 | .filter((item) => item !== ''),
19 | title,
20 | )
21 |
22 | return new NextResponse(svg, {
23 | headers: {
24 | 'Content-Type': 'image/svg+xml',
25 | },
26 | })
27 | }
28 |
--------------------------------------------------------------------------------
/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wontory/tech-orbit/83c6bc23a82989c1f04bb241f6e5f7a20d1c46b7/app/favicon.ico
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from 'next'
2 | import { Analytics } from '@vercel/analytics/react'
3 | import { GeistSans } from 'geist/font/sans'
4 | import { GeistMono } from 'geist/font/mono'
5 | import '@/styles/globals.css'
6 |
7 | import { cn } from '@/libs/utils'
8 | import Providers from './providers'
9 |
10 | export const metadata: Metadata = {
11 | title: 'tech-orbit',
12 | description: 'Show off your tech stack in style with tech-orbit!',
13 | }
14 |
15 | export default function RootLayout({
16 | children,
17 | }: Readonly<{
18 | children: React.ReactNode
19 | }>) {
20 | return (
21 |
22 |
23 | {children}
24 |
25 |
26 |
27 | )
28 | }
29 |
--------------------------------------------------------------------------------
/app/providers.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { Provider } from 'jotai'
4 |
5 | export default function Providers({ children }: { children: React.ReactNode }) {
6 | return {children}
7 | }
8 |
--------------------------------------------------------------------------------
/bun.lockb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wontory/tech-orbit/83c6bc23a82989c1f04bb241f6e5f7a20d1c46b7/bun.lockb
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": false,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "src/styles/globals.css",
9 | "baseColor": "slate",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/libs/utils"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | images: {
4 | dangerouslyAllowSVG: true,
5 | },
6 | }
7 |
8 | export default nextConfig
9 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tech-orbit",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "prepare": "bun script/generate-icons.mjs",
7 | "dev": "next dev",
8 | "prebuild": "bun script/generate-icons.mjs",
9 | "build": "next build",
10 | "start": "next start",
11 | "lint": "next lint"
12 | },
13 | "dependencies": {
14 | "@radix-ui/react-slider": "^1.2.0",
15 | "@radix-ui/react-slot": "^1.1.0",
16 | "@tanstack/react-virtual": "^3.10.6",
17 | "@vercel/analytics": "^1.5.0",
18 | "class-variance-authority": "^0.7.0",
19 | "clsx": "^2.1.1",
20 | "geist": "^1.3.0",
21 | "jotai": "^2.8.3",
22 | "lucide-react": "^0.482.0",
23 | "next": "14.2.24",
24 | "react": "^18.3.1",
25 | "react-dom": "^18.3.1",
26 | "simple-icons": "^14.0.0",
27 | "tailwind-merge": "^3.0.0",
28 | "tailwindcss-animate": "^1.0.7"
29 | },
30 | "devDependencies": {
31 | "@types/node": "^22.0.0",
32 | "@types/react": "^18.3.3",
33 | "@types/react-dom": "^18.3.0",
34 | "eslint": "^9.0.0",
35 | "eslint-config-next": "14.2.24",
36 | "eslint-config-prettier": "^10.0.0",
37 | "postcss": "^8.4.38",
38 | "prettier": "^3.3.2",
39 | "prettier-plugin-tailwindcss": "^0.6.5",
40 | "tailwindcss": "^3.4.4",
41 | "typescript": "^5.5.2"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | }
7 |
8 | export default config
9 |
--------------------------------------------------------------------------------
/public/logo.svg:
--------------------------------------------------------------------------------
1 |
23 |
--------------------------------------------------------------------------------
/script/generate-icons.mjs:
--------------------------------------------------------------------------------
1 | import fs from 'fs'
2 | import * as simpleIcons from 'simple-icons'
3 |
4 | const iconsObject = {}
5 |
6 | Object.keys(simpleIcons).forEach((key) => {
7 | const icon = simpleIcons[key]
8 |
9 | iconsObject[icon.title.toLowerCase()] = {
10 | title: icon.title,
11 | svg: icon.svg,
12 | hex: icon.hex,
13 | }
14 | })
15 |
16 | const iconsJson = JSON.stringify(iconsObject)
17 |
18 | fs.writeFileSync(
19 | './src/assets/icons.ts',
20 | `const icons = ${iconsJson}; export { icons };`,
21 | )
22 |
23 | console.log('Icons data has been generated and saved to iconsData.json')
24 |
--------------------------------------------------------------------------------
/src/assets/items.ts:
--------------------------------------------------------------------------------
1 | import { icons } from '@/assets/icons'
2 | import type { IconsKeyType } from '@/types/icons'
3 |
4 | function items(size: number, data: string[], duration: number) {
5 | return data.map((item, index) => {
6 | const orbitOrder = Math.floor(index / 2)
7 | const orbitRadius = 80 + 110 * orbitOrder
8 | const itemRadius = 30 + 20 * orbitOrder
9 | const initialAngle = (index % 2) * -180
10 |
11 | const icon = icons[item as IconsKeyType]
12 |
13 | return `
14 |
28 |
36 | ${icon.svg}
37 |
38 | `
39 | })
40 | }
41 |
42 | export { items }
43 |
--------------------------------------------------------------------------------
/src/assets/orbits.ts:
--------------------------------------------------------------------------------
1 | function orbits(size: number, length: number) {
2 | const orbitTotal = Math.ceil(length / 2)
3 |
4 | return Array.from(
5 | { length: orbitTotal },
6 | (_, index) => `
7 |
16 | `,
17 | )
18 | }
19 |
20 | export { orbits }
21 |
--------------------------------------------------------------------------------
/src/assets/tech-orbit.ts:
--------------------------------------------------------------------------------
1 | import { orbits } from '@/assets/orbits'
2 | import { items } from '@/assets/items'
3 | import { text } from '@/assets/text'
4 |
5 | function techOrbit(
6 | size: number,
7 | duration: number,
8 | data: string[],
9 | title: string,
10 | ) {
11 | return `
12 |
24 | `
25 | }
26 |
27 | export { techOrbit }
28 |
--------------------------------------------------------------------------------
/src/assets/text.ts:
--------------------------------------------------------------------------------
1 | function text(title: string) {
2 | return `
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
15 |
16 | ${title}
17 |
18 | `
19 | }
20 |
21 | export { text }
22 |
--------------------------------------------------------------------------------
/src/atoms/duration.ts:
--------------------------------------------------------------------------------
1 | import { atom } from 'jotai'
2 |
3 | const durationAtom = atom([20])
4 |
5 | export { durationAtom }
6 |
--------------------------------------------------------------------------------
/src/atoms/selected-icons.ts:
--------------------------------------------------------------------------------
1 | import { atom } from 'jotai'
2 |
3 | const selectedIconsAtom = atom([])
4 |
5 | export { selectedIconsAtom }
6 |
--------------------------------------------------------------------------------
/src/atoms/size.ts:
--------------------------------------------------------------------------------
1 | import { atom } from 'jotai'
2 |
3 | const sizeAtom = atom([500])
4 |
5 | export { sizeAtom }
6 |
--------------------------------------------------------------------------------
/src/atoms/title.ts:
--------------------------------------------------------------------------------
1 | import { atom } from 'jotai'
2 |
3 | const titleAtom = atom('')
4 |
5 | export { titleAtom }
6 |
--------------------------------------------------------------------------------
/src/components/add-icon-button.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useSetAtom } from 'jotai'
4 |
5 | import { Button } from '@/components/ui/button'
6 | import { selectedIconsAtom } from '@/atoms/selected-icons'
7 | import type { IconType } from '@/types/icons'
8 |
9 | function AddIconButton({ icon }: { icon: IconType }) {
10 | const setSelectedIcons = useSetAtom(selectedIconsAtom)
11 |
12 | const handleClick = () => setSelectedIcons((prev) => [...prev, icon.title])
13 |
14 | return (
15 |
28 | )
29 | }
30 |
31 | export { AddIconButton }
32 |
--------------------------------------------------------------------------------
/src/components/duration-slider.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useAtom } from 'jotai'
4 |
5 | import { Slider } from '@/components/ui/slider'
6 | import { durationAtom } from '@/atoms/duration'
7 |
8 | function DurationSlider() {
9 | const [duration, setDuration] = useAtom(durationAtom)
10 |
11 | return (
12 | setDuration(value)}
18 | className="py-1.5"
19 | />
20 | )
21 | }
22 |
23 | export { DurationSlider }
24 |
--------------------------------------------------------------------------------
/src/components/icon-select-section.tsx:
--------------------------------------------------------------------------------
1 | import { SelectedIcons } from '@/components/selected-icons'
2 | import { SearchIcons } from '@/components/search-icons'
3 |
4 | function IconSelectSection() {
5 | return (
6 |
11 | )
12 | }
13 |
14 | export { IconSelectSection }
15 |
--------------------------------------------------------------------------------
/src/components/icons-list.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useRef } from 'react'
4 | import { useVirtualizer } from '@tanstack/react-virtual'
5 |
6 | import { AddIconButton } from '@/components/add-icon-button'
7 | import { icons } from '@/assets/icons'
8 |
9 | function IconsList({ searchValue }: { searchValue: string }) {
10 | const parentRef = useRef(null)
11 |
12 | const filteredIcons = Object.entries(icons).filter((icon) =>
13 | icon[1].title.toLowerCase().includes(searchValue),
14 | )
15 |
16 | const rowVirtualizer = useVirtualizer({
17 | count: filteredIcons.length,
18 | getScrollElement: () => parentRef.current,
19 | estimateSize: () => 66,
20 | gap: 16,
21 | })
22 |
23 | return (
24 |
28 |
34 | {rowVirtualizer.getVirtualItems().map((virtualRow) => (
35 |
45 | ))}
46 |
47 |
48 | )
49 | }
50 |
51 | export { IconsList }
52 |
--------------------------------------------------------------------------------
/src/components/markdown-syntax-section.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useAtomValue } from 'jotai'
4 |
5 | import { titleAtom } from '@/atoms/title'
6 | import { selectedIconsAtom } from '@/atoms/selected-icons'
7 | import { sizeAtom } from '@/atoms/size'
8 | import { durationAtom } from '@/atoms/duration'
9 |
10 | function MarkdownSyntaxSection() {
11 | const title = useAtomValue(titleAtom)
12 | const selectedIcons = useAtomValue(selectedIconsAtom)
13 | const size = useAtomValue(sizeAtom)
14 | const duration = useAtomValue(durationAtom)
15 |
16 | const transTitle = title.replaceAll(' ', '%20')
17 | const transIcons = selectedIcons.join(',').replaceAll(' ', '%20')
18 |
19 | return (
20 |
21 | Markdown Syntax
22 |
23 | {``}
24 |
25 |
26 | )
27 | }
28 |
29 | export { MarkdownSyntaxSection }
30 |
--------------------------------------------------------------------------------
/src/components/preview-section.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useAtomValue } from 'jotai'
4 |
5 | import { titleAtom } from '@/atoms/title'
6 | import { selectedIconsAtom } from '@/atoms/selected-icons'
7 | import { sizeAtom } from '@/atoms/size'
8 | import { durationAtom } from '@/atoms/duration'
9 |
10 | function PreviewSection() {
11 | const title = useAtomValue(titleAtom)
12 | const selectedIcons = useAtomValue(selectedIconsAtom)
13 | const size = useAtomValue(sizeAtom)
14 | const duration = useAtomValue(durationAtom)
15 |
16 | const transTitle = title.replaceAll(' ', '%20')
17 | const transIcons = selectedIcons.join(',').replaceAll(' ', '%20')
18 |
19 | return (
20 |
21 | Preview
22 |
23 |

28 |
29 |
30 | )
31 | }
32 |
33 | export { PreviewSection }
34 |
--------------------------------------------------------------------------------
/src/components/remove-icon-button.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useSetAtom } from 'jotai'
4 |
5 | import { Button } from '@/components/ui/button'
6 | import { selectedIconsAtom } from '@/atoms/selected-icons'
7 | import type { IconType } from '@/types/icons'
8 |
9 | function RemoveIconButton({ index, icon }: { index: number; icon: IconType }) {
10 | const setSelectedIcons = useSetAtom(selectedIconsAtom)
11 |
12 | const handleClick = (index: number) => {
13 | setSelectedIcons((prev) => prev.filter((_, i) => i !== index))
14 | }
15 |
16 | return (
17 |
30 | )
31 | }
32 |
33 | export { RemoveIconButton }
34 |
--------------------------------------------------------------------------------
/src/components/search-icons.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useState } from 'react'
4 |
5 | import { Input } from '@/components/ui/input'
6 | import { IconsList } from './icons-list'
7 |
8 | function SearchIcons() {
9 | const [searchValue, setSearchValue] = useState('')
10 |
11 | const handleSearch = (event: React.ChangeEvent) => {
12 | setSearchValue(event.target.value.toLowerCase())
13 | }
14 |
15 | return (
16 | <>
17 | Search
18 |
19 |
20 | >
21 | )
22 | }
23 |
24 | export { SearchIcons }
25 |
--------------------------------------------------------------------------------
/src/components/selected-icons.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useAtomValue } from 'jotai'
4 |
5 | import { RemoveIconButton } from '@/components/remove-icon-button'
6 | import { selectedIconsAtom } from '@/atoms/selected-icons'
7 | import { icons } from '@/assets/icons'
8 | import type { IconsKeyType } from '@/types/icons'
9 |
10 | function SelectedIcons() {
11 | const selectedIcons = useAtomValue(selectedIconsAtom)
12 |
13 | return (
14 | <>
15 | Selected
16 |
17 | {selectedIcons.length === 0 ? (
18 |
19 | No Selected Icons
20 |
21 | ) : (
22 | selectedIcons.map((selectedIcon, index) => (
23 |
28 | ))
29 | )}
30 |
31 | >
32 | )
33 | }
34 |
35 | export { SelectedIcons }
36 |
--------------------------------------------------------------------------------
/src/components/size-slider.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useAtom } from 'jotai'
4 |
5 | import { Slider } from '@/components/ui/slider'
6 | import { sizeAtom } from '@/atoms/size'
7 |
8 | function SizeSlider() {
9 | const [size, setSize] = useAtom(sizeAtom)
10 |
11 | return (
12 | setSize(value)}
18 | className="py-1.5"
19 | />
20 | )
21 | }
22 |
23 | export { SizeSlider }
24 |
--------------------------------------------------------------------------------
/src/components/slider-section.tsx:
--------------------------------------------------------------------------------
1 | import { DurationSlider } from '@/components/duration-slider'
2 | import { SizeSlider } from '@/components/size-slider'
3 |
4 | function SliderSection() {
5 | return (
6 |
7 | Size
8 |
9 | Duration
10 |
11 |
12 | )
13 | }
14 |
15 | export { SliderSection }
16 |
--------------------------------------------------------------------------------
/src/components/title-input-section.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useSetAtom } from 'jotai'
4 |
5 | import { Input } from '@/components/ui/input'
6 | import { titleAtom } from '@/atoms/title'
7 |
8 | function TitleInputSection() {
9 | const setTitle = useSetAtom(titleAtom)
10 |
11 | const handleEditTitle = (event: React.ChangeEvent) => {
12 | setTitle(event.target.value)
13 | }
14 |
15 | return (
16 |
20 | )
21 | }
22 |
23 | export { TitleInputSection }
24 |
--------------------------------------------------------------------------------
/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { Slot } from '@radix-ui/react-slot'
3 | import { cva, type VariantProps } from 'class-variance-authority'
4 |
5 | import { cn } from '@/libs/utils'
6 |
7 | const buttonVariants = cva(
8 | 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
9 | {
10 | variants: {
11 | variant: {
12 | default: 'bg-primary text-primary-foreground hover:bg-primary/90',
13 | destructive:
14 | 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
15 | outline:
16 | 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
17 | secondary:
18 | 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
19 | ghost: 'hover:bg-accent hover:text-accent-foreground',
20 | link: 'text-primary underline-offset-4 hover:underline',
21 | },
22 | size: {
23 | default: 'h-10 px-4 py-2',
24 | sm: 'h-9 rounded-md px-3',
25 | lg: 'h-11 rounded-md px-8',
26 | icon: 'h-10 w-10',
27 | },
28 | },
29 | defaultVariants: {
30 | variant: 'default',
31 | size: 'default',
32 | },
33 | },
34 | )
35 |
36 | export interface ButtonProps
37 | extends React.ButtonHTMLAttributes,
38 | VariantProps {
39 | asChild?: boolean
40 | }
41 |
42 | const Button = React.forwardRef(
43 | ({ className, variant, size, asChild = false, ...props }, ref) => {
44 | const Comp = asChild ? Slot : 'button'
45 | return (
46 |
51 | )
52 | },
53 | )
54 | Button.displayName = 'Button'
55 |
56 | export { Button, buttonVariants }
57 |
--------------------------------------------------------------------------------
/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | import { cn } from '@/libs/utils'
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | )
21 | },
22 | )
23 | Input.displayName = 'Input'
24 |
25 | export { Input }
26 |
--------------------------------------------------------------------------------
/src/components/ui/slider.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import * as SliderPrimitive from '@radix-ui/react-slider'
3 |
4 | import { cn } from '@/libs/utils'
5 |
6 | const Slider = React.forwardRef<
7 | React.ElementRef,
8 | React.ComponentPropsWithoutRef
9 | >(({ className, ...props }, ref) => (
10 |
18 |
19 |
20 |
21 |
22 |
23 | ))
24 | Slider.displayName = SliderPrimitive.Root.displayName
25 |
26 | export { Slider }
27 |
--------------------------------------------------------------------------------
/src/libs/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from 'clsx'
2 | import { twMerge } from 'tailwind-merge'
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/src/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --background: 0 0% 100%;
8 | --foreground: 222.2 84% 4.9%;
9 |
10 | --card: 0 0% 100%;
11 | --card-foreground: 222.2 84% 4.9%;
12 |
13 | --popover: 0 0% 100%;
14 | --popover-foreground: 222.2 84% 4.9%;
15 |
16 | --primary: 222.2 47.4% 11.2%;
17 | --primary-foreground: 210 40% 98%;
18 |
19 | --secondary: 210 40% 96.1%;
20 | --secondary-foreground: 222.2 47.4% 11.2%;
21 |
22 | --muted: 210 40% 96.1%;
23 | --muted-foreground: 215.4 16.3% 46.9%;
24 |
25 | --accent: 210 40% 96.1%;
26 | --accent-foreground: 222.2 47.4% 11.2%;
27 |
28 | --destructive: 0 84.2% 60.2%;
29 | --destructive-foreground: 210 40% 98%;
30 |
31 | --border: 214.3 31.8% 91.4%;
32 | --input: 214.3 31.8% 91.4%;
33 | --ring: 222.2 84% 4.9%;
34 |
35 | --radius: 0.5rem;
36 | }
37 |
38 | .dark {
39 | --background: 222.2 84% 4.9%;
40 | --foreground: 210 40% 98%;
41 |
42 | --card: 222.2 84% 4.9%;
43 | --card-foreground: 210 40% 98%;
44 |
45 | --popover: 222.2 84% 4.9%;
46 | --popover-foreground: 210 40% 98%;
47 |
48 | --primary: 210 40% 98%;
49 | --primary-foreground: 222.2 47.4% 11.2%;
50 |
51 | --secondary: 217.2 32.6% 17.5%;
52 | --secondary-foreground: 210 40% 98%;
53 |
54 | --muted: 217.2 32.6% 17.5%;
55 | --muted-foreground: 215 20.2% 65.1%;
56 |
57 | --accent: 217.2 32.6% 17.5%;
58 | --accent-foreground: 210 40% 98%;
59 |
60 | --destructive: 0 62.8% 30.6%;
61 | --destructive-foreground: 210 40% 98%;
62 |
63 | --border: 217.2 32.6% 17.5%;
64 | --input: 217.2 32.6% 17.5%;
65 | --ring: 212.7 26.8% 83.9%;
66 | }
67 | }
68 |
69 | @layer base {
70 | * {
71 | @apply border-border;
72 | }
73 | body {
74 | @apply bg-background text-foreground;
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/types/icons.ts:
--------------------------------------------------------------------------------
1 | import { icons } from '@/assets/icons'
2 |
3 | type IconsKeyType = keyof typeof icons
4 |
5 | type IconType = {
6 | title: string
7 | svg: string
8 | hex: string
9 | }
10 |
11 | export type { IconType, IconsKeyType }
12 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from 'tailwindcss'
2 |
3 | const config = {
4 | darkMode: ['class'],
5 | content: [
6 | './pages/**/*.{ts,tsx}',
7 | './components/**/*.{ts,tsx}',
8 | './app/**/*.{ts,tsx}',
9 | './src/**/*.{ts,tsx}',
10 | ],
11 | prefix: '',
12 | theme: {
13 | container: {
14 | center: true,
15 | padding: '2rem',
16 | screens: {
17 | '2xl': '1400px',
18 | },
19 | },
20 | extend: {
21 | fontFamily: {
22 | sans: ['var(--font-geist-sans)'],
23 | mono: ['var(--font-geist-mono)'],
24 | },
25 | colors: {
26 | border: 'hsl(var(--border))',
27 | input: 'hsl(var(--input))',
28 | ring: 'hsl(var(--ring))',
29 | background: 'hsl(var(--background))',
30 | foreground: 'hsl(var(--foreground))',
31 | primary: {
32 | DEFAULT: 'hsl(var(--primary))',
33 | foreground: 'hsl(var(--primary-foreground))',
34 | },
35 | secondary: {
36 | DEFAULT: 'hsl(var(--secondary))',
37 | foreground: 'hsl(var(--secondary-foreground))',
38 | },
39 | destructive: {
40 | DEFAULT: 'hsl(var(--destructive))',
41 | foreground: 'hsl(var(--destructive-foreground))',
42 | },
43 | muted: {
44 | DEFAULT: 'hsl(var(--muted))',
45 | foreground: 'hsl(var(--muted-foreground))',
46 | },
47 | accent: {
48 | DEFAULT: 'hsl(var(--accent))',
49 | foreground: 'hsl(var(--accent-foreground))',
50 | },
51 | popover: {
52 | DEFAULT: 'hsl(var(--popover))',
53 | foreground: 'hsl(var(--popover-foreground))',
54 | },
55 | card: {
56 | DEFAULT: 'hsl(var(--card))',
57 | foreground: 'hsl(var(--card-foreground))',
58 | },
59 | },
60 | borderRadius: {
61 | lg: 'var(--radius)',
62 | md: 'calc(var(--radius) - 2px)',
63 | sm: 'calc(var(--radius) - 4px)',
64 | },
65 | keyframes: {
66 | 'accordion-down': {
67 | from: { height: '0' },
68 | to: { height: 'var(--radix-accordion-content-height)' },
69 | },
70 | 'accordion-up': {
71 | from: { height: 'var(--radix-accordion-content-height)' },
72 | to: { height: '0' },
73 | },
74 | },
75 | animation: {
76 | 'accordion-down': 'accordion-down 0.2s ease-out',
77 | 'accordion-up': 'accordion-up 0.2s ease-out',
78 | },
79 | },
80 | },
81 | plugins: [require('tailwindcss-animate')],
82 | } satisfies Config
83 |
84 | export default config
85 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "noEmit": true,
8 | "esModuleInterop": true,
9 | "module": "esnext",
10 | "moduleResolution": "bundler",
11 | "resolveJsonModule": true,
12 | "isolatedModules": true,
13 | "jsx": "preserve",
14 | "incremental": true,
15 | "plugins": [
16 | {
17 | "name": "next"
18 | }
19 | ],
20 | "paths": {
21 | "@/*": ["./src/*"]
22 | }
23 | },
24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
25 | "exclude": ["node_modules"]
26 | }
27 |
--------------------------------------------------------------------------------