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"