From 06a7cfb55f648983190b933b180559051268288d Mon Sep 17 00:00:00 2001 From: Sosokker Date: Mon, 7 Apr 2025 21:07:56 +0700 Subject: [PATCH] initial frontend commit --- frontend/.gitignore | 41 + frontend/README.md | 36 + frontend/app/favicon.ico | Bin 0 -> 25931 bytes frontend/app/globals.css | 105 + frontend/app/layout.tsx | 20 + .../app/map/components/analytics-overlay.tsx | 114 + .../app/map/components/analytics-panel.tsx | 143 + frontend/app/map/components/area-chart.tsx | 65 + frontend/app/map/components/chat-bot.tsx | 109 + frontend/app/map/components/chat-overlay.tsx | 78 + .../app/map/components/filters-overlay.tsx | 142 + frontend/app/map/components/map-container.tsx | 45 + frontend/app/map/components/map-header.tsx | 32 + frontend/app/map/components/map-sidebar.tsx | 127 + .../app/map/components/overlay-context.tsx | 94 + .../app/map/components/overlay-controls.tsx | 79 + .../app/map/components/overlay-manager.tsx | 59 + .../overlay-system/overlay-context.tsx | 223 + .../overlay-system/overlay-dock.tsx | 50 + .../map/components/overlay-system/overlay.tsx | 156 + .../app/map/components/property-filters.tsx | 167 + frontend/app/map/layout.tsx | 10 + frontend/app/map/page.tsx | 71 + .../components/feature-importance-chart.tsx | 65 + .../components/price-comparison-chart.tsx | 75 + frontend/app/model-explanation/page.tsx | 640 ++ frontend/app/page.tsx | 103 + frontend/components.json | 21 + frontend/components/theme-controller.tsx | 147 + frontend/components/theme-provider.tsx | 12 + frontend/components/theme-toggle.tsx | 47 + frontend/components/ui/accordion.tsx | 58 + frontend/components/ui/alert-dialog.tsx | 141 + frontend/components/ui/alert.tsx | 59 + frontend/components/ui/aspect-ratio.tsx | 7 + frontend/components/ui/avatar.tsx | 50 + frontend/components/ui/badge.tsx | 36 + frontend/components/ui/breadcrumb.tsx | 115 + frontend/components/ui/button.tsx | 56 + frontend/components/ui/calendar.tsx | 66 + frontend/components/ui/card.tsx | 79 + frontend/components/ui/carousel.tsx | 262 + frontend/components/ui/chart.tsx | 30 + frontend/components/ui/checkbox.tsx | 30 + frontend/components/ui/collapsible.tsx | 11 + frontend/components/ui/command.tsx | 153 + frontend/components/ui/context-menu.tsx | 200 + frontend/components/ui/dialog.tsx | 122 + frontend/components/ui/drawer.tsx | 118 + frontend/components/ui/dropdown-menu.tsx | 200 + frontend/components/ui/form.tsx | 178 + frontend/components/ui/hover-card.tsx | 29 + frontend/components/ui/input-otp.tsx | 71 + frontend/components/ui/input.tsx | 22 + frontend/components/ui/label.tsx | 26 + frontend/components/ui/menubar.tsx | 236 + frontend/components/ui/navigation-menu.tsx | 128 + frontend/components/ui/pagination.tsx | 117 + frontend/components/ui/popover.tsx | 31 + frontend/components/ui/progress.tsx | 26 + frontend/components/ui/radio-group.tsx | 44 + frontend/components/ui/resizable.tsx | 45 + frontend/components/ui/scroll-area.tsx | 41 + frontend/components/ui/select.tsx | 160 + frontend/components/ui/separator.tsx | 31 + frontend/components/ui/sheet.tsx | 140 + frontend/components/ui/sidebar.tsx | 763 +++ frontend/components/ui/skeleton.tsx | 15 + frontend/components/ui/slider.tsx | 31 + frontend/components/ui/sonner.tsx | 31 + frontend/components/ui/switch.tsx | 30 + frontend/components/ui/table.tsx | 117 + frontend/components/ui/tabs.tsx | 56 + frontend/components/ui/textarea.tsx | 22 + frontend/components/ui/toast.tsx | 129 + frontend/components/ui/toaster.tsx | 35 + frontend/components/ui/toggle-group.tsx | 61 + frontend/components/ui/toggle.tsx | 45 + frontend/components/ui/tooltip.tsx | 31 + frontend/components/ui/use-mobile.tsx | 19 + frontend/components/ui/use-toast.ts | 194 + frontend/eslint.config.mjs | 16 + frontend/hooks/use-mobile.tsx | 19 + frontend/hooks/use-toast.ts | 194 + frontend/lib/utils.ts | 6 + frontend/next.config.ts | 7 + frontend/package.json | 110 + frontend/pnpm-lock.yaml | 5941 +++++++++++++++++ frontend/postcss.config.mjs | 5 + frontend/public/file.svg | 1 + frontend/public/globe.svg | 1 + frontend/public/next.svg | 1 + frontend/public/vercel.svg | 1 + frontend/public/window.svg | 1 + frontend/tailwind.config.ts | 91 + frontend/tsconfig.json | 27 + 96 files changed, 14194 insertions(+) create mode 100644 frontend/.gitignore create mode 100644 frontend/README.md create mode 100644 frontend/app/favicon.ico create mode 100644 frontend/app/globals.css create mode 100644 frontend/app/layout.tsx create mode 100644 frontend/app/map/components/analytics-overlay.tsx create mode 100644 frontend/app/map/components/analytics-panel.tsx create mode 100644 frontend/app/map/components/area-chart.tsx create mode 100644 frontend/app/map/components/chat-bot.tsx create mode 100644 frontend/app/map/components/chat-overlay.tsx create mode 100644 frontend/app/map/components/filters-overlay.tsx create mode 100644 frontend/app/map/components/map-container.tsx create mode 100644 frontend/app/map/components/map-header.tsx create mode 100644 frontend/app/map/components/map-sidebar.tsx create mode 100644 frontend/app/map/components/overlay-context.tsx create mode 100644 frontend/app/map/components/overlay-controls.tsx create mode 100644 frontend/app/map/components/overlay-manager.tsx create mode 100644 frontend/app/map/components/overlay-system/overlay-context.tsx create mode 100644 frontend/app/map/components/overlay-system/overlay-dock.tsx create mode 100644 frontend/app/map/components/overlay-system/overlay.tsx create mode 100644 frontend/app/map/components/property-filters.tsx create mode 100644 frontend/app/map/layout.tsx create mode 100644 frontend/app/map/page.tsx create mode 100644 frontend/app/model-explanation/components/feature-importance-chart.tsx create mode 100644 frontend/app/model-explanation/components/price-comparison-chart.tsx create mode 100644 frontend/app/model-explanation/page.tsx create mode 100644 frontend/app/page.tsx create mode 100644 frontend/components.json create mode 100644 frontend/components/theme-controller.tsx create mode 100644 frontend/components/theme-provider.tsx create mode 100644 frontend/components/theme-toggle.tsx create mode 100644 frontend/components/ui/accordion.tsx create mode 100644 frontend/components/ui/alert-dialog.tsx create mode 100644 frontend/components/ui/alert.tsx create mode 100644 frontend/components/ui/aspect-ratio.tsx create mode 100644 frontend/components/ui/avatar.tsx create mode 100644 frontend/components/ui/badge.tsx create mode 100644 frontend/components/ui/breadcrumb.tsx create mode 100644 frontend/components/ui/button.tsx create mode 100644 frontend/components/ui/calendar.tsx create mode 100644 frontend/components/ui/card.tsx create mode 100644 frontend/components/ui/carousel.tsx create mode 100644 frontend/components/ui/chart.tsx create mode 100644 frontend/components/ui/checkbox.tsx create mode 100644 frontend/components/ui/collapsible.tsx create mode 100644 frontend/components/ui/command.tsx create mode 100644 frontend/components/ui/context-menu.tsx create mode 100644 frontend/components/ui/dialog.tsx create mode 100644 frontend/components/ui/drawer.tsx create mode 100644 frontend/components/ui/dropdown-menu.tsx create mode 100644 frontend/components/ui/form.tsx create mode 100644 frontend/components/ui/hover-card.tsx create mode 100644 frontend/components/ui/input-otp.tsx create mode 100644 frontend/components/ui/input.tsx create mode 100644 frontend/components/ui/label.tsx create mode 100644 frontend/components/ui/menubar.tsx create mode 100644 frontend/components/ui/navigation-menu.tsx create mode 100644 frontend/components/ui/pagination.tsx create mode 100644 frontend/components/ui/popover.tsx create mode 100644 frontend/components/ui/progress.tsx create mode 100644 frontend/components/ui/radio-group.tsx create mode 100644 frontend/components/ui/resizable.tsx create mode 100644 frontend/components/ui/scroll-area.tsx create mode 100644 frontend/components/ui/select.tsx create mode 100644 frontend/components/ui/separator.tsx create mode 100644 frontend/components/ui/sheet.tsx create mode 100644 frontend/components/ui/sidebar.tsx create mode 100644 frontend/components/ui/skeleton.tsx create mode 100644 frontend/components/ui/slider.tsx create mode 100644 frontend/components/ui/sonner.tsx create mode 100644 frontend/components/ui/switch.tsx create mode 100644 frontend/components/ui/table.tsx create mode 100644 frontend/components/ui/tabs.tsx create mode 100644 frontend/components/ui/textarea.tsx create mode 100644 frontend/components/ui/toast.tsx create mode 100644 frontend/components/ui/toaster.tsx create mode 100644 frontend/components/ui/toggle-group.tsx create mode 100644 frontend/components/ui/toggle.tsx create mode 100644 frontend/components/ui/tooltip.tsx create mode 100644 frontend/components/ui/use-mobile.tsx create mode 100644 frontend/components/ui/use-toast.ts create mode 100644 frontend/eslint.config.mjs create mode 100644 frontend/hooks/use-mobile.tsx create mode 100644 frontend/hooks/use-toast.ts create mode 100644 frontend/lib/utils.ts create mode 100644 frontend/next.config.ts create mode 100644 frontend/package.json create mode 100644 frontend/pnpm-lock.yaml create mode 100644 frontend/postcss.config.mjs create mode 100644 frontend/public/file.svg create mode 100644 frontend/public/globe.svg create mode 100644 frontend/public/next.svg create mode 100644 frontend/public/vercel.svg create mode 100644 frontend/public/window.svg create mode 100644 frontend/tailwind.config.ts create mode 100644 frontend/tsconfig.json 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 0000000000000000000000000000000000000000..718d6fea4835ec2d246af9800eddb7ffb276240c GIT binary patch literal 25931 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 LrJugUA?W`A8`#=m literal 0 HcmV?d00001 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 + +
+
10,000,000 Baht
+
+

