diff --git a/frontend/.gitignore b/frontend/.gitignore
new file mode 100644
index 0000000..5ef6a52
--- /dev/null
+++ b/frontend/.gitignore
@@ -0,0 +1,41 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.*
+.yarn/*
+!.yarn/patches
+!.yarn/plugins
+!.yarn/releases
+!.yarn/versions
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+.pnpm-debug.log*
+
+# env files (can opt-in for committing if needed)
+.env*
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
diff --git a/frontend/README.md b/frontend/README.md
new file mode 100644
index 0000000..e215bc4
--- /dev/null
+++ b/frontend/README.md
@@ -0,0 +1,36 @@
+This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
+
+## Getting Started
+
+First, run the development server:
+
+```bash
+npm run dev
+# or
+yarn dev
+# or
+pnpm dev
+# or
+bun dev
+```
+
+Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
+
+You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
+
+This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
+
+## Learn More
+
+To learn more about Next.js, take a look at the following resources:
+
+- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
+- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
+
+You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
+
+## Deploy on Vercel
+
+The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
+
+Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
diff --git a/frontend/app/favicon.ico b/frontend/app/favicon.ico
new file mode 100644
index 0000000..718d6fe
Binary files /dev/null and b/frontend/app/favicon.ico differ
diff --git a/frontend/app/globals.css b/frontend/app/globals.css
new file mode 100644
index 0000000..854c4f6
--- /dev/null
+++ b/frontend/app/globals.css
@@ -0,0 +1,105 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer base {
+ :root {
+ --background: 0 0% 100%;
+ --foreground: 240 10% 3.9%;
+ --card: 0 0% 100%;
+ --card-foreground: 240 10% 3.9%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 240 10% 3.9%;
+ --primary: 221.2 83.2% 53.3%;
+ --primary-foreground: 210 40% 98%;
+ --secondary: 210 40% 96.1%;
+ --secondary-foreground: 222.2 47.4% 11.2%;
+ --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%;
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 210 40% 98%;
+ --border: 214.3 31.8% 91.4%;
+ --input: 214.3 31.8% 91.4%;
+ --ring: 221.2 83.2% 53.3%;
+ --radius: 0.5rem;
+
+ /* Sidebar specific colors */
+ --sidebar-background: 0 0% 98%;
+ --sidebar-foreground: 240 5.3% 26.1%;
+ --sidebar-primary: 240 5.9% 10%;
+ --sidebar-primary-foreground: 0 0% 98%;
+ --sidebar-accent: 240 4.8% 95.9%;
+ --sidebar-accent-foreground: 240 5.9% 10%;
+ --sidebar-border: 220 13% 91%;
+ --sidebar-ring: 217.2 91.2% 59.8%;
+
+ /* Overlay constraints */
+ --max-overlay-width: calc(100vw - 32px);
+ --max-overlay-height: calc(100vh - 32px);
+ }
+
+ .dark {
+ --background: 240 10% 3.9%;
+ --foreground: 0 0% 98%;
+ --card: 240 10% 3.9%;
+ --card-foreground: 0 0% 98%;
+ --popover: 240 10% 3.9%;
+ --popover-foreground: 0 0% 98%;
+ --primary: 217.2 91.2% 59.8%;
+ --primary-foreground: 222.2 47.4% 11.2%;
+ --secondary: 217.2 32.6% 17.5%;
+ --secondary-foreground: 210 40% 98%;
+ --muted: 217.2 32.6% 17.5%;
+ --muted-foreground: 215 20.2% 65.1%;
+ --accent: 217.2 32.6% 17.5%;
+ --accent-foreground: 210 40% 98%;
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 210 40% 98%;
+ --border: 217.2 32.6% 17.5%;
+ --input: 217.2 32.6% 17.5%;
+ --ring: 224.3 76.3% 48%;
+
+ /* Sidebar specific colors for dark mode */
+ --sidebar-background: 240 5.9% 10%;
+ --sidebar-foreground: 240 4.8% 95.9%;
+ --sidebar-primary: 0 0% 98%;
+ --sidebar-primary-foreground: 240 5.9% 10%;
+ --sidebar-accent: 240 3.7% 15.9%;
+ --sidebar-accent-foreground: 240 4.8% 95.9%;
+ --sidebar-border: 240 3.7% 15.9%;
+ --sidebar-ring: 217.2 91.2% 59.8%;
+ }
+}
+
+@layer base {
+ * {
+ @apply border-border;
+ }
+ body {
+ @apply bg-background text-foreground;
+ }
+}
+
+@layer components {
+ .overlay-container {
+ @apply absolute z-20;
+ max-width: var(--max-overlay-width);
+ max-height: var(--max-overlay-height);
+ }
+
+ .overlay-container[data-minimized="true"] {
+ @apply z-10;
+ }
+
+ .overlay-card {
+ @apply bg-card/95 backdrop-blur-sm border border-border/50 shadow-lg;
+ max-width: 100%;
+ max-height: 100%;
+ }
+
+ .overlay-minimized {
+ @apply w-[200px] h-auto;
+ }
+}
diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx
new file mode 100644
index 0000000..17b2ce8
--- /dev/null
+++ b/frontend/app/layout.tsx
@@ -0,0 +1,20 @@
+import type { Metadata } from 'next'
+import './globals.css'
+
+export const metadata: Metadata = {
+ title: 'v0 App',
+ description: 'Created with v0',
+ generator: 'v0.dev',
+}
+
+export default function RootLayout({
+ children,
+}: Readonly<{
+ children: React.ReactNode
+}>) {
+ return (
+
+
{children}
+
+ )
+}
diff --git a/frontend/app/map/components/analytics-overlay.tsx b/frontend/app/map/components/analytics-overlay.tsx
new file mode 100644
index 0000000..f5abbbc
--- /dev/null
+++ b/frontend/app/map/components/analytics-overlay.tsx
@@ -0,0 +1,114 @@
+"use client"
+
+import { LineChart, Wind, Droplets, Sparkles, Bot } from "lucide-react"
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
+import { AreaChart } from "./area-chart"
+import { Overlay } from "./overlay-system/overlay"
+import { useOverlay } from "./overlay-system/overlay-context"
+
+export function AnalyticsOverlay() {
+ const { toggleOverlay } = useOverlay()
+
+ const handleChatClick = () => {
+ toggleOverlay("chat")
+ }
+
+ return (
+ }
+ initialPosition="top-right"
+ initialIsOpen={true}
+ width="350px"
+ >
+
+
+
+
Information in radius will be analyzed
+
+
+
+
+
+ Area Price History
+
+
+ Overall Price History of this area
+
+
+
+
+
+
+
+
+
+
+ Price Prediction
+
+
+ The estimated price based on various factors.
+
+
+
+
+
+
+
+
+
+
+
+ Flood Factor
+
+
+
+
+
+
+
+
+
+ Air Factor
+
+
+
+
+
+
+
+
+
+
+ Chat With AI
+
+ Want to ask specific question?
+
+
+
+
+
+
+ )
+}
+
diff --git a/frontend/app/map/components/analytics-panel.tsx b/frontend/app/map/components/analytics-panel.tsx
new file mode 100644
index 0000000..7dbb51a
--- /dev/null
+++ b/frontend/app/map/components/analytics-panel.tsx
@@ -0,0 +1,143 @@
+"use client"
+
+import { Bot, LineChart, Wind, Droplets, Sparkles, Maximize2, Minimize2, ArrowLeftRight } from "lucide-react"
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
+import { AreaChart } from "./area-chart"
+import { Button } from "@/components/ui/button"
+import { useOverlayContext } from "./overlay-context"
+import { ScrollArea } from "@/components/ui/scroll-area"
+
+interface AnalyticsPanelProps {
+ onChatClick: () => void
+}
+
+export function AnalyticsPanel({ onChatClick }: AnalyticsPanelProps) {
+ const { overlays, minimizeOverlay, maximizeOverlay, changePosition } = useOverlayContext()
+ const isMinimized = overlays.analytics.minimized
+ const position = overlays.analytics.position
+
+ if (isMinimized) {
+ return (
+
+
+ Analytics
+ maximizeOverlay("analytics")}>
+
+
+
+
+ )
+ }
+
+ return (
+
+
+
+
+ Analytics
+
+
+
changePosition("analytics", position === "right" ? "left" : "right")}
+ title={`Move to ${position === "right" ? "left" : "right"}`}
+ >
+
+
+
minimizeOverlay("analytics")}>
+
+
+
+
+
+
+
+
Information in radius will be analyzed
+
+
+
+
+
+ Area Price History
+
+
+ Overall Price History of this area
+
+
+
+
+
+
+
+
+
+
+ Price Prediction
+
+
+ The estimated price based on various factors.
+
+
+
+
+
+
+
+
+
+
+
+ Flood Factor
+
+
+
+
+
+
+
+
+
+ Air Factor
+
+
+
+
+
+
+
+
+
+
+ Chat With AI
+
+ Want to ask specific question?
+
+
+
+
+
+
+ )
+}
+
diff --git a/frontend/app/map/components/area-chart.tsx b/frontend/app/map/components/area-chart.tsx
new file mode 100644
index 0000000..ee04e2a
--- /dev/null
+++ b/frontend/app/map/components/area-chart.tsx
@@ -0,0 +1,65 @@
+"use client"
+
+import { LineChart, Line, ResponsiveContainer, XAxis, YAxis, Tooltip } from "@/components/ui/chart"
+import { useTheme } from "next-themes"
+
+interface AreaChartProps {
+ data: number[]
+ color: string
+}
+
+export function AreaChart({ data, color }: AreaChartProps) {
+ const { theme } = useTheme()
+ const isDark = theme === "dark"
+
+ // Generate labels (months)
+ const labels = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul"]
+
+ // Format the data for the chart
+ const chartData = data.map((value, index) => ({
+ name: labels[index],
+ value: value,
+ }))
+
+ // Format the price for display
+ const formatPrice = (value: number) => {
+ return new Intl.NumberFormat("th-TH", {
+ style: "currency",
+ currency: "THB",
+ minimumFractionDigits: 0,
+ maximumFractionDigits: 0,
+ }).format(value)
+ }
+
+ return (
+
+
+
+
+ dataMin * 0.95, (dataMax: number) => dataMax * 1.05]} />
+ [formatPrice(value), "Price"]}
+ contentStyle={{
+ backgroundColor: isDark ? "#1f2937" : "white",
+ borderRadius: "0.375rem",
+ border: isDark ? "1px solid #374151" : "1px solid #e2e8f0",
+ fontSize: "0.75rem",
+ color: isDark ? "#e5e7eb" : "#1f2937",
+ }}
+ />
+
+
+
+
+ )
+}
+
diff --git a/frontend/app/map/components/chat-bot.tsx b/frontend/app/map/components/chat-bot.tsx
new file mode 100644
index 0000000..4feccea
--- /dev/null
+++ b/frontend/app/map/components/chat-bot.tsx
@@ -0,0 +1,109 @@
+"use client"
+
+import { useState } from "react"
+import { Send, X, Minimize2, Maximize2 } from "lucide-react"
+import { Button } from "@/components/ui/button"
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
+import { Input } from "@/components/ui/input"
+import { useOverlayContext } from "./overlay-context"
+import { ScrollArea } from "@/components/ui/scroll-area"
+
+interface ChatBotProps {
+ onClose: () => void
+}
+
+export function ChatBot({ onClose }: ChatBotProps) {
+ const { overlays, minimizeOverlay, maximizeOverlay } = useOverlayContext()
+ const isMinimized = overlays.chat.minimized
+
+ const [message, setMessage] = useState("")
+ const [chatHistory, setChatHistory] = useState([{ role: "bot", content: "Hi! How can I help you today?" }])
+
+ const handleSendMessage = () => {
+ if (!message.trim()) return
+
+ // Add user message to chat
+ setChatHistory([...chatHistory, { role: "user", content: message }])
+
+ // Simulate bot response (in a real app, this would call an API)
+ setTimeout(() => {
+ setChatHistory((prev) => [
+ ...prev,
+ {
+ role: "bot",
+ content: "I can provide information about this area. What would you like to know?",
+ },
+ ])
+ }, 1000)
+
+ setMessage("")
+ }
+
+ if (isMinimized) {
+ return (
+
+
+ ChatBot
+
+ maximizeOverlay("chat")}>
+
+
+
+
+
+
+
+
+ )
+ }
+
+ return (
+
+
+ ChatBot
+
+ minimizeOverlay("chat")}>
+
+
+
+
+
+
+
+
+
+
+ {chatHistory.map((chat, index) => (
+
+ ))}
+
+
+
+ setMessage(e.target.value)}
+ placeholder="Type your message..."
+ className="flex-1"
+ onKeyDown={(e) => {
+ if (e.key === "Enter") {
+ handleSendMessage()
+ }
+ }}
+ />
+
+
+
+
+
+
+ )
+}
+
diff --git a/frontend/app/map/components/chat-overlay.tsx b/frontend/app/map/components/chat-overlay.tsx
new file mode 100644
index 0000000..2178db7
--- /dev/null
+++ b/frontend/app/map/components/chat-overlay.tsx
@@ -0,0 +1,78 @@
+"use client"
+
+import { useState } from "react"
+import { Send, MessageCircle } from "lucide-react"
+import { Button } from "@/components/ui/button"
+import { Input } from "@/components/ui/input"
+import { Overlay } from "./overlay-system/overlay"
+
+export function ChatOverlay() {
+ const [message, setMessage] = useState("")
+ const [chatHistory, setChatHistory] = useState([{ role: "bot", content: "Hi! How can I help you today?" }])
+
+ const handleSendMessage = () => {
+ if (!message.trim()) return
+
+ // Add user message to chat
+ setChatHistory([...chatHistory, { role: "user", content: message }])
+
+ // Simulate bot response (in a real app, this would call an API)
+ setTimeout(() => {
+ setChatHistory((prev) => [
+ ...prev,
+ {
+ role: "bot",
+ content: "I can provide information about this area. What would you like to know?",
+ },
+ ])
+ }, 1000)
+
+ setMessage("")
+ }
+
+ return (
+ }
+ initialPosition="bottom-right"
+ initialIsOpen={false}
+ width="400px"
+ >
+
+
+
+ {chatHistory.map((chat, index) => (
+
+ ))}
+
+
+
+ setMessage(e.target.value)}
+ placeholder="Type your message..."
+ className="flex-1"
+ onKeyDown={(e) => {
+ if (e.key === "Enter") {
+ handleSendMessage()
+ }
+ }}
+ />
+
+
+
+
+
+
+ )
+}
+
diff --git a/frontend/app/map/components/filters-overlay.tsx b/frontend/app/map/components/filters-overlay.tsx
new file mode 100644
index 0000000..852de42
--- /dev/null
+++ b/frontend/app/map/components/filters-overlay.tsx
@@ -0,0 +1,142 @@
+"use client"
+
+import { useState } from "react"
+import { Filter } from "lucide-react"
+import { Button } from "@/components/ui/button"
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
+import { Slider } from "@/components/ui/slider"
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
+import { Switch } from "@/components/ui/switch"
+import { Label } from "@/components/ui/label"
+import { Overlay } from "./overlay-system/overlay"
+
+export function FiltersOverlay() {
+ const [area, setArea] = useState("< 30 km")
+ const [timePeriod, setTimePeriod] = useState("All Time")
+ const [propertyType, setPropertyType] = useState("House")
+ const [priceRange, setPriceRange] = useState([5000000, 20000000])
+ const [activeTab, setActiveTab] = useState("basic")
+
+ return (
+ }
+ initialPosition="bottom-left"
+ initialIsOpen={true}
+ width="350px"
+ >
+
+
+
+ Basic
+ Advanced
+
+
+
+
+
+ Area Radius
+
+
+
+
+
+ {"< 10 km"}
+ {"< 20 km"}
+ {"< 30 km"}
+ {"< 50 km"}
+
+
+
+
+ Time Period
+
+
+
+
+
+ Last Month
+ Last 3 Months
+ Last Year
+ All Time
+
+
+
+
+
+ Property Type
+
+
+
+
+
+ House
+ Condo
+ Townhouse
+ Land
+ Commercial
+
+
+
+
+
+
+
+
+
+ Price Range
+
+ {new Intl.NumberFormat("th-TH", {
+ style: "currency",
+ currency: "THB",
+ minimumFractionDigits: 0,
+ maximumFractionDigits: 0,
+ }).format(priceRange[0])}{" "}
+ -{" "}
+ {new Intl.NumberFormat("th-TH", {
+ style: "currency",
+ currency: "THB",
+ minimumFractionDigits: 0,
+ maximumFractionDigits: 0,
+ }).format(priceRange[1])}
+
+
+
+
+
+
+
Environmental Factors
+
+
+
+ Low Flood Risk
+
+
+
+
+
+ Good Air Quality
+
+
+
+
+
+ Low Noise Pollution
+
+
+
+
+
+
+
+
+
+
+ Apply Filters
+
+
+
+ )
+}
+
diff --git a/frontend/app/map/components/map-container.tsx b/frontend/app/map/components/map-container.tsx
new file mode 100644
index 0000000..593a0bd
--- /dev/null
+++ b/frontend/app/map/components/map-container.tsx
@@ -0,0 +1,45 @@
+"use client"
+
+import { useEffect, useRef } from "react"
+
+interface MapContainerProps {
+ selectedLocation: {
+ lat: number
+ lng: number
+ name?: string
+ }
+}
+
+export function MapContainer({ selectedLocation }: MapContainerProps) {
+ const mapRef = useRef(null)
+
+ useEffect(() => {
+ // This is a placeholder for actual map integration
+ // In a real application, you would use a library like Google Maps, Mapbox, or Leaflet
+ const mapElement = mapRef.current
+
+ if (mapElement) {
+ // Simulate map loading with a background image
+ mapElement.style.backgroundImage = "url('/placeholder.svg?height=800&width=1200')"
+ mapElement.style.backgroundSize = "cover"
+ mapElement.style.backgroundPosition = "center"
+ }
+
+ // Clean up function
+ return () => {
+ if (mapElement) {
+ mapElement.style.backgroundImage = ""
+ }
+ }
+ }, [selectedLocation])
+
+ return (
+
+ {/* Map markers would be rendered here in a real implementation */}
+
+
+ )
+}
+
diff --git a/frontend/app/map/components/map-header.tsx b/frontend/app/map/components/map-header.tsx
new file mode 100644
index 0000000..ab0254c
--- /dev/null
+++ b/frontend/app/map/components/map-header.tsx
@@ -0,0 +1,32 @@
+"use client"
+
+import { ChevronRight } from "lucide-react"
+import Link from "next/link"
+import { Button } from "@/components/ui/button"
+import { ThemeToggle } from "@/components/theme-toggle"
+
+export function MapHeader() {
+ return (
+
+ )
+}
+
diff --git a/frontend/app/map/components/map-sidebar.tsx b/frontend/app/map/components/map-sidebar.tsx
new file mode 100644
index 0000000..7e938c0
--- /dev/null
+++ b/frontend/app/map/components/map-sidebar.tsx
@@ -0,0 +1,127 @@
+"use client"
+
+import type React from "react"
+
+import {
+ Home,
+ Clock,
+ Map,
+ FileText,
+ Settings,
+ PenTool,
+ BarChart3,
+ Plane,
+ LineChart,
+ DollarSign,
+ MoreHorizontal,
+} from "lucide-react"
+import Link from "next/link"
+import { cn } from "@/lib/utils"
+import { usePathname } from "next/navigation"
+
+export function MapSidebar() {
+ const pathname = usePathname()
+
+ const mainNavItems = [
+ { name: "Home", icon: Home, href: "/" },
+ { name: "My assets", icon: Clock, href: "/assets" },
+ { name: "Models", icon: Map, href: "/models" },
+ { name: "Trade", icon: LineChart, href: "/trade" },
+ { name: "Earn", icon: DollarSign, href: "/earn" },
+ { name: "Documentation", icon: FileText, href: "/documentation", badge: "NEW" },
+ { name: "Pay", icon: Settings, href: "/pay" },
+ { name: "More", icon: MoreHorizontal, href: "/more" },
+ ]
+
+ const projectNavItems = [
+ { name: "Design Engineering", icon: PenTool, href: "/projects/design" },
+ { name: "Sales & Marketing", icon: BarChart3, href: "/projects/sales" },
+ { name: "Travel", icon: Plane, href: "/projects/travel" },
+ ]
+
+ return (
+
+
+
+
+
+ {mainNavItems.map((item) => (
+
+
+
+ {item.name}
+
+ {item.badge && (
+
+ {item.badge}
+
+ )}
+
+ ))}
+
+
+
+
+
+
+
+
+
Get ฿10
+
Invite friends
+
+
+
+
+
+
+
+
+ GG
+
+
+
GG_WPX
+
garfield.wpx@gmail.com
+
+
+
+
+ )
+}
+
+function Gift(props: React.SVGProps) {
+ return (
+
+
+
+
+
+
+
+ )
+}
+
diff --git a/frontend/app/map/components/overlay-context.tsx b/frontend/app/map/components/overlay-context.tsx
new file mode 100644
index 0000000..3383b70
--- /dev/null
+++ b/frontend/app/map/components/overlay-context.tsx
@@ -0,0 +1,94 @@
+"use client"
+
+import { createContext, useContext, useState, type ReactNode } from "react"
+
+type OverlayType = "analytics" | "filters" | "chat"
+type OverlayPosition = "left" | "right"
+
+interface OverlayState {
+ visible: boolean
+ minimized: boolean
+ position: OverlayPosition
+}
+
+interface OverlayContextType {
+ overlays: Record
+ toggleOverlay: (type: OverlayType) => void
+ minimizeOverlay: (type: OverlayType) => void
+ maximizeOverlay: (type: OverlayType) => void
+ changePosition: (type: OverlayType, position: OverlayPosition) => void
+}
+
+const OverlayContext = createContext(undefined)
+
+export function OverlayProvider({ children }: { children: ReactNode }) {
+ const [overlays, setOverlays] = useState>({
+ analytics: { visible: true, minimized: false, position: "right" },
+ filters: { visible: true, minimized: false, position: "left" },
+ chat: { visible: false, minimized: false, position: "right" },
+ })
+
+ const toggleOverlay = (type: OverlayType) => {
+ setOverlays((prev) => ({
+ ...prev,
+ [type]: {
+ ...prev[type],
+ visible: !prev[type].visible,
+ minimized: false,
+ },
+ }))
+ }
+
+ const minimizeOverlay = (type: OverlayType) => {
+ setOverlays((prev) => ({
+ ...prev,
+ [type]: {
+ ...prev[type],
+ minimized: true,
+ },
+ }))
+ }
+
+ const maximizeOverlay = (type: OverlayType) => {
+ setOverlays((prev) => ({
+ ...prev,
+ [type]: {
+ ...prev[type],
+ minimized: false,
+ },
+ }))
+ }
+
+ const changePosition = (type: OverlayType, position: OverlayPosition) => {
+ setOverlays((prev) => ({
+ ...prev,
+ [type]: {
+ ...prev[type],
+ position,
+ },
+ }))
+ }
+
+ return (
+
+ {children}
+
+ )
+}
+
+export function useOverlayContext() {
+ const context = useContext(OverlayContext)
+ if (context === undefined) {
+ throw new Error("useOverlayContext must be used within an OverlayProvider")
+ }
+ return context
+}
+
diff --git a/frontend/app/map/components/overlay-controls.tsx b/frontend/app/map/components/overlay-controls.tsx
new file mode 100644
index 0000000..2d546ec
--- /dev/null
+++ b/frontend/app/map/components/overlay-controls.tsx
@@ -0,0 +1,79 @@
+"use client"
+
+import { MessageCircle, Filter, Layers, ArrowLeftRight } from "lucide-react"
+import { Button } from "@/components/ui/button"
+import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
+import { useOverlayContext } from "./overlay-context"
+
+export function OverlayControls() {
+ const { overlays, toggleOverlay, changePosition } = useOverlayContext()
+
+ return (
+
+
+
+
+ toggleOverlay("analytics")}
+ >
+
+
+
+
+ {overlays.analytics.visible ? "Hide analytics" : "Show analytics"}
+
+
+
+
+
+ toggleOverlay("filters")}
+ >
+
+
+
+ {overlays.filters.visible ? "Hide filters" : "Show filters"}
+
+
+
+
+ changePosition("analytics", overlays.analytics.position === "right" ? "left" : "right")}
+ >
+
+
+
+
+ Move analytics to the {overlays.analytics.position === "right" ? "left" : "right"}
+
+
+
+ {!overlays.chat.visible && (
+
+
+ toggleOverlay("chat")}
+ >
+
+
+
+ Open chat
+
+ )}
+
+
+ )
+}
+
diff --git a/frontend/app/map/components/overlay-manager.tsx b/frontend/app/map/components/overlay-manager.tsx
new file mode 100644
index 0000000..5f96e22
--- /dev/null
+++ b/frontend/app/map/components/overlay-manager.tsx
@@ -0,0 +1,59 @@
+"use client"
+
+import { useOverlayContext } from "./overlay-context"
+import { AnalyticsPanel } from "./analytics-panel"
+import { PropertyFilters } from "./property-filters"
+import { ChatBot } from "./chat-bot"
+import { OverlayControls } from "./overlay-controls"
+
+export function OverlayManager() {
+ const { overlays, toggleOverlay } = useOverlayContext()
+
+ // Function to ensure overlays stay within viewport bounds
+ const getPositionClasses = (position: string, type: string) => {
+ if (position === "right") {
+ return "right-4"
+ } else if (position === "left") {
+ return "left-4"
+ }
+ return type === "analytics" ? "right-4" : "left-4"
+ }
+
+ return (
+ <>
+ {/* Analytics Panel */}
+ {overlays.analytics.visible && (
+
+
toggleOverlay("chat")} />
+
+ )}
+
+ {/* Property Filters */}
+ {overlays.filters.visible && (
+
+ )}
+
+ {/* Chat Bot */}
+ {overlays.chat.visible && (
+
+ toggleOverlay("chat")} />
+
+ )}
+
+ {/* Overlay Controls */}
+
+ >
+ )
+}
+
diff --git a/frontend/app/map/components/overlay-system/overlay-context.tsx b/frontend/app/map/components/overlay-system/overlay-context.tsx
new file mode 100644
index 0000000..0503f5a
--- /dev/null
+++ b/frontend/app/map/components/overlay-system/overlay-context.tsx
@@ -0,0 +1,223 @@
+"use client";
+
+import React, { createContext, useContext, useState, useCallback, useRef, type ReactNode } from "react";
+
+// Define overlay types and positions
+export type OverlayId = string;
+export type OverlayPosition = "top-left" | "top-right" | "bottom-left" | "bottom-right" | "center";
+
+// Interface for overlay state
+export interface OverlayState {
+ id: OverlayId;
+ isOpen: boolean;
+ isMinimized: boolean;
+ position: OverlayPosition;
+ zIndex: number;
+ title: string;
+ icon?: React.ReactNode;
+}
+
+// Interface for the overlay context
+interface OverlayContextType {
+ overlays: Record;
+ registerOverlay: (id: OverlayId, initialState: Partial) => void;
+ unregisterOverlay: (id: OverlayId) => void;
+ openOverlay: (id: OverlayId) => void;
+ closeOverlay: (id: OverlayId) => void;
+ toggleOverlay: (id: OverlayId) => void;
+ minimizeOverlay: (id: OverlayId) => void;
+ maximizeOverlay: (id: OverlayId) => void;
+ setPosition: (id: OverlayId, position: OverlayPosition) => void;
+ bringToFront: (id: OverlayId) => void;
+ getNextZIndex: () => number;
+}
+
+// Create the context
+const OverlayContext = createContext(undefined);
+
+// Default values for overlay state
+const defaultOverlayState: Omit = {
+ isOpen: false,
+ isMinimized: false,
+ position: "bottom-right",
+ zIndex: 10,
+};
+
+export function OverlayProvider({ children }: { children: ReactNode }) {
+ const [overlays, setOverlays] = useState>({});
+ const maxZIndexRef = useRef(10);
+
+ // Get the next z-index value using a ref so it doesn't trigger re-renders
+ const getNextZIndex = useCallback(() => {
+ maxZIndexRef.current++;
+ return maxZIndexRef.current;
+ }, []);
+
+ // Register a new overlay
+ const registerOverlay = useCallback((id: OverlayId, initialState: Partial) => {
+ setOverlays((prev) => {
+ if (prev[id]) return prev;
+ return {
+ ...prev,
+ [id]: {
+ ...defaultOverlayState,
+ id,
+ title: id,
+ ...initialState,
+ },
+ };
+ });
+ }, []);
+
+ // Unregister an overlay
+ const unregisterOverlay = useCallback((id: OverlayId) => {
+ setOverlays((prev) => {
+ const newOverlays = { ...prev };
+ delete newOverlays[id];
+ return newOverlays;
+ });
+ }, []);
+
+ // Open an overlay
+ const openOverlay = useCallback(
+ (id: OverlayId) => {
+ setOverlays((prev) => {
+ if (!prev[id]) return prev;
+ return {
+ ...prev,
+ [id]: {
+ ...prev[id],
+ isOpen: true,
+ isMinimized: false,
+ zIndex: getNextZIndex(),
+ },
+ };
+ });
+ },
+ [getNextZIndex]
+ );
+
+ // Close an overlay
+ const closeOverlay = useCallback((id: OverlayId) => {
+ setOverlays((prev) => {
+ if (!prev[id]) return prev;
+ return {
+ ...prev,
+ [id]: {
+ ...prev[id],
+ isOpen: false,
+ },
+ };
+ });
+ }, []);
+
+ // Toggle an overlay
+ const toggleOverlay = useCallback(
+ (id: OverlayId) => {
+ setOverlays((prev) => {
+ if (!prev[id]) return prev;
+ const newState = {
+ ...prev[id],
+ isOpen: !prev[id].isOpen,
+ };
+ if (newState.isOpen) {
+ newState.isMinimized = false;
+ newState.zIndex = getNextZIndex();
+ }
+ return {
+ ...prev,
+ [id]: newState,
+ };
+ });
+ },
+ [getNextZIndex]
+ );
+
+ // Minimize an overlay
+ const minimizeOverlay = useCallback((id: OverlayId) => {
+ setOverlays((prev) => {
+ if (!prev[id]) return prev;
+ return {
+ ...prev,
+ [id]: {
+ ...prev[id],
+ isMinimized: true,
+ },
+ };
+ });
+ }, []);
+
+ // Maximize an overlay
+ const maximizeOverlay = useCallback(
+ (id: OverlayId) => {
+ setOverlays((prev) => {
+ if (!prev[id]) return prev;
+ return {
+ ...prev,
+ [id]: {
+ ...prev[id],
+ isMinimized: false,
+ zIndex: getNextZIndex(),
+ },
+ };
+ });
+ },
+ [getNextZIndex]
+ );
+
+ // Set the position of an overlay
+ const setPosition = useCallback((id: OverlayId, position: OverlayPosition) => {
+ setOverlays((prev) => {
+ if (!prev[id]) return prev;
+ return {
+ ...prev,
+ [id]: {
+ ...prev[id],
+ position,
+ },
+ };
+ });
+ }, []);
+
+ // Bring an overlay to the front
+ const bringToFront = useCallback(
+ (id: OverlayId) => {
+ setOverlays((prev) => {
+ if (!prev[id]) return prev;
+ return {
+ ...prev,
+ [id]: {
+ ...prev[id],
+ zIndex: getNextZIndex(),
+ },
+ };
+ });
+ },
+ [getNextZIndex]
+ );
+
+ const value = {
+ overlays,
+ registerOverlay,
+ unregisterOverlay,
+ openOverlay,
+ closeOverlay,
+ toggleOverlay,
+ minimizeOverlay,
+ maximizeOverlay,
+ setPosition,
+ bringToFront,
+ getNextZIndex,
+ };
+
+ return {children} ;
+}
+
+export function useOverlay() {
+ const context = useContext(OverlayContext);
+ if (context === undefined) {
+ throw new Error("useOverlay must be used within an OverlayProvider");
+ }
+ return context;
+}
+
diff --git a/frontend/app/map/components/overlay-system/overlay-dock.tsx b/frontend/app/map/components/overlay-system/overlay-dock.tsx
new file mode 100644
index 0000000..281862d
--- /dev/null
+++ b/frontend/app/map/components/overlay-system/overlay-dock.tsx
@@ -0,0 +1,50 @@
+"use client"
+import { Button } from "@/components/ui/button"
+import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
+import { useOverlay } from "./overlay-context"
+
+interface OverlayDockProps {
+ position?: "bottom" | "right"
+ className?: string
+}
+
+export function OverlayDock({ position = "bottom", className }: OverlayDockProps) {
+ const { overlays, toggleOverlay } = useOverlay()
+
+ // Filter overlays that have icons
+ const overlaysWithIcons = Object.values(overlays).filter((overlay) => overlay.icon)
+
+ if (overlaysWithIcons.length === 0) return null
+
+ const positionClasses = {
+ bottom: "fixed bottom-4 left-1/2 -translate-x-1/2 flex flex-row gap-2 z-50",
+ right: "fixed right-4 top-1/2 -translate-y-1/2 flex flex-col gap-2 z-50",
+ }
+
+ return (
+
+
+ {overlaysWithIcons.map((overlay) => (
+
+
+
+ toggleOverlay(overlay.id)}
+ >
+ {overlay.icon}
+
+
+
+ {overlay.isOpen ? `Hide ${overlay.title}` : `Show ${overlay.title}`}
+
+
+
+ ))}
+
+
+ )
+}
+
diff --git a/frontend/app/map/components/overlay-system/overlay.tsx b/frontend/app/map/components/overlay-system/overlay.tsx
new file mode 100644
index 0000000..80ee07e
--- /dev/null
+++ b/frontend/app/map/components/overlay-system/overlay.tsx
@@ -0,0 +1,156 @@
+"use client"
+
+import type React from "react"
+import { useEffect, useState, useRef } from "react"
+import { X, Minimize2, Maximize2, Move } from "lucide-react"
+import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"
+import { Button } from "@/components/ui/button"
+import { cn } from "@/lib/utils"
+import { useOverlay, type OverlayId, type OverlayPosition } from "./overlay-context"
+
+interface OverlayProps {
+ id: OverlayId
+ title: string
+ icon?: React.ReactNode
+ initialPosition?: OverlayPosition
+ initialIsOpen?: boolean
+ className?: string
+ children: React.ReactNode
+ onClose?: () => void
+ showMinimize?: boolean
+ width?: string
+ height?: string
+ maxHeight?: string
+}
+
+export function Overlay({
+ id,
+ title,
+ icon,
+ initialPosition = "bottom-right",
+ initialIsOpen = false,
+ className,
+ children,
+ onClose,
+ showMinimize = true,
+ width = "350px",
+ height = "auto",
+ maxHeight = "80vh",
+}: OverlayProps) {
+ const { overlays, registerOverlay, unregisterOverlay, closeOverlay, minimizeOverlay, maximizeOverlay, bringToFront } =
+ useOverlay()
+
+ const [isDragging, setIsDragging] = useState(false)
+ const overlayRef = useRef(null)
+
+ // Register overlay on mount
+ useEffect(() => {
+ registerOverlay(id, {
+ title,
+ icon,
+ position: initialPosition,
+ isOpen: initialIsOpen,
+ })
+
+ // Unregister on unmount
+ return () => unregisterOverlay(id)
+ }, [id])
+
+ // Get overlay state
+ const overlay = overlays[id]
+ if (!overlay) return null
+
+ const handleClose = () => {
+ closeOverlay(id)
+ if (onClose) onClose()
+ }
+
+ const handleMinimize = () => {
+ minimizeOverlay(id)
+ }
+
+ const handleMaximize = () => {
+ maximizeOverlay(id)
+ }
+
+ const handleHeaderClick = () => {
+ if (!isDragging) {
+ bringToFront(id)
+ }
+ }
+
+ // Position classes based on position
+ const positionClasses = {
+ "top-left": "top-4 left-4",
+ "top-right": "top-4 right-4",
+ "bottom-left": "bottom-4 left-4",
+ "bottom-right": "bottom-4 right-4",
+ center: "top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2",
+ }
+
+ // If not open, don't render
+ if (!overlay.isOpen) return null
+
+ // Render minimized state
+ if (overlay.isMinimized) {
+ return (
+
+
+
+
+ {icon && {icon} }
+ {title}
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+ }
+
+ // Render full overlay
+ return (
+
+
+
+
+ {icon && {icon} }
+ {title}
+
+
+
+ {showMinimize && (
+
+
+
+ )}
+
+
+
+
+
+ {children}
+
+
+ )
+}
+
diff --git a/frontend/app/map/components/property-filters.tsx b/frontend/app/map/components/property-filters.tsx
new file mode 100644
index 0000000..633e065
--- /dev/null
+++ b/frontend/app/map/components/property-filters.tsx
@@ -0,0 +1,167 @@
+"use client"
+
+import { useState } from "react"
+import { Button } from "@/components/ui/button"
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
+import { Slider } from "@/components/ui/slider"
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
+import { Switch } from "@/components/ui/switch"
+import { Label } from "@/components/ui/label"
+import { Minimize2, Maximize2 } from "lucide-react"
+import { useOverlayContext } from "./overlay-context"
+import { ScrollArea } from "@/components/ui/scroll-area"
+
+export function PropertyFilters() {
+ const { overlays, minimizeOverlay, maximizeOverlay } = useOverlayContext()
+ const isMinimized = overlays.filters.minimized
+
+ const [area, setArea] = useState("< 30 km")
+ const [timePeriod, setTimePeriod] = useState("All Time")
+ const [propertyType, setPropertyType] = useState("House")
+ const [priceRange, setPriceRange] = useState([5000000, 20000000])
+ const [activeTab, setActiveTab] = useState("basic")
+
+ if (isMinimized) {
+ return (
+
+
+ Filters
+ maximizeOverlay("filters")}>
+
+
+
+
+ )
+ }
+
+ return (
+
+
+ Property Filters
+ minimizeOverlay("filters")}>
+
+
+
+
+
+
+
+ Basic
+ Advanced
+
+
+
+
+
+ Area Radius
+
+
+
+
+
+ {"< 10 km"}
+ {"< 20 km"}
+ {"< 30 km"}
+ {"< 50 km"}
+
+
+
+
+ Time Period
+
+
+
+
+
+ Last Month
+ Last 3 Months
+ Last Year
+ All Time
+
+
+
+
+
+ Property Type
+
+
+
+
+
+ House
+ Condo
+ Townhouse
+ Land
+ Commercial
+
+
+
+
+
+
+
+
+
+ Price Range
+
+ {new Intl.NumberFormat("th-TH", {
+ style: "currency",
+ currency: "THB",
+ minimumFractionDigits: 0,
+ maximumFractionDigits: 0,
+ }).format(priceRange[0])}{" "}
+ -{" "}
+ {new Intl.NumberFormat("th-TH", {
+ style: "currency",
+ currency: "THB",
+ minimumFractionDigits: 0,
+ maximumFractionDigits: 0,
+ }).format(priceRange[1])}
+
+
+
+
+
+
+
Environmental Factors
+
+
+
+ Low Flood Risk
+
+
+
+
+
+ Good Air Quality
+
+
+
+
+
+ Low Noise Pollution
+
+
+
+
+
+
+
+
+
+
+ Apply Filters
+
+
+
+
+ )
+}
+
diff --git a/frontend/app/map/layout.tsx b/frontend/app/map/layout.tsx
new file mode 100644
index 0000000..84e2591
--- /dev/null
+++ b/frontend/app/map/layout.tsx
@@ -0,0 +1,10 @@
+import type React from "react"
+
+export default function MapLayout({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ )
+}
+
diff --git a/frontend/app/map/page.tsx b/frontend/app/map/page.tsx
new file mode 100644
index 0000000..97ea8f9
--- /dev/null
+++ b/frontend/app/map/page.tsx
@@ -0,0 +1,71 @@
+"use client"
+
+import { useState } from "react"
+import { MapContainer } from "./components/map-container"
+import { MapSidebar } from "./components/map-sidebar"
+import { MapHeader } from "./components/map-header"
+import { SidebarProvider } from "@/components/ui/sidebar"
+import { ThemeProvider } from "@/components/theme-provider"
+import { Button } from "@/components/ui/button"
+import { ArrowRight } from "lucide-react"
+import Link from "next/link"
+import { OverlayProvider } from "./components/overlay-system/overlay-context"
+import { OverlayDock } from "./components/overlay-system/overlay-dock"
+import { AnalyticsOverlay } from "./components/analytics-overlay"
+import { FiltersOverlay } from "./components/filters-overlay"
+import { ChatOverlay } from "./components/chat-overlay"
+import { ThemeController } from "@/components/theme-controller"
+
+export default function MapPage() {
+ const [selectedLocation, setSelectedLocation] = useState<{
+ lat: number
+ lng: number
+ name?: string
+ }>({
+ lat: 13.7563,
+ lng: 100.5018,
+ name: "Bangkok",
+ })
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ {/* Prediction model banner */}
+
+
+
+
Price Prediction: 15,000,000 ฿
+
Based on our AI model analysis
+
+
+
+ Explain
+
+
+
+
+
+ {/* Overlay System */}
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
diff --git a/frontend/app/model-explanation/components/feature-importance-chart.tsx b/frontend/app/model-explanation/components/feature-importance-chart.tsx
new file mode 100644
index 0000000..3496371
--- /dev/null
+++ b/frontend/app/model-explanation/components/feature-importance-chart.tsx
@@ -0,0 +1,65 @@
+"use client"
+
+import { useTheme } from "next-themes"
+import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Cell } from "@/components/ui/chart"
+
+interface Feature {
+ name: string
+ importance: number
+ value: string
+ impact: "positive" | "negative" | "neutral"
+}
+
+interface FeatureImportanceChartProps {
+ features: Feature[]
+}
+
+export function FeatureImportanceChart({ features }: FeatureImportanceChartProps) {
+ const { theme } = useTheme()
+ const isDark = theme === "dark"
+
+ // Sort features by importance
+ const sortedFeatures = [...features].sort((a, b) => b.importance - a.importance)
+
+ const getBarColor = (impact: string) => {
+ if (impact === "positive") return "#10b981"
+ if (impact === "negative") return "#ef4444"
+ return "#f59e0b"
+ }
+
+ return (
+
+
+
+ `${value}%`}
+ stroke={isDark ? "#9ca3af" : "#6b7280"}
+ />
+
+ [`${value}%`, "Importance"]}
+ contentStyle={{
+ backgroundColor: isDark ? "#1f2937" : "white",
+ borderRadius: "0.375rem",
+ border: isDark ? "1px solid #374151" : "1px solid #e2e8f0",
+ fontSize: "0.75rem",
+ color: isDark ? "#e5e7eb" : "#1f2937",
+ }}
+ />
+
+ {sortedFeatures.map((entry, index) => (
+ |
+ ))}
+
+
+
+ )
+}
+
diff --git a/frontend/app/model-explanation/components/price-comparison-chart.tsx b/frontend/app/model-explanation/components/price-comparison-chart.tsx
new file mode 100644
index 0000000..f5b4bdd
--- /dev/null
+++ b/frontend/app/model-explanation/components/price-comparison-chart.tsx
@@ -0,0 +1,75 @@
+"use client"
+
+import { useTheme } from "next-themes"
+import {
+ BarChart,
+ Bar,
+ XAxis,
+ YAxis,
+ CartesianGrid,
+ Tooltip,
+ ResponsiveContainer,
+ Legend,
+ Cell,
+} from "@/components/ui/chart"
+
+interface PropertyData {
+ name: string
+ price: number
+ size: number
+ age: number
+}
+
+interface PriceComparisonChartProps {
+ property: PropertyData
+ comparisons: PropertyData[]
+}
+
+export function PriceComparisonChart({ property, comparisons }: PriceComparisonChartProps) {
+ const { theme } = useTheme()
+ const isDark = theme === "dark"
+
+ // Combine property and comparisons
+ const data = [property, ...comparisons]
+
+ // Format the price for display
+ const formatPrice = (value: number) => {
+ return new Intl.NumberFormat("th-TH", {
+ style: "currency",
+ currency: "THB",
+ minimumFractionDigits: 0,
+ maximumFractionDigits: 0,
+ }).format(value)
+ }
+
+ return (
+
+
+
+
+ `${(value / 1000000).toFixed(1)}M`} stroke={isDark ? "#9ca3af" : "#6b7280"} />
+ [formatPrice(value), "Price"]}
+ contentStyle={{
+ backgroundColor: isDark ? "#1f2937" : "white",
+ borderRadius: "0.375rem",
+ border: isDark ? "1px solid #374151" : "1px solid #e2e8f0",
+ fontSize: "0.75rem",
+ color: isDark ? "#e5e7eb" : "#1f2937",
+ }}
+ />
+
+
+ {data.map((entry, index) => (
+ |
+ ))}
+
+
+
+ )
+}
+
diff --git a/frontend/app/model-explanation/page.tsx b/frontend/app/model-explanation/page.tsx
new file mode 100644
index 0000000..471b06b
--- /dev/null
+++ b/frontend/app/model-explanation/page.tsx
@@ -0,0 +1,640 @@
+"use client"
+
+import { useState } from "react"
+import {
+ ChevronRight,
+ Info,
+ ArrowRight,
+ Home,
+ Building,
+ Ruler,
+ Calendar,
+ Coins,
+ Droplets,
+ Wind,
+ Sun,
+ Car,
+ School,
+ ShoppingBag,
+} from "lucide-react"
+import Link from "next/link"
+import { Button } from "@/components/ui/button"
+import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
+import { Progress } from "@/components/ui/progress"
+import { Slider } from "@/components/ui/slider"
+import { FeatureImportanceChart } from "./components/feature-importance-chart"
+import { PriceComparisonChart } from "./components/price-comparison-chart"
+import { MapSidebar } from "../map/components/map-sidebar"
+import { SidebarProvider } from "@/components/ui/sidebar"
+import { ThemeProvider } from "@/components/theme-provider"
+
+export default function ModelExplanationPage() {
+ const [activeStep, setActiveStep] = useState(1)
+ const [propertySize, setPropertySize] = useState(150)
+ const [propertyAge, setPropertyAge] = useState(5)
+
+ // Sample data for the model explanation
+ const propertyDetails = {
+ address: "123 Sukhumvit Road, Bangkok",
+ type: "Condominium",
+ size: 150, // sqm
+ bedrooms: 3,
+ bathrooms: 2,
+ age: 5, // years
+ floor: 15,
+ amenities: ["Swimming Pool", "Gym", "Security", "Parking"],
+ predictedPrice: 15000000, // THB
+ similarProperties: [
+ { address: "125 Sukhumvit Road", price: 14500000, size: 145, age: 6 },
+ { address: "130 Sukhumvit Road", price: 16200000, size: 160, age: 3 },
+ { address: "118 Sukhumvit Road", price: 13800000, size: 140, age: 7 },
+ ],
+ features: [
+ { name: "Location", importance: 35, value: "Prime Area", impact: "positive" },
+ { name: "Size", importance: 25, value: "150 sqm", impact: "positive" },
+ { name: "Age", importance: 15, value: "5 years", impact: "neutral" },
+ { name: "Amenities", importance: 10, value: "4 amenities", impact: "positive" },
+ { name: "Floor", importance: 8, value: "15th floor", impact: "positive" },
+ { name: "Environmental Factors", importance: 7, value: "Low flood risk", impact: "positive" },
+ ],
+ }
+
+ const steps = [
+ {
+ id: 1,
+ title: "Property Details",
+ description: "Basic information about the property",
+ icon: Home,
+ },
+ {
+ id: 2,
+ title: "Feature Analysis",
+ description: "How each feature affects the price",
+ icon: Ruler,
+ },
+ {
+ id: 3,
+ title: "Market Comparison",
+ description: "Comparison with similar properties",
+ icon: Building,
+ },
+ {
+ id: 4,
+ title: "Environmental Factors",
+ description: "Impact of environmental conditions",
+ icon: Droplets,
+ },
+ {
+ id: 5,
+ title: "Final Prediction",
+ description: "The predicted price and confidence level",
+ icon: Coins,
+ },
+ ]
+
+ // Calculate a new price based on slider changes
+ const calculateAdjustedPrice = () => {
+ // Simple formula for demonstration
+ const sizeImpact = (propertySize - 150) * 50000 // 50,000 THB per sqm
+ const ageImpact = (5 - propertyAge) * 200000 // 200,000 THB per year newer
+
+ return propertyDetails.predictedPrice + sizeImpact + ageImpact
+ }
+
+ const adjustedPrice = calculateAdjustedPrice()
+
+ return (
+
+
+
+
+
+ {/* Header */}
+
+
+ {/* Main content */}
+
+
+
+
Explainable Price Prediction Model
+
+ Understand how our AI model predicts property prices and what factors influence the valuation.
+
+
+
+ {/* Steps navigation */}
+
+
+ {steps.map((step) => (
+ setActiveStep(step.id)}
+ >
+
+ {step.title}
+
+ ))}
+
+
+
+
+ {/* Step content */}
+
+ {/* Left column - Property details */}
+
+
+
+ Property Details
+ {propertyDetails.address}
+
+
+
+ Type
+ {propertyDetails.type}
+
+
+ Size
+ {propertySize} sqm
+
+
+ Bedrooms
+ {propertyDetails.bedrooms}
+
+
+ Bathrooms
+ {propertyDetails.bathrooms}
+
+
+ Age
+ {propertyAge} years
+
+
+ Floor
+ {propertyDetails.floor}
+
+
+
+
+
+
+ Adjust Parameters
+ See how changes affect the prediction
+
+
+
+
+ Property Size
+ {propertySize} sqm
+
+
setPropertySize(value[0])}
+ />
+
+
+
+
+ Property Age
+ {propertyAge} years
+
+
setPropertyAge(value[0])}
+ />
+
+
+
+
+
+ Adjusted Price
+
+ {new Intl.NumberFormat("th-TH", {
+ style: "currency",
+ currency: "THB",
+ minimumFractionDigits: 0,
+ maximumFractionDigits: 0,
+ }).format(adjustedPrice)}
+
+
+
+ {adjustedPrice > propertyDetails.predictedPrice ? "↑" : "↓"}
+ {Math.abs(adjustedPrice - propertyDetails.predictedPrice).toLocaleString()} THB from
+ original prediction
+
+
+
+
+
+
+ {/* Middle column - Step content */}
+
+ {activeStep === 1 && (
+
+
+ Property Overview
+ Basic information used in our prediction model
+
+
+
+
+ Our AI model begins by analyzing the core attributes of your property. These fundamental
+ characteristics form the baseline for our prediction.
+
+
+
+
+
+
+
Property Type
+
+ {propertyDetails.type} properties in this area have specific market dynamics
+
+
+
+
+
+
+
+
Size & Layout
+
+ {propertyDetails.size} sqm with {propertyDetails.bedrooms} bedrooms and{" "}
+ {propertyDetails.bathrooms} bathrooms
+
+
+
+
+
+
+
+
Property Age
+
+ Built {propertyDetails.age} years ago, affecting depreciation calculations
+
+
+
+
+
+
+
+
Floor & View
+
+ Located on floor {propertyDetails.floor}, impacting value and desirability
+
+
+
+
+
+
+
+ setActiveStep(2)} className="ml-auto">
+ Next Step
+
+
+
+ )}
+
+ {activeStep === 2 && (
+
+
+ Feature Analysis
+ How different features impact the predicted price
+
+
+
+
+ Our model analyzes various features of your property and determines how each one
+ contributes to the final price prediction. Below is a breakdown of the most important
+ factors.
+
+
+
+
+
+
+
+ {propertyDetails.features.map((feature) => (
+
+
+ {feature.name}
+
+ {feature.impact === "positive"
+ ? "↑ Positive"
+ : feature.impact === "negative"
+ ? "↓ Negative"
+ : "→ Neutral"}{" "}
+ Impact
+
+
+
+
+
{feature.importance}%
+
+
{feature.value}
+
+ ))}
+
+
+
+
+ setActiveStep(1)}>
+ Previous
+
+ setActiveStep(3)}>
+ Next Step
+
+
+
+ )}
+
+ {activeStep === 3 && (
+
+
+ Market Comparison
+
+ How your property compares to similar properties in the area
+
+
+
+
+
+ Our model analyzes recent sales data from similar properties in your area to establish a
+ baseline for comparison. This helps ensure our prediction is aligned with current market
+ conditions.
+
+
+
+
({
+ name: p.address.split(" ")[0],
+ price: p.price,
+ size: p.size,
+ age: p.age,
+ }))}
+ />
+
+
+
+
Similar Properties
+
+ {propertyDetails.similarProperties.map((property, index) => (
+
+
{property.address}
+
+ {property.size} sqm, {property.age} years old
+
+
+ {new Intl.NumberFormat("th-TH", {
+ style: "currency",
+ currency: "THB",
+ minimumFractionDigits: 0,
+ maximumFractionDigits: 0,
+ }).format(property.price)}
+
+
+ ))}
+
+
+
+
+
+ setActiveStep(2)}>
+ Previous
+
+ setActiveStep(4)}>
+ Next Step
+
+
+
+ )}
+
+ {activeStep === 4 && (
+
+
+ Environmental Factors
+ How environmental conditions affect the property value
+
+
+
+
+ Environmental factors can significantly impact property values. Our model considers
+ various environmental conditions to provide a more accurate prediction.
+
+
+
+
+
+
Flood Risk
+
+
+ Historical data shows moderate flood risk in this area
+
+
+
+
+
+
Air Quality
+
+
+ Air quality is below average, affecting property value
+
+
+
+
+
+
Noise Level
+
+
+ The area has relatively low noise pollution
+
+
+
+
+
+
Proximity to Amenities
+
+
+
+
Public Transport: 300m
+
+
+
+
+
+
+
+
+
+ setActiveStep(3)}>
+ Previous
+
+ setActiveStep(5)}>
+ Next Step
+
+
+
+ )}
+
+ {activeStep === 5 && (
+
+
+ Final Prediction
+ The predicted price and confidence level
+
+
+
+
+
Predicted Price
+
+ {new Intl.NumberFormat("th-TH", {
+ style: "currency",
+ currency: "THB",
+ minimumFractionDigits: 0,
+ maximumFractionDigits: 0,
+ }).format(adjustedPrice)}
+
+
Confidence Level: 92%
+
+
+
+
+
+ Price Range
+
+
+ Based on our model's confidence level, the price could range between:
+
+
+
+
Lower Bound
+
+ {new Intl.NumberFormat("th-TH", {
+ style: "currency",
+ currency: "THB",
+ minimumFractionDigits: 0,
+ maximumFractionDigits: 0,
+ }).format(adjustedPrice * 0.95)}
+
+
+
+
Prediction
+
+ {new Intl.NumberFormat("th-TH", {
+ style: "currency",
+ currency: "THB",
+ minimumFractionDigits: 0,
+ maximumFractionDigits: 0,
+ }).format(adjustedPrice)}
+
+
+
+
Upper Bound
+
+ {new Intl.NumberFormat("th-TH", {
+ style: "currency",
+ currency: "THB",
+ minimumFractionDigits: 0,
+ maximumFractionDigits: 0,
+ }).format(adjustedPrice * 1.05)}
+
+
+
+
+
+
+
Summary of Factors
+
+ The final prediction is based on a combination of all factors analyzed in previous
+ steps:
+
+
+
+
+ Property characteristics (size, age, layout)
+
+
+
+ Location and neighborhood analysis
+
+
+
+ Market trends and comparable properties
+
+
+
+ Environmental factors and amenities
+
+
+
+
+
+
+ setActiveStep(4)}>
+ Previous
+
+ (window.location.href = "/map")}>
+ Back to Map
+
+
+
+ )}
+
+
+
+
+
+
+
+
+ )
+}
+
diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx
new file mode 100644
index 0000000..88f0cc9
--- /dev/null
+++ b/frontend/app/page.tsx
@@ -0,0 +1,103 @@
+import Image from "next/image";
+
+export default function Home() {
+ return (
+
+
+
+
+
+ Get started by editing{" "}
+
+ app/page.tsx
+
+ .
+
+
+ Save and see your changes instantly.
+
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/components.json b/frontend/components.json
new file mode 100644
index 0000000..335484f
--- /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": "app/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/components/theme-controller.tsx b/frontend/components/theme-controller.tsx
new file mode 100644
index 0000000..2ffe13d
--- /dev/null
+++ b/frontend/components/theme-controller.tsx
@@ -0,0 +1,147 @@
+"use client"
+
+import { useState, useEffect, useRef, type ReactNode } from "react"
+import { useTheme } from "next-themes"
+import { Sun, Moon, Laptop, Palette, Check } from "lucide-react"
+import { Button } from "@/components/ui/button"
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+ DropdownMenuSeparator,
+ DropdownMenuLabel,
+} from "@/components/ui/dropdown-menu"
+import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
+
+// Define available color schemes
+const colorSchemes = [
+ { name: "Blue", primary: "221.2 83.2% 53.3%" },
+ { name: "Green", primary: "142.1 76.2% 36.3%" },
+ { name: "Purple", primary: "262.1 83.3% 57.8%" },
+ { name: "Orange", primary: "24.6 95% 53.1%" },
+ { name: "Teal", primary: "173 80.4% 40%" },
+]
+
+interface ThemeControllerProps {
+ children: ReactNode
+ defaultColorScheme?: string
+}
+
+export function ThemeController({ children, defaultColorScheme = "Blue" }: ThemeControllerProps) {
+ const { setTheme, theme } = useTheme()
+ const [colorScheme, setColorScheme] = useState(defaultColorScheme)
+ const [overlayBoundaries, setOverlayBoundaries] = useState({ width: 0, height: 0 })
+ const containerRef = useRef(null)
+
+ // Update overlay boundaries when window resizes
+ useEffect(() => {
+ const updateBoundaries = () => {
+ if (containerRef.current) {
+ setOverlayBoundaries({
+ width: containerRef.current.clientWidth,
+ height: containerRef.current.clientHeight,
+ })
+ }
+ }
+
+ // Initial update
+ updateBoundaries()
+
+ // Add resize listener
+ window.addEventListener("resize", updateBoundaries)
+
+ // Cleanup
+ return () => window.removeEventListener("resize", updateBoundaries)
+ }, [])
+
+ // Apply color scheme
+ useEffect(() => {
+ const scheme = colorSchemes.find((s) => s.name === colorScheme)
+ if (scheme) {
+ document.documentElement.style.setProperty("--primary", scheme.primary)
+ }
+ }, [colorScheme])
+
+ // Apply CSS variables for overlay constraints
+ useEffect(() => {
+ document.documentElement.style.setProperty("--max-overlay-width", `${overlayBoundaries.width - 32}px`)
+ document.documentElement.style.setProperty("--max-overlay-height", `${overlayBoundaries.height - 32}px`)
+ }, [overlayBoundaries])
+
+ return (
+
+ {children}
+
+ {/* Theme Controller UI */}
+
+
+
+
+
+
+
+
+
+
+
+ Theme Options
+
+
+ setTheme("light")} className="flex items-center justify-between">
+
+
+ Light
+
+ {theme === "light" && }
+
+
+ setTheme("dark")} className="flex items-center justify-between">
+
+
+ Dark
+
+ {theme === "dark" && }
+
+
+ setTheme("system")} className="flex items-center justify-between">
+
+
+ System
+
+ {theme === "system" && }
+
+
+
+ Color Scheme
+
+ {colorSchemes.map((scheme) => (
+ setColorScheme(scheme.name)}
+ className="flex items-center justify-between"
+ >
+
+ {colorScheme === scheme.name && }
+
+ ))}
+
+
+
+
+ Theme Settings
+
+
+
+
+
+ )
+}
+
diff --git a/frontend/components/theme-provider.tsx b/frontend/components/theme-provider.tsx
new file mode 100644
index 0000000..77cb61d
--- /dev/null
+++ b/frontend/components/theme-provider.tsx
@@ -0,0 +1,12 @@
+"use client"
+import { ThemeProvider as NextThemesProvider } from "next-themes"
+import type { ThemeProviderProps } from "next-themes"
+
+export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
+ return (
+
+ {children}
+
+ )
+}
+
diff --git a/frontend/components/theme-toggle.tsx b/frontend/components/theme-toggle.tsx
new file mode 100644
index 0000000..47408b2
--- /dev/null
+++ b/frontend/components/theme-toggle.tsx
@@ -0,0 +1,47 @@
+"use client"
+
+import { Moon, Sun, Laptop } from "lucide-react"
+import { useTheme } from "next-themes"
+import { Button } from "@/components/ui/button"
+import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
+import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
+
+export function ThemeToggle() {
+ const { setTheme, theme } = useTheme()
+
+ return (
+
+
+
+
+
+
+
+
+ Toggle theme
+
+
+
+ setTheme("light")}>
+
+ Light
+
+ setTheme("dark")}>
+
+ Dark
+
+ setTheme("system")}>
+
+ System
+
+
+
+
+
+ Change theme
+
+
+
+ )
+}
+
diff --git a/frontend/components/ui/accordion.tsx b/frontend/components/ui/accordion.tsx
new file mode 100644
index 0000000..24c788c
--- /dev/null
+++ b/frontend/components/ui/accordion.tsx
@@ -0,0 +1,58 @@
+"use client"
+
+import * as React from "react"
+import * as AccordionPrimitive from "@radix-ui/react-accordion"
+import { ChevronDown } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Accordion = AccordionPrimitive.Root
+
+const AccordionItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AccordionItem.displayName = "AccordionItem"
+
+const AccordionTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ svg]:rotate-180",
+ className
+ )}
+ {...props}
+ >
+ {children}
+
+
+
+))
+AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
+
+const AccordionContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ {children}
+
+))
+
+AccordionContent.displayName = AccordionPrimitive.Content.displayName
+
+export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
diff --git a/frontend/components/ui/alert-dialog.tsx b/frontend/components/ui/alert-dialog.tsx
new file mode 100644
index 0000000..25e7b47
--- /dev/null
+++ b/frontend/components/ui/alert-dialog.tsx
@@ -0,0 +1,141 @@
+"use client"
+
+import * as React from "react"
+import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
+
+import { cn } from "@/lib/utils"
+import { buttonVariants } from "@/components/ui/button"
+
+const AlertDialog = AlertDialogPrimitive.Root
+
+const AlertDialogTrigger = AlertDialogPrimitive.Trigger
+
+const AlertDialogPortal = AlertDialogPrimitive.Portal
+
+const AlertDialogOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
+
+const AlertDialogContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+))
+AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
+
+const AlertDialogHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+AlertDialogHeader.displayName = "AlertDialogHeader"
+
+const AlertDialogFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+AlertDialogFooter.displayName = "AlertDialogFooter"
+
+const AlertDialogTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
+
+const AlertDialogDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogDescription.displayName =
+ AlertDialogPrimitive.Description.displayName
+
+const AlertDialogAction = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
+
+const AlertDialogCancel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
+
+export {
+ AlertDialog,
+ AlertDialogPortal,
+ AlertDialogOverlay,
+ AlertDialogTrigger,
+ AlertDialogContent,
+ AlertDialogHeader,
+ AlertDialogFooter,
+ AlertDialogTitle,
+ AlertDialogDescription,
+ AlertDialogAction,
+ AlertDialogCancel,
+}
diff --git a/frontend/components/ui/alert.tsx b/frontend/components/ui/alert.tsx
new file mode 100644
index 0000000..41fa7e0
--- /dev/null
+++ b/frontend/components/ui/alert.tsx
@@ -0,0 +1,59 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const alertVariants = cva(
+ "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
+ {
+ variants: {
+ variant: {
+ default: "bg-background text-foreground",
+ destructive:
+ "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+const Alert = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes & VariantProps
+>(({ className, variant, ...props }, ref) => (
+
+))
+Alert.displayName = "Alert"
+
+const AlertTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+AlertTitle.displayName = "AlertTitle"
+
+const AlertDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+AlertDescription.displayName = "AlertDescription"
+
+export { Alert, AlertTitle, AlertDescription }
diff --git a/frontend/components/ui/aspect-ratio.tsx b/frontend/components/ui/aspect-ratio.tsx
new file mode 100644
index 0000000..d6a5226
--- /dev/null
+++ b/frontend/components/ui/aspect-ratio.tsx
@@ -0,0 +1,7 @@
+"use client"
+
+import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
+
+const AspectRatio = AspectRatioPrimitive.Root
+
+export { AspectRatio }
diff --git a/frontend/components/ui/avatar.tsx b/frontend/components/ui/avatar.tsx
new file mode 100644
index 0000000..51e507b
--- /dev/null
+++ b/frontend/components/ui/avatar.tsx
@@ -0,0 +1,50 @@
+"use client"
+
+import * as React from "react"
+import * as AvatarPrimitive from "@radix-ui/react-avatar"
+
+import { cn } from "@/lib/utils"
+
+const Avatar = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+Avatar.displayName = AvatarPrimitive.Root.displayName
+
+const AvatarImage = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AvatarImage.displayName = AvatarPrimitive.Image.displayName
+
+const AvatarFallback = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
+
+export { Avatar, AvatarImage, AvatarFallback }
diff --git a/frontend/components/ui/badge.tsx b/frontend/components/ui/badge.tsx
new file mode 100644
index 0000000..f000e3e
--- /dev/null
+++ b/frontend/components/ui/badge.tsx
@@ -0,0 +1,36 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const badgeVariants = cva(
+ "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
+ {
+ variants: {
+ variant: {
+ default:
+ "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
+ secondary:
+ "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ destructive:
+ "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
+ outline: "text-foreground",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+export interface BadgeProps
+ extends React.HTMLAttributes,
+ VariantProps {}
+
+function Badge({ className, variant, ...props }: BadgeProps) {
+ return (
+
+ )
+}
+
+export { Badge, badgeVariants }
diff --git a/frontend/components/ui/breadcrumb.tsx b/frontend/components/ui/breadcrumb.tsx
new file mode 100644
index 0000000..60e6c96
--- /dev/null
+++ b/frontend/components/ui/breadcrumb.tsx
@@ -0,0 +1,115 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { ChevronRight, MoreHorizontal } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Breadcrumb = React.forwardRef<
+ HTMLElement,
+ React.ComponentPropsWithoutRef<"nav"> & {
+ separator?: React.ReactNode
+ }
+>(({ ...props }, ref) => )
+Breadcrumb.displayName = "Breadcrumb"
+
+const BreadcrumbList = React.forwardRef<
+ HTMLOListElement,
+ React.ComponentPropsWithoutRef<"ol">
+>(({ className, ...props }, ref) => (
+
+))
+BreadcrumbList.displayName = "BreadcrumbList"
+
+const BreadcrumbItem = React.forwardRef<
+ HTMLLIElement,
+ React.ComponentPropsWithoutRef<"li">
+>(({ className, ...props }, ref) => (
+
+))
+BreadcrumbItem.displayName = "BreadcrumbItem"
+
+const BreadcrumbLink = React.forwardRef<
+ HTMLAnchorElement,
+ React.ComponentPropsWithoutRef<"a"> & {
+ asChild?: boolean
+ }
+>(({ asChild, className, ...props }, ref) => {
+ const Comp = asChild ? Slot : "a"
+
+ return (
+
+ )
+})
+BreadcrumbLink.displayName = "BreadcrumbLink"
+
+const BreadcrumbPage = React.forwardRef<
+ HTMLSpanElement,
+ React.ComponentPropsWithoutRef<"span">
+>(({ className, ...props }, ref) => (
+
+))
+BreadcrumbPage.displayName = "BreadcrumbPage"
+
+const BreadcrumbSeparator = ({
+ children,
+ className,
+ ...props
+}: React.ComponentProps<"li">) => (
+ svg]:w-3.5 [&>svg]:h-3.5", className)}
+ {...props}
+ >
+ {children ?? }
+
+)
+BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
+
+const BreadcrumbEllipsis = ({
+ className,
+ ...props
+}: React.ComponentProps<"span">) => (
+
+
+ More
+
+)
+BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
+
+export {
+ Breadcrumb,
+ BreadcrumbList,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbPage,
+ BreadcrumbSeparator,
+ BreadcrumbEllipsis,
+}
diff --git a/frontend/components/ui/button.tsx b/frontend/components/ui/button.tsx
new file mode 100644
index 0000000..36496a2
--- /dev/null
+++ b/frontend/components/ui/button.tsx
@@ -0,0 +1,56 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+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 hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-destructive-foreground hover:bg-destructive/90",
+ outline:
+ "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
+ secondary:
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ ghost: "hover:bg-accent hover:text-accent-foreground",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-10 px-4 py-2",
+ sm: "h-9 rounded-md px-3",
+ lg: "h-11 rounded-md px-8",
+ icon: "h-10 w-10",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes,
+ VariantProps {
+ asChild?: boolean
+}
+
+const Button = React.forwardRef(
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button"
+ return (
+
+ )
+ }
+)
+Button.displayName = "Button"
+
+export { Button, buttonVariants }
diff --git a/frontend/components/ui/calendar.tsx b/frontend/components/ui/calendar.tsx
new file mode 100644
index 0000000..61d2b45
--- /dev/null
+++ b/frontend/components/ui/calendar.tsx
@@ -0,0 +1,66 @@
+"use client"
+
+import * as React from "react"
+import { ChevronLeft, ChevronRight } from "lucide-react"
+import { DayPicker } from "react-day-picker"
+
+import { cn } from "@/lib/utils"
+import { buttonVariants } from "@/components/ui/button"
+
+export type CalendarProps = React.ComponentProps
+
+function Calendar({
+ className,
+ classNames,
+ showOutsideDays = true,
+ ...props
+}: CalendarProps) {
+ return (
+ ,
+ IconRight: ({ ...props }) => ,
+ }}
+ {...props}
+ />
+ )
+}
+Calendar.displayName = "Calendar"
+
+export { Calendar }
diff --git a/frontend/components/ui/card.tsx b/frontend/components/ui/card.tsx
new file mode 100644
index 0000000..f62edea
--- /dev/null
+++ b/frontend/components/ui/card.tsx
@@ -0,0 +1,79 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+const Card = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+Card.displayName = "Card"
+
+const CardHeader = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardHeader.displayName = "CardHeader"
+
+const CardTitle = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardTitle.displayName = "CardTitle"
+
+const CardDescription = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardDescription.displayName = "CardDescription"
+
+const CardContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardContent.displayName = "CardContent"
+
+const CardFooter = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardFooter.displayName = "CardFooter"
+
+export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
diff --git a/frontend/components/ui/carousel.tsx b/frontend/components/ui/carousel.tsx
new file mode 100644
index 0000000..ec505d0
--- /dev/null
+++ b/frontend/components/ui/carousel.tsx
@@ -0,0 +1,262 @@
+"use client"
+
+import * as React from "react"
+import useEmblaCarousel, {
+ type UseEmblaCarouselType,
+} from "embla-carousel-react"
+import { ArrowLeft, ArrowRight } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+import { Button } from "@/components/ui/button"
+
+type CarouselApi = UseEmblaCarouselType[1]
+type UseCarouselParameters = Parameters
+type CarouselOptions = UseCarouselParameters[0]
+type CarouselPlugin = UseCarouselParameters[1]
+
+type CarouselProps = {
+ opts?: CarouselOptions
+ plugins?: CarouselPlugin
+ orientation?: "horizontal" | "vertical"
+ setApi?: (api: CarouselApi) => void
+}
+
+type CarouselContextProps = {
+ carouselRef: ReturnType[0]
+ api: ReturnType[1]
+ scrollPrev: () => void
+ scrollNext: () => void
+ canScrollPrev: boolean
+ canScrollNext: boolean
+} & CarouselProps
+
+const CarouselContext = React.createContext(null)
+
+function useCarousel() {
+ const context = React.useContext(CarouselContext)
+
+ if (!context) {
+ throw new Error("useCarousel must be used within a ")
+ }
+
+ return context
+}
+
+const Carousel = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes & CarouselProps
+>(
+ (
+ {
+ orientation = "horizontal",
+ opts,
+ setApi,
+ plugins,
+ className,
+ children,
+ ...props
+ },
+ ref
+ ) => {
+ const [carouselRef, api] = useEmblaCarousel(
+ {
+ ...opts,
+ axis: orientation === "horizontal" ? "x" : "y",
+ },
+ plugins
+ )
+ const [canScrollPrev, setCanScrollPrev] = React.useState(false)
+ const [canScrollNext, setCanScrollNext] = React.useState(false)
+
+ const onSelect = React.useCallback((api: CarouselApi) => {
+ if (!api) {
+ return
+ }
+
+ setCanScrollPrev(api.canScrollPrev())
+ setCanScrollNext(api.canScrollNext())
+ }, [])
+
+ const scrollPrev = React.useCallback(() => {
+ api?.scrollPrev()
+ }, [api])
+
+ const scrollNext = React.useCallback(() => {
+ api?.scrollNext()
+ }, [api])
+
+ const handleKeyDown = React.useCallback(
+ (event: React.KeyboardEvent) => {
+ if (event.key === "ArrowLeft") {
+ event.preventDefault()
+ scrollPrev()
+ } else if (event.key === "ArrowRight") {
+ event.preventDefault()
+ scrollNext()
+ }
+ },
+ [scrollPrev, scrollNext]
+ )
+
+ React.useEffect(() => {
+ if (!api || !setApi) {
+ return
+ }
+
+ setApi(api)
+ }, [api, setApi])
+
+ React.useEffect(() => {
+ if (!api) {
+ return
+ }
+
+ onSelect(api)
+ api.on("reInit", onSelect)
+ api.on("select", onSelect)
+
+ return () => {
+ api?.off("select", onSelect)
+ }
+ }, [api, onSelect])
+
+ return (
+
+
+ {children}
+
+
+ )
+ }
+)
+Carousel.displayName = "Carousel"
+
+const CarouselContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const { carouselRef, orientation } = useCarousel()
+
+ return (
+
+ )
+})
+CarouselContent.displayName = "CarouselContent"
+
+const CarouselItem = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const { orientation } = useCarousel()
+
+ return (
+
+ )
+})
+CarouselItem.displayName = "CarouselItem"
+
+const CarouselPrevious = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps
+>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
+ const { orientation, scrollPrev, canScrollPrev } = useCarousel()
+
+ return (
+
+
+ Previous slide
+
+ )
+})
+CarouselPrevious.displayName = "CarouselPrevious"
+
+const CarouselNext = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps
+>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
+ const { orientation, scrollNext, canScrollNext } = useCarousel()
+
+ return (
+
+
+ Next slide
+
+ )
+})
+CarouselNext.displayName = "CarouselNext"
+
+export {
+ type CarouselApi,
+ Carousel,
+ CarouselContent,
+ CarouselItem,
+ CarouselPrevious,
+ CarouselNext,
+}
diff --git a/frontend/components/ui/chart.tsx b/frontend/components/ui/chart.tsx
new file mode 100644
index 0000000..8eadef6
--- /dev/null
+++ b/frontend/components/ui/chart.tsx
@@ -0,0 +1,30 @@
+import {
+ LineChart as RechartsLineChart,
+ Line,
+ ResponsiveContainer,
+ XAxis,
+ YAxis,
+ Tooltip,
+ Area,
+ BarChart as RechartsBarChart,
+ Bar,
+ CartesianGrid,
+ Legend,
+ Cell,
+} from "recharts"
+
+export {
+ RechartsLineChart as LineChart,
+ RechartsBarChart as BarChart,
+ Line,
+ ResponsiveContainer,
+ XAxis,
+ YAxis,
+ Tooltip,
+ Area,
+ Bar,
+ CartesianGrid,
+ Legend,
+ Cell,
+}
+
diff --git a/frontend/components/ui/checkbox.tsx b/frontend/components/ui/checkbox.tsx
new file mode 100644
index 0000000..df61a13
--- /dev/null
+++ b/frontend/components/ui/checkbox.tsx
@@ -0,0 +1,30 @@
+"use client"
+
+import * as React from "react"
+import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
+import { Check } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Checkbox = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+
+))
+Checkbox.displayName = CheckboxPrimitive.Root.displayName
+
+export { Checkbox }
diff --git a/frontend/components/ui/collapsible.tsx b/frontend/components/ui/collapsible.tsx
new file mode 100644
index 0000000..9fa4894
--- /dev/null
+++ b/frontend/components/ui/collapsible.tsx
@@ -0,0 +1,11 @@
+"use client"
+
+import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
+
+const Collapsible = CollapsiblePrimitive.Root
+
+const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
+
+const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
+
+export { Collapsible, CollapsibleTrigger, CollapsibleContent }
diff --git a/frontend/components/ui/command.tsx b/frontend/components/ui/command.tsx
new file mode 100644
index 0000000..59a2645
--- /dev/null
+++ b/frontend/components/ui/command.tsx
@@ -0,0 +1,153 @@
+"use client"
+
+import * as React from "react"
+import { type DialogProps } from "@radix-ui/react-dialog"
+import { Command as CommandPrimitive } from "cmdk"
+import { Search } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+import { Dialog, DialogContent } from "@/components/ui/dialog"
+
+const Command = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+Command.displayName = CommandPrimitive.displayName
+
+const CommandDialog = ({ children, ...props }: DialogProps) => {
+ return (
+
+
+
+ {children}
+
+
+
+ )
+}
+
+const CommandInput = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+))
+
+CommandInput.displayName = CommandPrimitive.Input.displayName
+
+const CommandList = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+
+CommandList.displayName = CommandPrimitive.List.displayName
+
+const CommandEmpty = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>((props, ref) => (
+
+))
+
+CommandEmpty.displayName = CommandPrimitive.Empty.displayName
+
+const CommandGroup = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+
+CommandGroup.displayName = CommandPrimitive.Group.displayName
+
+const CommandSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+CommandSeparator.displayName = CommandPrimitive.Separator.displayName
+
+const CommandItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+
+CommandItem.displayName = CommandPrimitive.Item.displayName
+
+const CommandShortcut = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => {
+ return (
+
+ )
+}
+CommandShortcut.displayName = "CommandShortcut"
+
+export {
+ Command,
+ CommandDialog,
+ CommandInput,
+ CommandList,
+ CommandEmpty,
+ CommandGroup,
+ CommandItem,
+ CommandShortcut,
+ CommandSeparator,
+}
diff --git a/frontend/components/ui/context-menu.tsx b/frontend/components/ui/context-menu.tsx
new file mode 100644
index 0000000..93ef37b
--- /dev/null
+++ b/frontend/components/ui/context-menu.tsx
@@ -0,0 +1,200 @@
+"use client"
+
+import * as React from "react"
+import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
+import { Check, ChevronRight, Circle } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const ContextMenu = ContextMenuPrimitive.Root
+
+const ContextMenuTrigger = ContextMenuPrimitive.Trigger
+
+const ContextMenuGroup = ContextMenuPrimitive.Group
+
+const ContextMenuPortal = ContextMenuPrimitive.Portal
+
+const ContextMenuSub = ContextMenuPrimitive.Sub
+
+const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup
+
+const ContextMenuSubTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean
+ }
+>(({ className, inset, children, ...props }, ref) => (
+
+ {children}
+
+
+))
+ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName
+
+const ContextMenuSubContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName
+
+const ContextMenuContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+))
+ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName
+
+const ContextMenuItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean
+ }
+>(({ className, inset, ...props }, ref) => (
+
+))
+ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName
+
+const ContextMenuCheckboxItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, checked, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+))
+ContextMenuCheckboxItem.displayName =
+ ContextMenuPrimitive.CheckboxItem.displayName
+
+const ContextMenuRadioItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+))
+ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName
+
+const ContextMenuLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean
+ }
+>(({ className, inset, ...props }, ref) => (
+
+))
+ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName
+
+const ContextMenuSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName
+
+const ContextMenuShortcut = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => {
+ return (
+
+ )
+}
+ContextMenuShortcut.displayName = "ContextMenuShortcut"
+
+export {
+ ContextMenu,
+ ContextMenuTrigger,
+ ContextMenuContent,
+ ContextMenuItem,
+ ContextMenuCheckboxItem,
+ ContextMenuRadioItem,
+ ContextMenuLabel,
+ ContextMenuSeparator,
+ ContextMenuShortcut,
+ ContextMenuGroup,
+ ContextMenuPortal,
+ ContextMenuSub,
+ ContextMenuSubContent,
+ ContextMenuSubTrigger,
+ ContextMenuRadioGroup,
+}
diff --git a/frontend/components/ui/dialog.tsx b/frontend/components/ui/dialog.tsx
new file mode 100644
index 0000000..01ff19c
--- /dev/null
+++ b/frontend/components/ui/dialog.tsx
@@ -0,0 +1,122 @@
+"use client"
+
+import * as React from "react"
+import * as DialogPrimitive from "@radix-ui/react-dialog"
+import { X } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Dialog = DialogPrimitive.Root
+
+const DialogTrigger = DialogPrimitive.Trigger
+
+const DialogPortal = DialogPrimitive.Portal
+
+const DialogClose = DialogPrimitive.Close
+
+const DialogOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
+
+const DialogContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+ {children}
+
+
+ Close
+
+
+
+))
+DialogContent.displayName = DialogPrimitive.Content.displayName
+
+const DialogHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+DialogHeader.displayName = "DialogHeader"
+
+const DialogFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+DialogFooter.displayName = "DialogFooter"
+
+const DialogTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DialogTitle.displayName = DialogPrimitive.Title.displayName
+
+const DialogDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DialogDescription.displayName = DialogPrimitive.Description.displayName
+
+export {
+ Dialog,
+ DialogPortal,
+ DialogOverlay,
+ DialogClose,
+ DialogTrigger,
+ DialogContent,
+ DialogHeader,
+ DialogFooter,
+ DialogTitle,
+ DialogDescription,
+}
diff --git a/frontend/components/ui/drawer.tsx b/frontend/components/ui/drawer.tsx
new file mode 100644
index 0000000..6a0ef53
--- /dev/null
+++ b/frontend/components/ui/drawer.tsx
@@ -0,0 +1,118 @@
+"use client"
+
+import * as React from "react"
+import { Drawer as DrawerPrimitive } from "vaul"
+
+import { cn } from "@/lib/utils"
+
+const Drawer = ({
+ shouldScaleBackground = true,
+ ...props
+}: React.ComponentProps) => (
+
+)
+Drawer.displayName = "Drawer"
+
+const DrawerTrigger = DrawerPrimitive.Trigger
+
+const DrawerPortal = DrawerPrimitive.Portal
+
+const DrawerClose = DrawerPrimitive.Close
+
+const DrawerOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
+
+const DrawerContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+
+ {children}
+
+
+))
+DrawerContent.displayName = "DrawerContent"
+
+const DrawerHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+DrawerHeader.displayName = "DrawerHeader"
+
+const DrawerFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+DrawerFooter.displayName = "DrawerFooter"
+
+const DrawerTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DrawerTitle.displayName = DrawerPrimitive.Title.displayName
+
+const DrawerDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DrawerDescription.displayName = DrawerPrimitive.Description.displayName
+
+export {
+ Drawer,
+ DrawerPortal,
+ DrawerOverlay,
+ DrawerTrigger,
+ DrawerClose,
+ DrawerContent,
+ DrawerHeader,
+ DrawerFooter,
+ DrawerTitle,
+ DrawerDescription,
+}
diff --git a/frontend/components/ui/dropdown-menu.tsx b/frontend/components/ui/dropdown-menu.tsx
new file mode 100644
index 0000000..0fc4c0e
--- /dev/null
+++ b/frontend/components/ui/dropdown-menu.tsx
@@ -0,0 +1,200 @@
+"use client"
+
+import * as React from "react"
+import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
+import { Check, ChevronRight, Circle } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const DropdownMenu = DropdownMenuPrimitive.Root
+
+const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
+
+const DropdownMenuGroup = DropdownMenuPrimitive.Group
+
+const DropdownMenuPortal = DropdownMenuPrimitive.Portal
+
+const DropdownMenuSub = DropdownMenuPrimitive.Sub
+
+const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
+
+const DropdownMenuSubTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean
+ }
+>(({ className, inset, children, ...props }, ref) => (
+
+ {children}
+
+
+))
+DropdownMenuSubTrigger.displayName =
+ DropdownMenuPrimitive.SubTrigger.displayName
+
+const DropdownMenuSubContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DropdownMenuSubContent.displayName =
+ DropdownMenuPrimitive.SubContent.displayName
+
+const DropdownMenuContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, sideOffset = 4, ...props }, ref) => (
+
+
+
+))
+DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
+
+const DropdownMenuItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean
+ }
+>(({ className, inset, ...props }, ref) => (
+
+))
+DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
+
+const DropdownMenuCheckboxItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, checked, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+))
+DropdownMenuCheckboxItem.displayName =
+ DropdownMenuPrimitive.CheckboxItem.displayName
+
+const DropdownMenuRadioItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+))
+DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
+
+const DropdownMenuLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean
+ }
+>(({ className, inset, ...props }, ref) => (
+
+))
+DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
+
+const DropdownMenuSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
+
+const DropdownMenuShortcut = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => {
+ return (
+
+ )
+}
+DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
+
+export {
+ DropdownMenu,
+ DropdownMenuTrigger,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuCheckboxItem,
+ DropdownMenuRadioItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuShortcut,
+ DropdownMenuGroup,
+ DropdownMenuPortal,
+ DropdownMenuSub,
+ DropdownMenuSubContent,
+ DropdownMenuSubTrigger,
+ DropdownMenuRadioGroup,
+}
diff --git a/frontend/components/ui/form.tsx b/frontend/components/ui/form.tsx
new file mode 100644
index 0000000..ce264ae
--- /dev/null
+++ b/frontend/components/ui/form.tsx
@@ -0,0 +1,178 @@
+"use client"
+
+import * as React from "react"
+import * as LabelPrimitive from "@radix-ui/react-label"
+import { Slot } from "@radix-ui/react-slot"
+import {
+ Controller,
+ ControllerProps,
+ FieldPath,
+ FieldValues,
+ FormProvider,
+ useFormContext,
+} from "react-hook-form"
+
+import { cn } from "@/lib/utils"
+import { Label } from "@/components/ui/label"
+
+const Form = FormProvider
+
+type FormFieldContextValue<
+ TFieldValues extends FieldValues = FieldValues,
+ TName extends FieldPath = FieldPath
+> = {
+ name: TName
+}
+
+const FormFieldContext = React.createContext(
+ {} as FormFieldContextValue
+)
+
+const FormField = <
+ TFieldValues extends FieldValues = FieldValues,
+ TName extends FieldPath = FieldPath
+>({
+ ...props
+}: ControllerProps) => {
+ return (
+
+
+
+ )
+}
+
+const useFormField = () => {
+ const fieldContext = React.useContext(FormFieldContext)
+ const itemContext = React.useContext(FormItemContext)
+ const { getFieldState, formState } = useFormContext()
+
+ const fieldState = getFieldState(fieldContext.name, formState)
+
+ if (!fieldContext) {
+ throw new Error("useFormField should be used within ")
+ }
+
+ const { id } = itemContext
+
+ return {
+ id,
+ name: fieldContext.name,
+ formItemId: `${id}-form-item`,
+ formDescriptionId: `${id}-form-item-description`,
+ formMessageId: `${id}-form-item-message`,
+ ...fieldState,
+ }
+}
+
+type FormItemContextValue = {
+ id: string
+}
+
+const FormItemContext = React.createContext(
+ {} as FormItemContextValue
+)
+
+const FormItem = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const id = React.useId()
+
+ return (
+
+
+
+ )
+})
+FormItem.displayName = "FormItem"
+
+const FormLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => {
+ const { error, formItemId } = useFormField()
+
+ return (
+
+ )
+})
+FormLabel.displayName = "FormLabel"
+
+const FormControl = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ ...props }, ref) => {
+ const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
+
+ return (
+
+ )
+})
+FormControl.displayName = "FormControl"
+
+const FormDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const { formDescriptionId } = useFormField()
+
+ return (
+
+ )
+})
+FormDescription.displayName = "FormDescription"
+
+const FormMessage = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, children, ...props }, ref) => {
+ const { error, formMessageId } = useFormField()
+ const body = error ? String(error?.message) : children
+
+ if (!body) {
+ return null
+ }
+
+ return (
+
+ {body}
+
+ )
+})
+FormMessage.displayName = "FormMessage"
+
+export {
+ useFormField,
+ Form,
+ FormItem,
+ FormLabel,
+ FormControl,
+ FormDescription,
+ FormMessage,
+ FormField,
+}
diff --git a/frontend/components/ui/hover-card.tsx b/frontend/components/ui/hover-card.tsx
new file mode 100644
index 0000000..e54d91c
--- /dev/null
+++ b/frontend/components/ui/hover-card.tsx
@@ -0,0 +1,29 @@
+"use client"
+
+import * as React from "react"
+import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
+
+import { cn } from "@/lib/utils"
+
+const HoverCard = HoverCardPrimitive.Root
+
+const HoverCardTrigger = HoverCardPrimitive.Trigger
+
+const HoverCardContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
+
+))
+HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
+
+export { HoverCard, HoverCardTrigger, HoverCardContent }
diff --git a/frontend/components/ui/input-otp.tsx b/frontend/components/ui/input-otp.tsx
new file mode 100644
index 0000000..f66fcfa
--- /dev/null
+++ b/frontend/components/ui/input-otp.tsx
@@ -0,0 +1,71 @@
+"use client"
+
+import * as React from "react"
+import { OTPInput, OTPInputContext } from "input-otp"
+import { Dot } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const InputOTP = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, containerClassName, ...props }, ref) => (
+
+))
+InputOTP.displayName = "InputOTP"
+
+const InputOTPGroup = React.forwardRef<
+ React.ElementRef<"div">,
+ React.ComponentPropsWithoutRef<"div">
+>(({ className, ...props }, ref) => (
+
+))
+InputOTPGroup.displayName = "InputOTPGroup"
+
+const InputOTPSlot = React.forwardRef<
+ React.ElementRef<"div">,
+ React.ComponentPropsWithoutRef<"div"> & { index: number }
+>(({ index, className, ...props }, ref) => {
+ const inputOTPContext = React.useContext(OTPInputContext)
+ const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index]
+
+ return (
+
+ {char}
+ {hasFakeCaret && (
+
+ )}
+
+ )
+})
+InputOTPSlot.displayName = "InputOTPSlot"
+
+const InputOTPSeparator = React.forwardRef<
+ React.ElementRef<"div">,
+ React.ComponentPropsWithoutRef<"div">
+>(({ ...props }, ref) => (
+
+
+
+))
+InputOTPSeparator.displayName = "InputOTPSeparator"
+
+export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }
diff --git a/frontend/components/ui/input.tsx b/frontend/components/ui/input.tsx
new file mode 100644
index 0000000..68551b9
--- /dev/null
+++ b/frontend/components/ui/input.tsx
@@ -0,0 +1,22 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+const Input = React.forwardRef>(
+ ({ className, type, ...props }, ref) => {
+ return (
+
+ )
+ }
+)
+Input.displayName = "Input"
+
+export { Input }
diff --git a/frontend/components/ui/label.tsx b/frontend/components/ui/label.tsx
new file mode 100644
index 0000000..5341821
--- /dev/null
+++ b/frontend/components/ui/label.tsx
@@ -0,0 +1,26 @@
+"use client"
+
+import * as React from "react"
+import * as LabelPrimitive from "@radix-ui/react-label"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const labelVariants = cva(
+ "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
+)
+
+const Label = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef &
+ VariantProps
+>(({ className, ...props }, ref) => (
+
+))
+Label.displayName = LabelPrimitive.Root.displayName
+
+export { Label }
diff --git a/frontend/components/ui/menubar.tsx b/frontend/components/ui/menubar.tsx
new file mode 100644
index 0000000..5586fa9
--- /dev/null
+++ b/frontend/components/ui/menubar.tsx
@@ -0,0 +1,236 @@
+"use client"
+
+import * as React from "react"
+import * as MenubarPrimitive from "@radix-ui/react-menubar"
+import { Check, ChevronRight, Circle } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const MenubarMenu = MenubarPrimitive.Menu
+
+const MenubarGroup = MenubarPrimitive.Group
+
+const MenubarPortal = MenubarPrimitive.Portal
+
+const MenubarSub = MenubarPrimitive.Sub
+
+const MenubarRadioGroup = MenubarPrimitive.RadioGroup
+
+const Menubar = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+Menubar.displayName = MenubarPrimitive.Root.displayName
+
+const MenubarTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName
+
+const MenubarSubTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean
+ }
+>(({ className, inset, children, ...props }, ref) => (
+
+ {children}
+
+
+))
+MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName
+
+const MenubarSubContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName
+
+const MenubarContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(
+ (
+ { className, align = "start", alignOffset = -4, sideOffset = 8, ...props },
+ ref
+ ) => (
+
+
+
+ )
+)
+MenubarContent.displayName = MenubarPrimitive.Content.displayName
+
+const MenubarItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean
+ }
+>(({ className, inset, ...props }, ref) => (
+
+))
+MenubarItem.displayName = MenubarPrimitive.Item.displayName
+
+const MenubarCheckboxItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, checked, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+))
+MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName
+
+const MenubarRadioItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+))
+MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName
+
+const MenubarLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean
+ }
+>(({ className, inset, ...props }, ref) => (
+
+))
+MenubarLabel.displayName = MenubarPrimitive.Label.displayName
+
+const MenubarSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName
+
+const MenubarShortcut = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => {
+ return (
+
+ )
+}
+MenubarShortcut.displayname = "MenubarShortcut"
+
+export {
+ Menubar,
+ MenubarMenu,
+ MenubarTrigger,
+ MenubarContent,
+ MenubarItem,
+ MenubarSeparator,
+ MenubarLabel,
+ MenubarCheckboxItem,
+ MenubarRadioGroup,
+ MenubarRadioItem,
+ MenubarPortal,
+ MenubarSubContent,
+ MenubarSubTrigger,
+ MenubarGroup,
+ MenubarSub,
+ MenubarShortcut,
+}
diff --git a/frontend/components/ui/navigation-menu.tsx b/frontend/components/ui/navigation-menu.tsx
new file mode 100644
index 0000000..1419f56
--- /dev/null
+++ b/frontend/components/ui/navigation-menu.tsx
@@ -0,0 +1,128 @@
+import * as React from "react"
+import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
+import { cva } from "class-variance-authority"
+import { ChevronDown } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const NavigationMenu = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ {children}
+
+
+))
+NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName
+
+const NavigationMenuList = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName
+
+const NavigationMenuItem = NavigationMenuPrimitive.Item
+
+const navigationMenuTriggerStyle = cva(
+ "group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50"
+)
+
+const NavigationMenuTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ {children}{" "}
+
+
+))
+NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName
+
+const NavigationMenuContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName
+
+const NavigationMenuLink = NavigationMenuPrimitive.Link
+
+const NavigationMenuViewport = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+))
+NavigationMenuViewport.displayName =
+ NavigationMenuPrimitive.Viewport.displayName
+
+const NavigationMenuIndicator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+))
+NavigationMenuIndicator.displayName =
+ NavigationMenuPrimitive.Indicator.displayName
+
+export {
+ navigationMenuTriggerStyle,
+ NavigationMenu,
+ NavigationMenuList,
+ NavigationMenuItem,
+ NavigationMenuContent,
+ NavigationMenuTrigger,
+ NavigationMenuLink,
+ NavigationMenuIndicator,
+ NavigationMenuViewport,
+}
diff --git a/frontend/components/ui/pagination.tsx b/frontend/components/ui/pagination.tsx
new file mode 100644
index 0000000..ea40d19
--- /dev/null
+++ b/frontend/components/ui/pagination.tsx
@@ -0,0 +1,117 @@
+import * as React from "react"
+import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+import { ButtonProps, buttonVariants } from "@/components/ui/button"
+
+const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
+
+)
+Pagination.displayName = "Pagination"
+
+const PaginationContent = React.forwardRef<
+ HTMLUListElement,
+ React.ComponentProps<"ul">
+>(({ className, ...props }, ref) => (
+
+))
+PaginationContent.displayName = "PaginationContent"
+
+const PaginationItem = React.forwardRef<
+ HTMLLIElement,
+ React.ComponentProps<"li">
+>(({ className, ...props }, ref) => (
+
+))
+PaginationItem.displayName = "PaginationItem"
+
+type PaginationLinkProps = {
+ isActive?: boolean
+} & Pick &
+ React.ComponentProps<"a">
+
+const PaginationLink = ({
+ className,
+ isActive,
+ size = "icon",
+ ...props
+}: PaginationLinkProps) => (
+
+)
+PaginationLink.displayName = "PaginationLink"
+
+const PaginationPrevious = ({
+ className,
+ ...props
+}: React.ComponentProps) => (
+
+
+ Previous
+
+)
+PaginationPrevious.displayName = "PaginationPrevious"
+
+const PaginationNext = ({
+ className,
+ ...props
+}: React.ComponentProps) => (
+
+ Next
+
+
+)
+PaginationNext.displayName = "PaginationNext"
+
+const PaginationEllipsis = ({
+ className,
+ ...props
+}: React.ComponentProps<"span">) => (
+
+
+ More pages
+
+)
+PaginationEllipsis.displayName = "PaginationEllipsis"
+
+export {
+ Pagination,
+ PaginationContent,
+ PaginationEllipsis,
+ PaginationItem,
+ PaginationLink,
+ PaginationNext,
+ PaginationPrevious,
+}
diff --git a/frontend/components/ui/popover.tsx b/frontend/components/ui/popover.tsx
new file mode 100644
index 0000000..a0ec48b
--- /dev/null
+++ b/frontend/components/ui/popover.tsx
@@ -0,0 +1,31 @@
+"use client"
+
+import * as React from "react"
+import * as PopoverPrimitive from "@radix-ui/react-popover"
+
+import { cn } from "@/lib/utils"
+
+const Popover = PopoverPrimitive.Root
+
+const PopoverTrigger = PopoverPrimitive.Trigger
+
+const PopoverContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
+
+
+
+))
+PopoverContent.displayName = PopoverPrimitive.Content.displayName
+
+export { Popover, PopoverTrigger, PopoverContent }
diff --git a/frontend/components/ui/progress.tsx b/frontend/components/ui/progress.tsx
new file mode 100644
index 0000000..36aeedd
--- /dev/null
+++ b/frontend/components/ui/progress.tsx
@@ -0,0 +1,26 @@
+"use client"
+
+import * as React from "react"
+import * as ProgressPrimitive from "@radix-ui/react-progress"
+
+import { cn } from "@/lib/utils"
+
+const Progress = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, value, ...props }, ref) => (
+
+
+
+))
+Progress.displayName = ProgressPrimitive.Root.displayName
+
+export { Progress }
+
diff --git a/frontend/components/ui/radio-group.tsx b/frontend/components/ui/radio-group.tsx
new file mode 100644
index 0000000..e9bde17
--- /dev/null
+++ b/frontend/components/ui/radio-group.tsx
@@ -0,0 +1,44 @@
+"use client"
+
+import * as React from "react"
+import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
+import { Circle } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const RadioGroup = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => {
+ return (
+
+ )
+})
+RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
+
+const RadioGroupItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => {
+ return (
+
+
+
+
+
+ )
+})
+RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
+
+export { RadioGroup, RadioGroupItem }
diff --git a/frontend/components/ui/resizable.tsx b/frontend/components/ui/resizable.tsx
new file mode 100644
index 0000000..f4bc558
--- /dev/null
+++ b/frontend/components/ui/resizable.tsx
@@ -0,0 +1,45 @@
+"use client"
+
+import { GripVertical } from "lucide-react"
+import * as ResizablePrimitive from "react-resizable-panels"
+
+import { cn } from "@/lib/utils"
+
+const ResizablePanelGroup = ({
+ className,
+ ...props
+}: React.ComponentProps) => (
+
+)
+
+const ResizablePanel = ResizablePrimitive.Panel
+
+const ResizableHandle = ({
+ withHandle,
+ className,
+ ...props
+}: React.ComponentProps & {
+ withHandle?: boolean
+}) => (
+ div]:rotate-90",
+ className
+ )}
+ {...props}
+ >
+ {withHandle && (
+
+
+
+ )}
+
+)
+
+export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
diff --git a/frontend/components/ui/scroll-area.tsx b/frontend/components/ui/scroll-area.tsx
new file mode 100644
index 0000000..f33e797
--- /dev/null
+++ b/frontend/components/ui/scroll-area.tsx
@@ -0,0 +1,41 @@
+"use client"
+
+import * as React from "react"
+import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
+
+import { cn } from "@/lib/utils"
+
+const ScrollArea = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ {children}
+
+
+
+))
+ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
+
+const ScrollBar = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, orientation = "vertical", ...props }, ref) => (
+
+
+
+))
+ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
+
+export { ScrollArea, ScrollBar }
+
diff --git a/frontend/components/ui/select.tsx b/frontend/components/ui/select.tsx
new file mode 100644
index 0000000..cbe5a36
--- /dev/null
+++ b/frontend/components/ui/select.tsx
@@ -0,0 +1,160 @@
+"use client"
+
+import * as React from "react"
+import * as SelectPrimitive from "@radix-ui/react-select"
+import { Check, ChevronDown, ChevronUp } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Select = SelectPrimitive.Root
+
+const SelectGroup = SelectPrimitive.Group
+
+const SelectValue = SelectPrimitive.Value
+
+const SelectTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+ span]:line-clamp-1",
+ className
+ )}
+ {...props}
+ >
+ {children}
+
+
+
+
+))
+SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
+
+const SelectScrollUpButton = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+))
+SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
+
+const SelectScrollDownButton = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+))
+SelectScrollDownButton.displayName =
+ SelectPrimitive.ScrollDownButton.displayName
+
+const SelectContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, position = "popper", ...props }, ref) => (
+
+
+
+
+ {children}
+
+
+
+
+))
+SelectContent.displayName = SelectPrimitive.Content.displayName
+
+const SelectLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+SelectLabel.displayName = SelectPrimitive.Label.displayName
+
+const SelectItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+
+
+
+
+ {children}
+
+))
+SelectItem.displayName = SelectPrimitive.Item.displayName
+
+const SelectSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+SelectSeparator.displayName = SelectPrimitive.Separator.displayName
+
+export {
+ Select,
+ SelectGroup,
+ SelectValue,
+ SelectTrigger,
+ SelectContent,
+ SelectLabel,
+ SelectItem,
+ SelectSeparator,
+ SelectScrollUpButton,
+ SelectScrollDownButton,
+}
diff --git a/frontend/components/ui/separator.tsx b/frontend/components/ui/separator.tsx
new file mode 100644
index 0000000..12d81c4
--- /dev/null
+++ b/frontend/components/ui/separator.tsx
@@ -0,0 +1,31 @@
+"use client"
+
+import * as React from "react"
+import * as SeparatorPrimitive from "@radix-ui/react-separator"
+
+import { cn } from "@/lib/utils"
+
+const Separator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(
+ (
+ { className, orientation = "horizontal", decorative = true, ...props },
+ ref
+ ) => (
+
+ )
+)
+Separator.displayName = SeparatorPrimitive.Root.displayName
+
+export { Separator }
diff --git a/frontend/components/ui/sheet.tsx b/frontend/components/ui/sheet.tsx
new file mode 100644
index 0000000..a37f17b
--- /dev/null
+++ b/frontend/components/ui/sheet.tsx
@@ -0,0 +1,140 @@
+"use client"
+
+import * as React from "react"
+import * as SheetPrimitive from "@radix-ui/react-dialog"
+import { cva, type VariantProps } from "class-variance-authority"
+import { X } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Sheet = SheetPrimitive.Root
+
+const SheetTrigger = SheetPrimitive.Trigger
+
+const SheetClose = SheetPrimitive.Close
+
+const SheetPortal = SheetPrimitive.Portal
+
+const SheetOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
+
+const sheetVariants = cva(
+ "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
+ {
+ variants: {
+ side: {
+ top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
+ bottom:
+ "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
+ left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
+ right:
+ "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
+ },
+ },
+ defaultVariants: {
+ side: "right",
+ },
+ }
+)
+
+interface SheetContentProps
+ extends React.ComponentPropsWithoutRef,
+ VariantProps {}
+
+const SheetContent = React.forwardRef<
+ React.ElementRef,
+ SheetContentProps
+>(({ side = "right", className, children, ...props }, ref) => (
+
+
+
+ {children}
+
+
+ Close
+
+
+
+))
+SheetContent.displayName = SheetPrimitive.Content.displayName
+
+const SheetHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+SheetHeader.displayName = "SheetHeader"
+
+const SheetFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+SheetFooter.displayName = "SheetFooter"
+
+const SheetTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+SheetTitle.displayName = SheetPrimitive.Title.displayName
+
+const SheetDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+SheetDescription.displayName = SheetPrimitive.Description.displayName
+
+export {
+ Sheet,
+ SheetPortal,
+ SheetOverlay,
+ SheetTrigger,
+ SheetClose,
+ SheetContent,
+ SheetHeader,
+ SheetFooter,
+ SheetTitle,
+ SheetDescription,
+}
diff --git a/frontend/components/ui/sidebar.tsx b/frontend/components/ui/sidebar.tsx
new file mode 100644
index 0000000..eeb2d7a
--- /dev/null
+++ b/frontend/components/ui/sidebar.tsx
@@ -0,0 +1,763 @@
+"use client"
+
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { VariantProps, cva } from "class-variance-authority"
+import { PanelLeft } from "lucide-react"
+
+import { useIsMobile } from "@/hooks/use-mobile"
+import { cn } from "@/lib/utils"
+import { Button } from "@/components/ui/button"
+import { Input } from "@/components/ui/input"
+import { Separator } from "@/components/ui/separator"
+import { Sheet, SheetContent } from "@/components/ui/sheet"
+import { Skeleton } from "@/components/ui/skeleton"
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from "@/components/ui/tooltip"
+
+const SIDEBAR_COOKIE_NAME = "sidebar:state"
+const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
+const SIDEBAR_WIDTH = "16rem"
+const SIDEBAR_WIDTH_MOBILE = "18rem"
+const SIDEBAR_WIDTH_ICON = "3rem"
+const SIDEBAR_KEYBOARD_SHORTCUT = "b"
+
+type SidebarContext = {
+ state: "expanded" | "collapsed"
+ open: boolean
+ setOpen: (open: boolean) => void
+ openMobile: boolean
+ setOpenMobile: (open: boolean) => void
+ isMobile: boolean
+ toggleSidebar: () => void
+}
+
+const SidebarContext = React.createContext(null)
+
+function useSidebar() {
+ const context = React.useContext(SidebarContext)
+ if (!context) {
+ throw new Error("useSidebar must be used within a SidebarProvider.")
+ }
+
+ return context
+}
+
+const SidebarProvider = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div"> & {
+ defaultOpen?: boolean
+ open?: boolean
+ onOpenChange?: (open: boolean) => void
+ }
+>(
+ (
+ {
+ defaultOpen = true,
+ open: openProp,
+ onOpenChange: setOpenProp,
+ className,
+ style,
+ children,
+ ...props
+ },
+ ref
+ ) => {
+ const isMobile = useIsMobile()
+ const [openMobile, setOpenMobile] = React.useState(false)
+
+ // This is the internal state of the sidebar.
+ // We use openProp and setOpenProp for control from outside the component.
+ const [_open, _setOpen] = React.useState(defaultOpen)
+ const open = openProp ?? _open
+ const setOpen = React.useCallback(
+ (value: boolean | ((value: boolean) => boolean)) => {
+ const openState = typeof value === "function" ? value(open) : value
+ if (setOpenProp) {
+ setOpenProp(openState)
+ } else {
+ _setOpen(openState)
+ }
+
+ // This sets the cookie to keep the sidebar state.
+ document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
+ },
+ [setOpenProp, open]
+ )
+
+ // Helper to toggle the sidebar.
+ const toggleSidebar = React.useCallback(() => {
+ return isMobile
+ ? setOpenMobile((open) => !open)
+ : setOpen((open) => !open)
+ }, [isMobile, setOpen, setOpenMobile])
+
+ // Adds a keyboard shortcut to toggle the sidebar.
+ React.useEffect(() => {
+ const handleKeyDown = (event: KeyboardEvent) => {
+ if (
+ event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
+ (event.metaKey || event.ctrlKey)
+ ) {
+ event.preventDefault()
+ toggleSidebar()
+ }
+ }
+
+ window.addEventListener("keydown", handleKeyDown)
+ return () => window.removeEventListener("keydown", handleKeyDown)
+ }, [toggleSidebar])
+
+ // We add a state so that we can do data-state="expanded" or "collapsed".
+ // This makes it easier to style the sidebar with Tailwind classes.
+ const state = open ? "expanded" : "collapsed"
+
+ const contextValue = React.useMemo(
+ () => ({
+ state,
+ open,
+ setOpen,
+ isMobile,
+ openMobile,
+ setOpenMobile,
+ toggleSidebar,
+ }),
+ [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
+ )
+
+ return (
+
+
+
+ {children}
+
+
+
+ )
+ }
+)
+SidebarProvider.displayName = "SidebarProvider"
+
+const Sidebar = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div"> & {
+ side?: "left" | "right"
+ variant?: "sidebar" | "floating" | "inset"
+ collapsible?: "offcanvas" | "icon" | "none"
+ }
+>(
+ (
+ {
+ side = "left",
+ variant = "sidebar",
+ collapsible = "offcanvas",
+ className,
+ children,
+ ...props
+ },
+ ref
+ ) => {
+ const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
+
+ if (collapsible === "none") {
+ return (
+
+ {children}
+
+ )
+ }
+
+ if (isMobile) {
+ return (
+
+
+ {children}
+
+
+ )
+ }
+
+ return (
+
+ {/* This is what handles the sidebar gap on desktop */}
+
+
+
+ )
+ }
+)
+Sidebar.displayName = "Sidebar"
+
+const SidebarTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentProps
+>(({ className, onClick, ...props }, ref) => {
+ const { toggleSidebar } = useSidebar()
+
+ return (
+ {
+ onClick?.(event)
+ toggleSidebar()
+ }}
+ {...props}
+ >
+
+ Toggle Sidebar
+
+ )
+})
+SidebarTrigger.displayName = "SidebarTrigger"
+
+const SidebarRail = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps<"button">
+>(({ className, ...props }, ref) => {
+ const { toggleSidebar } = useSidebar()
+
+ return (
+
+ )
+})
+SidebarRail.displayName = "SidebarRail"
+
+const SidebarInset = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"main">
+>(({ className, ...props }, ref) => {
+ return (
+
+ )
+})
+SidebarInset.displayName = "SidebarInset"
+
+const SidebarInput = React.forwardRef<
+ React.ElementRef,
+ React.ComponentProps
+>(({ className, ...props }, ref) => {
+ return (
+
+ )
+})
+SidebarInput.displayName = "SidebarInput"
+
+const SidebarHeader = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div">
+>(({ className, ...props }, ref) => {
+ return (
+
+ )
+})
+SidebarHeader.displayName = "SidebarHeader"
+
+const SidebarFooter = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div">
+>(({ className, ...props }, ref) => {
+ return (
+
+ )
+})
+SidebarFooter.displayName = "SidebarFooter"
+
+const SidebarSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentProps
+>(({ className, ...props }, ref) => {
+ return (
+
+ )
+})
+SidebarSeparator.displayName = "SidebarSeparator"
+
+const SidebarContent = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div">
+>(({ className, ...props }, ref) => {
+ return (
+
+ )
+})
+SidebarContent.displayName = "SidebarContent"
+
+const SidebarGroup = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div">
+>(({ className, ...props }, ref) => {
+ return (
+
+ )
+})
+SidebarGroup.displayName = "SidebarGroup"
+
+const SidebarGroupLabel = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div"> & { asChild?: boolean }
+>(({ className, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "div"
+
+ return (
+ svg]:size-4 [&>svg]:shrink-0",
+ "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
+ className
+ )}
+ {...props}
+ />
+ )
+})
+SidebarGroupLabel.displayName = "SidebarGroupLabel"
+
+const SidebarGroupAction = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps<"button"> & { asChild?: boolean }
+>(({ className, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button"
+
+ return (
+ svg]:size-4 [&>svg]:shrink-0",
+ // Increases the hit area of the button on mobile.
+ "after:absolute after:-inset-2 after:md:hidden",
+ "group-data-[collapsible=icon]:hidden",
+ className
+ )}
+ {...props}
+ />
+ )
+})
+SidebarGroupAction.displayName = "SidebarGroupAction"
+
+const SidebarGroupContent = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div">
+>(({ className, ...props }, ref) => (
+
+))
+SidebarGroupContent.displayName = "SidebarGroupContent"
+
+const SidebarMenu = React.forwardRef<
+ HTMLUListElement,
+ React.ComponentProps<"ul">
+>(({ className, ...props }, ref) => (
+
+))
+SidebarMenu.displayName = "SidebarMenu"
+
+const SidebarMenuItem = React.forwardRef<
+ HTMLLIElement,
+ React.ComponentProps<"li">
+>(({ className, ...props }, ref) => (
+
+))
+SidebarMenuItem.displayName = "SidebarMenuItem"
+
+const sidebarMenuButtonVariants = cva(
+ "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
+ {
+ variants: {
+ variant: {
+ default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
+ outline:
+ "bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
+ },
+ size: {
+ default: "h-8 text-sm",
+ sm: "h-7 text-xs",
+ lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+const SidebarMenuButton = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps<"button"> & {
+ asChild?: boolean
+ isActive?: boolean
+ tooltip?: string | React.ComponentProps
+ } & VariantProps
+>(
+ (
+ {
+ asChild = false,
+ isActive = false,
+ variant = "default",
+ size = "default",
+ tooltip,
+ className,
+ ...props
+ },
+ ref
+ ) => {
+ const Comp = asChild ? Slot : "button"
+ const { isMobile, state } = useSidebar()
+
+ const button = (
+
+ )
+
+ if (!tooltip) {
+ return button
+ }
+
+ if (typeof tooltip === "string") {
+ tooltip = {
+ children: tooltip,
+ }
+ }
+
+ return (
+
+ {button}
+
+
+ )
+ }
+)
+SidebarMenuButton.displayName = "SidebarMenuButton"
+
+const SidebarMenuAction = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps<"button"> & {
+ asChild?: boolean
+ showOnHover?: boolean
+ }
+>(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button"
+
+ return (
+ svg]:size-4 [&>svg]:shrink-0",
+ // Increases the hit area of the button on mobile.
+ "after:absolute after:-inset-2 after:md:hidden",
+ "peer-data-[size=sm]/menu-button:top-1",
+ "peer-data-[size=default]/menu-button:top-1.5",
+ "peer-data-[size=lg]/menu-button:top-2.5",
+ "group-data-[collapsible=icon]:hidden",
+ showOnHover &&
+ "group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0",
+ className
+ )}
+ {...props}
+ />
+ )
+})
+SidebarMenuAction.displayName = "SidebarMenuAction"
+
+const SidebarMenuBadge = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div">
+>(({ className, ...props }, ref) => (
+
+))
+SidebarMenuBadge.displayName = "SidebarMenuBadge"
+
+const SidebarMenuSkeleton = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div"> & {
+ showIcon?: boolean
+ }
+>(({ className, showIcon = false, ...props }, ref) => {
+ // Random width between 50 to 90%.
+ const width = React.useMemo(() => {
+ return `${Math.floor(Math.random() * 40) + 50}%`
+ }, [])
+
+ return (
+
+ {showIcon && (
+
+ )}
+
+
+ )
+})
+SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton"
+
+const SidebarMenuSub = React.forwardRef<
+ HTMLUListElement,
+ React.ComponentProps<"ul">
+>(({ className, ...props }, ref) => (
+
+))
+SidebarMenuSub.displayName = "SidebarMenuSub"
+
+const SidebarMenuSubItem = React.forwardRef<
+ HTMLLIElement,
+ React.ComponentProps<"li">
+>(({ ...props }, ref) => )
+SidebarMenuSubItem.displayName = "SidebarMenuSubItem"
+
+const SidebarMenuSubButton = React.forwardRef<
+ HTMLAnchorElement,
+ React.ComponentProps<"a"> & {
+ asChild?: boolean
+ size?: "sm" | "md"
+ isActive?: boolean
+ }
+>(({ asChild = false, size = "md", isActive, className, ...props }, ref) => {
+ const Comp = asChild ? Slot : "a"
+
+ return (
+ span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
+ "data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
+ size === "sm" && "text-xs",
+ size === "md" && "text-sm",
+ "group-data-[collapsible=icon]:hidden",
+ className
+ )}
+ {...props}
+ />
+ )
+})
+SidebarMenuSubButton.displayName = "SidebarMenuSubButton"
+
+export {
+ Sidebar,
+ SidebarContent,
+ SidebarFooter,
+ SidebarGroup,
+ SidebarGroupAction,
+ SidebarGroupContent,
+ SidebarGroupLabel,
+ SidebarHeader,
+ SidebarInput,
+ SidebarInset,
+ SidebarMenu,
+ SidebarMenuAction,
+ SidebarMenuBadge,
+ SidebarMenuButton,
+ SidebarMenuItem,
+ SidebarMenuSkeleton,
+ SidebarMenuSub,
+ SidebarMenuSubButton,
+ SidebarMenuSubItem,
+ SidebarProvider,
+ SidebarRail,
+ SidebarSeparator,
+ SidebarTrigger,
+ useSidebar,
+}
diff --git a/frontend/components/ui/skeleton.tsx b/frontend/components/ui/skeleton.tsx
new file mode 100644
index 0000000..01b8b6d
--- /dev/null
+++ b/frontend/components/ui/skeleton.tsx
@@ -0,0 +1,15 @@
+import { cn } from "@/lib/utils"
+
+function Skeleton({
+ className,
+ ...props
+}: React.HTMLAttributes) {
+ return (
+