img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardAction({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardContent({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+export {
+ Card,
+ CardHeader,
+ CardFooter,
+ CardTitle,
+ CardAction,
+ CardDescription,
+ CardContent,
+}
diff --git a/frontend/src/components/ui/checkbox.tsx b/frontend/src/components/ui/checkbox.tsx
new file mode 100644
index 00000000..824b50f9
--- /dev/null
+++ b/frontend/src/components/ui/checkbox.tsx
@@ -0,0 +1,27 @@
+import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox"
+
+import { cn } from "@/lib/utils"
+import { CheckIcon } from "lucide-react"
+
+function Checkbox({ className, ...props }: CheckboxPrimitive.Root.Props) {
+ return (
+
+
+
+
+
+ )
+}
+
+export { Checkbox }
diff --git a/frontend/src/components/ui/dialog.tsx b/frontend/src/components/ui/dialog.tsx
new file mode 100644
index 00000000..014f5aa3
--- /dev/null
+++ b/frontend/src/components/ui/dialog.tsx
@@ -0,0 +1,160 @@
+"use client"
+
+import * as React from "react"
+import { Dialog as DialogPrimitive } from "@base-ui/react/dialog"
+
+import { cn } from "@/lib/utils"
+import { Button } from "@/components/ui/button"
+import { XIcon } from "lucide-react"
+
+function Dialog({ ...props }: DialogPrimitive.Root.Props) {
+ return
+}
+
+function DialogTrigger({ ...props }: DialogPrimitive.Trigger.Props) {
+ return
+}
+
+function DialogPortal({ ...props }: DialogPrimitive.Portal.Props) {
+ return
+}
+
+function DialogClose({ ...props }: DialogPrimitive.Close.Props) {
+ return
+}
+
+function DialogOverlay({
+ className,
+ ...props
+}: DialogPrimitive.Backdrop.Props) {
+ return (
+
+ )
+}
+
+function DialogContent({
+ className,
+ children,
+ showCloseButton = true,
+ ...props
+}: DialogPrimitive.Popup.Props & {
+ showCloseButton?: boolean
+}) {
+ return (
+
+
+
+ {children}
+ {showCloseButton && (
+
+ }
+ >
+
+ Close
+
+ )}
+
+
+ )
+}
+
+function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function DialogFooter({
+ className,
+ showCloseButton = false,
+ children,
+ ...props
+}: React.ComponentProps<"div"> & {
+ showCloseButton?: boolean
+}) {
+ return (
+
+ {children}
+ {showCloseButton && (
+ }>
+ Close
+
+ )}
+
+ )
+}
+
+function DialogTitle({ className, ...props }: DialogPrimitive.Title.Props) {
+ return (
+
+ )
+}
+
+function DialogDescription({
+ className,
+ ...props
+}: DialogPrimitive.Description.Props) {
+ return (
+
+ )
+}
+
+export {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogOverlay,
+ DialogPortal,
+ DialogTitle,
+ DialogTrigger,
+}
diff --git a/frontend/src/components/ui/radio-group.tsx b/frontend/src/components/ui/radio-group.tsx
new file mode 100644
index 00000000..3172cf3a
--- /dev/null
+++ b/frontend/src/components/ui/radio-group.tsx
@@ -0,0 +1,38 @@
+"use client"
+
+import { Radio as RadioPrimitive } from "@base-ui/react/radio"
+import { RadioGroup as RadioGroupPrimitive } from "@base-ui/react/radio-group"
+
+import { cn } from "@/lib/utils"
+
+function RadioGroup({ className, ...props }: RadioGroupPrimitive.Props) {
+ return (
+
+ )
+}
+
+function RadioGroupItem({ className, ...props }: RadioPrimitive.Root.Props) {
+ return (
+
+
+
+
+
+ )
+}
+
+export { RadioGroup, RadioGroupItem }
diff --git a/frontend/src/components/ui/sonner.tsx b/frontend/src/components/ui/sonner.tsx
new file mode 100644
index 00000000..9772eb20
--- /dev/null
+++ b/frontend/src/components/ui/sonner.tsx
@@ -0,0 +1,47 @@
+import { useTheme } from "next-themes"
+import { Toaster as Sonner, type ToasterProps } from "sonner"
+import { CircleCheckIcon, InfoIcon, TriangleAlertIcon, OctagonXIcon, Loader2Icon } from "lucide-react"
+
+const Toaster = ({ ...props }: ToasterProps) => {
+ const { theme = "system" } = useTheme()
+
+ return (
+
+ ),
+ info: (
+
+ ),
+ warning: (
+
+ ),
+ error: (
+
+ ),
+ loading: (
+
+ ),
+ }}
+ style={
+ {
+ "--normal-bg": "var(--popover)",
+ "--normal-text": "var(--popover-foreground)",
+ "--normal-border": "var(--border)",
+ "--border-radius": "var(--radius)",
+ } as React.CSSProperties
+ }
+ toastOptions={{
+ classNames: {
+ toast: "cn-toast",
+ },
+ }}
+ {...props}
+ />
+ )
+}
+
+export { Toaster }
diff --git a/frontend/src/components/ui/textarea.tsx b/frontend/src/components/ui/textarea.tsx
new file mode 100644
index 00000000..04d27f7d
--- /dev/null
+++ b/frontend/src/components/ui/textarea.tsx
@@ -0,0 +1,18 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
+ return (
+
+ )
+}
+
+export { Textarea }
diff --git a/frontend/src/index.css b/frontend/src/index.css
new file mode 100644
index 00000000..40c6263b
--- /dev/null
+++ b/frontend/src/index.css
@@ -0,0 +1,89 @@
+@import "tw-animate-css";
+@import "shadcn/tailwind.css";
+@import "@fontsource-variable/geist";
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer base {
+ .theme {
+ --font-heading: var(--font-sans);
+ --font-sans: 'Geist Variable', sans-serif
+ }
+ :root {
+ --background: oklch(1 0 0);
+ --foreground: oklch(0.145 0 0);
+ --card: oklch(1 0 0);
+ --card-foreground: oklch(0.145 0 0);
+ --popover: oklch(1 0 0);
+ --popover-foreground: oklch(0.145 0 0);
+ --primary: oklch(0.205 0 0);
+ --primary-foreground: oklch(0.985 0 0);
+ --secondary: oklch(0.97 0 0);
+ --secondary-foreground: oklch(0.205 0 0);
+ --muted: oklch(0.97 0 0);
+ --muted-foreground: oklch(0.556 0 0);
+ --accent: oklch(0.97 0 0);
+ --accent-foreground: oklch(0.205 0 0);
+ --destructive: oklch(0.577 0.245 27.325);
+ --border: oklch(0.922 0 0);
+ --input: oklch(0.922 0 0);
+ --ring: oklch(0.708 0 0);
+ --chart-1: oklch(0.87 0 0);
+ --chart-2: oklch(0.556 0 0);
+ --chart-3: oklch(0.439 0 0);
+ --chart-4: oklch(0.371 0 0);
+ --chart-5: oklch(0.269 0 0);
+ --radius: 0.625rem;
+ --sidebar: oklch(0.985 0 0);
+ --sidebar-foreground: oklch(0.145 0 0);
+ --sidebar-primary: oklch(0.205 0 0);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.97 0 0);
+ --sidebar-accent-foreground: oklch(0.205 0 0);
+ --sidebar-border: oklch(0.922 0 0);
+ --sidebar-ring: oklch(0.708 0 0)
+ }
+ .dark {
+ --background: oklch(0.145 0 0);
+ --foreground: oklch(0.985 0 0);
+ --card: oklch(0.205 0 0);
+ --card-foreground: oklch(0.985 0 0);
+ --popover: oklch(0.205 0 0);
+ --popover-foreground: oklch(0.985 0 0);
+ --primary: oklch(0.922 0 0);
+ --primary-foreground: oklch(0.205 0 0);
+ --secondary: oklch(0.269 0 0);
+ --secondary-foreground: oklch(0.985 0 0);
+ --muted: oklch(0.269 0 0);
+ --muted-foreground: oklch(0.708 0 0);
+ --accent: oklch(0.269 0 0);
+ --accent-foreground: oklch(0.985 0 0);
+ --destructive: oklch(0.704 0.191 22.216);
+ --border: oklch(1 0 0 / 10%);
+ --input: oklch(1 0 0 / 15%);
+ --ring: oklch(0.556 0 0);
+ --chart-1: oklch(0.87 0 0);
+ --chart-2: oklch(0.556 0 0);
+ --chart-3: oklch(0.439 0 0);
+ --chart-4: oklch(0.371 0 0);
+ --chart-5: oklch(0.269 0 0);
+ --sidebar: oklch(0.205 0 0);
+ --sidebar-foreground: oklch(0.985 0 0);
+ --sidebar-primary: oklch(0.488 0.243 264.376);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.269 0 0);
+ --sidebar-accent-foreground: oklch(0.985 0 0);
+ --sidebar-border: oklch(1 0 0 / 10%);
+ --sidebar-ring: oklch(0.556 0 0)
+ }
+ * {
+ @apply border-border outline-ring/50;
+ }
+ body {
+ @apply bg-background text-foreground;
+ }
+ html {
+ @apply font-sans;
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts
new file mode 100644
index 00000000..bd0c391d
--- /dev/null
+++ b/frontend/src/lib/utils.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/src/main.tsx b/frontend/src/main.tsx
new file mode 100644
index 00000000..a93d054a
--- /dev/null
+++ b/frontend/src/main.tsx
@@ -0,0 +1,10 @@
+import { StrictMode } from "react";
+import { createRoot } from "react-dom/client";
+import { App } from "./App";
+import "./index.css";
+
+createRoot(document.getElementById("root")!).render(
+
+
+
+);
diff --git a/frontend/src/test/setup.ts b/frontend/src/test/setup.ts
new file mode 100644
index 00000000..f149f27a
--- /dev/null
+++ b/frontend/src/test/setup.ts
@@ -0,0 +1 @@
+import "@testing-library/jest-dom/vitest";
diff --git a/frontend/tailwind.config.ts b/frontend/tailwind.config.ts
new file mode 100644
index 00000000..5cfb302c
--- /dev/null
+++ b/frontend/tailwind.config.ts
@@ -0,0 +1,63 @@
+import type { Config } from "tailwindcss";
+
+export default {
+ content: ["./index.html", "./src/**/*.{ts,tsx}"],
+ theme: {
+ extend: {
+ colors: {
+ background: "oklch(var(--background) /
)",
+ foreground: "oklch(var(--foreground) / )",
+ card: {
+ DEFAULT: "oklch(var(--card) / )",
+ foreground: "oklch(var(--card-foreground) / )",
+ },
+ popover: {
+ DEFAULT: "oklch(var(--popover) / )",
+ foreground: "oklch(var(--popover-foreground) / )",
+ },
+ primary: {
+ DEFAULT: "oklch(var(--primary) / )",
+ foreground: "oklch(var(--primary-foreground) / )",
+ },
+ secondary: {
+ DEFAULT: "oklch(var(--secondary) / )",
+ foreground: "oklch(var(--secondary-foreground) / )",
+ },
+ muted: {
+ DEFAULT: "oklch(var(--muted) / )",
+ foreground: "oklch(var(--muted-foreground) / )",
+ },
+ accent: {
+ DEFAULT: "oklch(var(--accent) / )",
+ foreground: "oklch(var(--accent-foreground) / )",
+ },
+ destructive: {
+ DEFAULT: "oklch(var(--destructive) / )",
+ },
+ border: "oklch(var(--border) / )",
+ input: "oklch(var(--input) / )",
+ ring: "oklch(var(--ring) / )",
+ sidebar: {
+ DEFAULT: "oklch(var(--sidebar) / )",
+ foreground: "oklch(var(--sidebar-foreground) / )",
+ primary: "oklch(var(--sidebar-primary) / )",
+ "primary-foreground": "oklch(var(--sidebar-primary-foreground) / )",
+ accent: "oklch(var(--sidebar-accent) / )",
+ "accent-foreground": "oklch(var(--sidebar-accent-foreground) / )",
+ border: "oklch(var(--sidebar-border) / )",
+ ring: "oklch(var(--sidebar-ring) / )",
+ },
+ },
+ borderRadius: {
+ lg: "var(--radius)",
+ md: "calc(var(--radius) - 2px)",
+ sm: "calc(var(--radius) - 4px)",
+ },
+ fontFamily: {
+ sans: ["var(--font-sans)", "sans-serif"],
+ heading: ["var(--font-heading)", "sans-serif"],
+ },
+ },
+ },
+ plugins: [],
+} satisfies Config;
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
new file mode 100644
index 00000000..4f46cd45
--- /dev/null
+++ b/frontend/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "useDefineForClassFields": true,
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "jsx": "react-jsx",
+ "strict": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "types": ["vitest/globals", "@testing-library/jest-dom"],
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ },
+ "include": ["src"]
+}
diff --git a/frontend/tsconfig.tsbuildinfo b/frontend/tsconfig.tsbuildinfo
new file mode 100644
index 00000000..d8a56118
--- /dev/null
+++ b/frontend/tsconfig.tsbuildinfo
@@ -0,0 +1 @@
+{"root":["./src/app.test.tsx","./src/app.tsx","./src/main.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/checkbox.tsx","./src/components/ui/dialog.tsx","./src/components/ui/radio-group.tsx","./src/components/ui/sonner.tsx","./src/components/ui/textarea.tsx","./src/lib/utils.ts","./src/test/setup.ts"],"version":"5.9.3"}
\ No newline at end of file
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts
new file mode 100644
index 00000000..6109f839
--- /dev/null
+++ b/frontend/vite.config.ts
@@ -0,0 +1,12 @@
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react";
+import path from "path";
+
+export default defineConfig({
+ plugins: [react()],
+ resolve: {
+ alias: {
+ "@": path.resolve(__dirname, "./src"),
+ },
+ },
+});
diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts
new file mode 100644
index 00000000..8f809937
--- /dev/null
+++ b/frontend/vitest.config.ts
@@ -0,0 +1,18 @@
+import { defineConfig } from "vitest/config";
+import react from "@vitejs/plugin-react";
+import path from "path";
+
+export default defineConfig({
+ plugins: [react()],
+ resolve: {
+ alias: {
+ "@": path.resolve(__dirname, "./src"),
+ },
+ },
+ test: {
+ environment: "jsdom",
+ globals: true,
+ setupFiles: ["./src/test/setup.ts"],
+ include: ["src/**/*.test.{ts,tsx}"],
+ },
+});