Overall Price History of this area

+
+ + + +
+ + + + + + Price Prediction + +
+
15,000,000 Baht
+
+

The estimated price based on various factors.

+
+ + + +
+ +
+ + + + + Flood Factor + +
+
+ Moderate +
+
+
+ + + + + + Air Factor + +
+
+ Bad +
+
+
+
+ + + + + + 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 + + + + ) + } + + return ( + + + + + Analytics + +
+ + +
+
+ + +
+

Information in radius will be analyzed

+ + + + + + Area Price History + +
+
10,000,000 Baht
+
+

Overall Price History of this area

+
+ + + +
+ + + + + + Price Prediction + +
+
15,000,000 Baht
+
+

The estimated price based on various factors.

+
+ + + +
+ +
+ + + + + Flood Factor + +
+
+ Moderate +
+
+
+ + + + + + Air Factor + +
+
+ Bad +
+
+
+
+ + + + + + 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 +
+ + +
+
+
+ ) + } + + return ( + + + ChatBot +
+ + +
+
+ + +
+ {chatHistory.map((chat, index) => ( +
+
+ {chat.content} +
+
+ ))} +
+
+
+ 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) => ( +
+
+ {chat.content} +
+
+ ))} +
+
+
+ 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 + + + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ + + {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])} + +
+ +
+ +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+
+ + +
+
+ ) +} + 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 ( +
+
+ + Tools + + + Map +
+ +
+ + + + +
+
+ ) +} + 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 ( + + ) +} + +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 ( + +
+ + + + + + {overlays.analytics.visible ? "Hide analytics" : "Show analytics"} + + + + + + + + {overlays.filters.visible ? "Hide filters" : "Show filters"} + + + + + + + + Move analytics to the {overlays.analytics.position === "right" ? "left" : "right"} + + + + {!overlays.chat.visible && ( + + + + + 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) => ( +
+ + + + + + {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 + + + + ) + } + + return ( + + + Property Filters + + + + + + + Basic + Advanced + + + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ + + {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])} + +
+ +
+ +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+
+ + +
+
+
+ ) +} + 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

