diff --git a/frontend/components.json b/frontend/components.json new file mode 100644 index 0000000..a678827 --- /dev/null +++ b/frontend/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/styles/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index 26531b0..4922303 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -15,11 +15,17 @@ "@graphql-codegen/typescript": "4.1.6", "@graphql-codegen/typescript-operations": "4.6.1", "@graphql-codegen/typescript-react-apollo": "4.3.3", + "@radix-ui/react-slot": "1.2.3", + "class-variance-authority": "0.7.1", + "clsx": "2.1.1", "graphql": "16.11.0", + "lucide-react": "0.534.0", "next": "15.4.5", - "next-intl": "^4.3.4", + "next-intl": "4.3.4", + "next-themes": "0.4.6", "react": "19.1.1", - "react-dom": "19.1.1" + "react-dom": "19.1.1", + "tailwind-merge": "3.3.1" }, "devDependencies": { "@eslint/eslintrc": "3.3.1", @@ -30,6 +36,7 @@ "eslint": "9.32.0", "eslint-config-next": "15.4.4", "tailwindcss": "4.1.11", + "tw-animate-css": "1.3.6", "typescript": "5.8.3" } } diff --git a/frontend/postcss.config.mjs b/frontend/postcss.config.mjs index c7bcb4b..4f154f6 100644 --- a/frontend/postcss.config.mjs +++ b/frontend/postcss.config.mjs @@ -1,5 +1,7 @@ const config = { - plugins: ["@tailwindcss/postcss"], + plugins: { + "@tailwindcss/postcss": {} + }, }; export default config; diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index e3c67d3..f2cb1b7 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -1,8 +1,9 @@ import ApolloClientProvider from '@/providers/ApolloClientProvider'; +import { ThemeProvider } from '@/providers/ThemeProvider'; import type { Metadata } from "next"; import { NextIntlClientProvider } from 'next-intl'; import { getLocale, getMessages } from 'next-intl/server'; -import { Geist, Geist_Mono } from "next/font/google"; +import { Geist } from "next/font/google"; import "../styles/globals.css"; const geistSans = Geist({ @@ -10,11 +11,6 @@ const geistSans = Geist({ subsets: ["latin"], }); -const geistMono = Geist_Mono({ - variable: "--font-geist-mono", - subsets: ["latin"], -}); - export const metadata: Metadata = { title: "Create Next App", description: "Generated by create next app", @@ -29,13 +25,18 @@ export default async function RootLayout({ const messages = await getMessages() return ( - - + + - {children} + + {children} + diff --git a/frontend/src/components/ui/common/Button.tsx b/frontend/src/components/ui/common/Button.tsx new file mode 100644 index 0000000..d4214ed --- /dev/null +++ b/frontend/src/components/ui/common/Button.tsx @@ -0,0 +1,50 @@ +import { cn } from '@/utils/twMerge' +import { Slot } from '@radix-ui/react-slot' +import { type VariantProps, cva } from 'class-variance-authority' +import { type ButtonHTMLAttributes, forwardRef } from 'react' + + +const buttonVariants = cva( + 'inline-flex items-center justify-center gap-2 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 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0', + { + variants: { + variant: { + default: 'bg-primary text-primary-foreground', + outline: 'border border-border bg-background', + secondary: 'bg-secondary text-secondary-foreground', + ghost: 'text-accent-foreground hover:bg-accent hover:text-accent-foreground' + }, + size: { + default: 'h-10 px-5 py-2 rounded-full', + icon: 'size-8 rounded-full', + lgIcon: 'size-10 rounded-full' + } + }, + defaultVariants: { + variant: 'default', + size: 'default' + } + } +) + +export interface ButtonProps + extends ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : 'button' + return ( + + ) + } +) +Button.displayName = 'Button' + +export { Button, buttonVariants } diff --git a/frontend/src/providers/ThemeProvider.tsx b/frontend/src/providers/ThemeProvider.tsx new file mode 100644 index 0000000..3131d41 --- /dev/null +++ b/frontend/src/providers/ThemeProvider.tsx @@ -0,0 +1,10 @@ +"use client"; + +import * as React from "react"; +import { ThemeProvider as NextThemesProvider } from "next-themes"; + +export function ThemeProvider({children, + ...props +}: React.ComponentProps) { + return {children}; +} diff --git a/frontend/src/styles/globals.css b/frontend/src/styles/globals.css index a2dc41e..fc76f0b 100644 --- a/frontend/src/styles/globals.css +++ b/frontend/src/styles/globals.css @@ -1,26 +1,162 @@ -@import "tailwindcss"; +@import 'tailwindcss'; +@import "tw-animate-css"; -:root { - --background: #ffffff; - --foreground: #171717; -} +@custom-variant dark (&:is(.dark *)); + + +@theme { + --background-image-gradient-radial: radial-gradient(var(--tw-gradient-stops)); + --background-image-gradient-conic: conic-gradient( + from 180deg at 50% 50%, + var(--tw-gradient-stops) + ); + + --radius-lg: var(--radius); + --radius-md: calc(var(--radius) - 2px); + --radius-sm: calc(var(--radius) - 4px); -@theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); --font-sans: var(--font-geist-sans); - --font-mono: var(--font-geist-mono); -} -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; + --color-background: hsl(var(--background)); + --color-foreground: hsl(var(--foreground)); + + --color-card: hsl(var(--card)); + --color-card-foreground: hsl(var(--card-foreground)); + + --color-popover: hsl(var(--popover)); + --color-popover-foreground: hsl(var(--popover-foreground)); + + --color-primary: hsl(var(--primary)); + --color-primary-foreground: hsl(var(--primary-foreground)); + + --color-secondary: hsl(var(--secondary)); + --color-secondary-foreground: hsl(var(--secondary-foreground)); + + --color-muted: hsl(var(--muted)); + --color-muted-foreground: hsl(var(--muted-foreground)); + + --color-accent: hsl(var(--accent)); + --color-accent-foreground: hsl(var(--accent-foreground)); + + --color-destructive: hsl(var(--destructive)); + --color-destructive-foreground: hsl(var(--destructive-foreground)); + + --color-border: hsl(var(--border)); + --color-input: hsl(var(--input)); + --color-ring: hsl(var(--ring)); + + --color-chart-1: hsl(var(--chart-1)); + --color-chart-2: hsl(var(--chart-2)); + --color-chart-3: hsl(var(--chart-3)); + --color-chart-4: hsl(var(--chart-4)); + --color-chart-5: hsl(var(--chart-5)); + + --animate-caret-blink: caret-blink 1.25s ease-out infinite; + + @keyframes caret-blink { + 0%, + 70%, + 100% { + opacity: 1; + } + 20%, + 50% { + opacity: 0; + } } } -body { - background: var(--background); - color: var(--foreground); - font-family: Arial, Helvetica, sans-serif; +@layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentcolor); + } +} + +@layer base { + :root { + --background: 240, 7%, 97%; + --foreground: 240, 7%, 6%; + + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; + + --popover: 0 0% 100%; + --popover-foreground: 222.2 47.4% 11.2%; + + --card: 225, 10%, 93%; + --card-foreground: 222.2 47.4% 11.2%; + + --primary: 176, 77%, 41%; + --primary-foreground: 210 40% 98%; + + --secondary: 210 29% 96%; + --secondary-foreground: 222.2 47.4% 11.2%; + + --border: 214.3 31.8% 91.4%; + --input: 240, 7%, 97%; + --radius: 0.5rem; + } + + .dark { + --background: 227, 13%, 14%; + --foreground: 213 31% 91%; + + --muted: 224, 16%, 27%; + --muted-foreground: 215.4 16.3% 56.9%; + + --accent: 227, 13%, 27%; + --accent-foreground: 210 40% 98%; + + --popover: 231, 14%, 20%; + --popover-foreground: 213 31% 91%; + + --card: 224, 16%, 19%; + --card-foreground: 213 31% 91%; + + --primary: 176, 77%, 41%; + --primary-foreground: 210 40% 98%; + + --secondary: 230, 12%, 29%; + --secondary-foreground: 210 40% 98%; + + --border: 217, 19%, 27%; + --input: 227, 13%, 14%; + --radius: 0.5rem; + } +} + +@layer base { + * { + @apply border-border; + } + + html { + @apply scroll-smooth; + } + + body { + @apply overscroll-none bg-background text-foreground; + + font-family: var(--font-geist-sans), sans-serif; + height: 100vh; + } +} + +::-webkit-scrollbar { + width: 9px; +} + +::-webkit-scrollbar-track { + @apply bg-background; +} + +::-webkit-scrollbar-thumb { + @apply rounded-full bg-secondary; } diff --git a/frontend/src/utils/twMerge.ts b/frontend/src/utils/twMerge.ts new file mode 100644 index 0000000..bd0c391 --- /dev/null +++ b/frontend/src/utils/twMerge.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/frontend/tailwind.config.ts b/frontend/tailwind.config.ts new file mode 100644 index 0000000..f57bd1c --- /dev/null +++ b/frontend/tailwind.config.ts @@ -0,0 +1,74 @@ +import type { Config } from 'tailwindcss' + +const config: Config = { + content: ['./src/**/*.{js,ts,jsx,tsx,mdx}'], + theme: { + extend: { + backgroundImage: { + 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', + 'gradient-conic': + 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))' + }, + borderRadius: { + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)' + }, + fontFamily: { + sans: ['var(--font-geist-sans)'] + }, + colors: { + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))' + }, + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))' + }, + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))' + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))' + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))' + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))' + }, + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))' + }, + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + chart: { + '1': 'hsl(var(--chart-1))', + '2': 'hsl(var(--chart-2))', + '3': 'hsl(var(--chart-3))', + '4': 'hsl(var(--chart-4))', + '5': 'hsl(var(--chart-5))' + } + }, + keyframes: { + 'caret-blink': { + '0%,70%,100%': { opacity: '1' }, + '20%,50%': { opacity: '0' } + } + }, + animation: { + 'caret-blink': 'caret-blink 1.25s ease-out infinite' + } + } + }, +} +export default config diff --git a/frontend/yarn.lock b/frontend/yarn.lock index c601c88..e701910 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -1458,6 +1458,18 @@ resolved "https://registry.yarnpkg.com/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz#3dc35ba0f1e66b403c00b39344f870298ebb1c8e" integrity sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA== +"@radix-ui/react-compose-refs@1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz#a2c4c47af6337048ee78ff6dc0d090b390d2bb30" + integrity sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg== + +"@radix-ui/react-slot@^1.2.3": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz#502d6e354fc847d4169c3bc5f189de777f68cfe1" + integrity sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A== + dependencies: + "@radix-ui/react-compose-refs" "1.1.2" + "@repeaterjs/repeater@^3.0.4", "@repeaterjs/repeater@^3.0.6": version "3.0.6" resolved "https://registry.yarnpkg.com/@repeaterjs/repeater/-/repeater-3.0.6.tgz#be23df0143ceec3c69f8b6c2517971a5578fdaa2" @@ -2344,6 +2356,13 @@ chownr@^3.0.0: resolved "https://registry.yarnpkg.com/chownr/-/chownr-3.0.0.tgz#9855e64ecd240a9cc4267ce8a4aa5d24a1da15e4" integrity sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g== +class-variance-authority@0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/class-variance-authority/-/class-variance-authority-0.7.1.tgz#4008a798a0e4553a781a57ac5177c9fb5d043787" + integrity sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg== + dependencies: + clsx "^2.1.1" + clean-stack@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" @@ -2402,6 +2421,11 @@ clone@^1.0.2: resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg== +clsx@2.1.1, clsx@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999" + integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== + color-convert@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" @@ -4113,6 +4137,11 @@ lru-cache@^5.1.1: dependencies: yallist "^3.0.2" +lucide-react@0.534.0: + version "0.534.0" + resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.534.0.tgz#87b5083e20f97af294d91de43c7d1fbd049ed359" + integrity sha512-4Bz7rujQ/mXHqCwjx09ih/Q9SCizz9CjBV5repw9YSHZZZaop9/Oj0RgCDt6WdEaeAPfbcZ8l2b4jzApStqgNw== + magic-string@^0.30.17: version "0.30.17" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.17.tgz#450a449673d2460e5bbcfba9a61916a1714c7453" @@ -4228,6 +4257,11 @@ next-intl@^4.3.4: negotiator "^1.0.0" use-intl "^4.3.4" +next-themes@^0.4.6: + version "0.4.6" + resolved "https://registry.yarnpkg.com/next-themes/-/next-themes-0.4.6.tgz#8d7e92d03b8fea6582892a50a928c9b23502e8b6" + integrity sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA== + next@15.4.5: version "15.4.5" resolved "https://registry.yarnpkg.com/next/-/next-15.4.5.tgz#88bd5dae80a9d9efcfd8e2bd7d444ae3b9fec407" @@ -5204,6 +5238,11 @@ sync-fetch@0.6.0-2: timeout-signal "^2.0.0" whatwg-mimetype "^4.0.0" +tailwind-merge@3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-3.3.1.tgz#a7e7db7c714f6020319e626ecfb7e7dac8393a4b" + integrity sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g== + tailwindcss@4.1.11: version "4.1.11" resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-4.1.11.tgz#799af3e98c19c5baaefafc6e0c16304a0e684854" @@ -5312,6 +5351,11 @@ tslib@~2.6.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== +tw-animate-css@1.3.6: + version "1.3.6" + resolved "https://registry.yarnpkg.com/tw-animate-css/-/tw-animate-css-1.3.6.tgz#55e4a672445bfd3e9c17e808bec154ba180635b3" + integrity sha512-9dy0R9UsYEGmgf26L8UcHiLmSFTHa9+D7+dAt/G/sF5dCnPePZbfgDYinc7/UzAM7g/baVrmS6m9yEpU46d+LA== + type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"