+
+ + + +
+
+ + {/* 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 */} +
+
+ + Map + + + Price Prediction Model +
+
+ + {/* 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) => ( + + ))} +
+
+ +
+
+ + {/* 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 + + +
+
+ + {propertySize} sqm +
+ setPropertySize(value[0])} + /> +
+ +
+
+ + {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 +

+
+
+
+
+
+ + + +
+ )} + + {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}

+
+ ))} +
+
+
+ + + + +
+ )} + + {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)} +
+
+ ))} +
+
+
+
+ + + + +
+ )} + + {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

+
+
+ Moderate +
+

+ Historical data shows moderate flood risk in this area +

+
+ +
+ +

Air Quality

+
+
+ Poor +
+

+ Air quality is below average, affecting property value +

+
+ +
+ +

Noise Level

+
+
+ Low +
+

+ The area has relatively low noise pollution +

+
+
+ +
+

Proximity to Amenities

+
+
+ +
Public Transport: 300m
+
+
+ +
Schools: 1.2km
+
+
+ +
Shopping: 500m
+
+
+ +
Hospitals: 2.5km
+
+
+
+
+
+ + + + +
+ )} + + {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 +
  • +
+
+
+
+ + + + +
+ )} +
+
+
+
+
+
+
+
+ ) +} + 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 ( +
+
+ Next.js logo +
    +
  1. + Get started by editing{" "} + + app/page.tsx + + . +
  2. +
  3. + Save and see your changes instantly. +
  4. +
+ + +
+ +
+ ); +} 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" + > +
+
+ {scheme.name} +
+ {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 ( + + + + + + + + + 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) =>