mirror of
https://github.com/borbann-platform/backend-api.git
synced 2025-12-19 12:44:04 +01:00
refactor: remove unrelated files and restructure
This commit is contained in:
parent
ae15103e1f
commit
30d135ba0e
407
frontend/app/(routes)/data-pipeline/create/page.tsx
Normal file
407
frontend/app/(routes)/data-pipeline/create/page.tsx
Normal file
@ -0,0 +1,407 @@
|
|||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
|
import { Label } from "@/components/ui/label"
|
||||||
|
import { Textarea } from "@/components/ui/textarea"
|
||||||
|
import { ArrowLeft, Globe, FileUp, DatabaseIcon, Plus, Trash2, BrainCircuit } from "lucide-react"
|
||||||
|
import Link from "next/link"
|
||||||
|
import PageHeader from "@/components/page-header"
|
||||||
|
import { Badge } from "@/components/ui/badge"
|
||||||
|
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"
|
||||||
|
import { Switch } from "@/components/ui/switch"
|
||||||
|
|
||||||
|
export default function CreatePipelinePage() {
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto p-6">
|
||||||
|
<PageHeader
|
||||||
|
title="Create Data Pipeline"
|
||||||
|
description="Set up a new automated data collection pipeline"
|
||||||
|
breadcrumb={[
|
||||||
|
{ title: "Home", href: "/" },
|
||||||
|
{ title: "Data Pipeline", href: "/data-pipeline" },
|
||||||
|
{ title: "Create", href: "/data-pipeline/create" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="mt-6">
|
||||||
|
<Link href="/data-pipeline">
|
||||||
|
<Button variant="outline" className="mb-6">
|
||||||
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||||
|
Back to Pipelines
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<div className="grid gap-6 md:grid-cols-2">
|
||||||
|
<div>
|
||||||
|
<Card className="border-2 hover:border-highlight-border transition-all duration-200">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Pipeline Details</CardTitle>
|
||||||
|
<CardDescription>Basic information about your data pipeline</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="name">Pipeline Name</Label>
|
||||||
|
<Input id="name" placeholder="e.g., Property Listings Pipeline" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="description">Description</Label>
|
||||||
|
<Textarea
|
||||||
|
id="description"
|
||||||
|
placeholder="Describe what this pipeline collects and how it will be used"
|
||||||
|
rows={4}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="tags">Tags (optional)</Label>
|
||||||
|
<Input id="tags" placeholder="e.g., real-estate, properties, listings" />
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">Separate tags with commas</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="mt-6 border-2 hover:border-highlight-border transition-all duration-200">
|
||||||
|
<CardHeader>
|
||||||
|
<div>
|
||||||
|
<CardTitle className="text-lg flex items-center">
|
||||||
|
<BrainCircuit className="mr-2 h-5 w-5 text-primary" />
|
||||||
|
AI Assistant
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>Customize how AI processes your data</CardDescription>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="ai-prompt">Additional Instructions for AI</Label>
|
||||||
|
<Textarea
|
||||||
|
id="ai-prompt"
|
||||||
|
placeholder="E.g., Focus on extracting pricing trends, ignore promotional content, prioritize property features..."
|
||||||
|
rows={4}
|
||||||
|
className="border-primary/20"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
Provide specific instructions to guide the AI in processing your data sources
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3 pt-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<Label htmlFor="detect-fields">Auto-detect common fields</Label>
|
||||||
|
<p className="text-xs text-muted-foreground">Automatically identify price, location, etc.</p>
|
||||||
|
</div>
|
||||||
|
<Switch id="detect-fields" defaultChecked />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<Label htmlFor="suggest-mappings">Suggest field mappings</Label>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Get AI suggestions for matching fields across sources
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch id="suggest-mappings" defaultChecked />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<Label htmlFor="deduplicate">Deduplicate records</Label>
|
||||||
|
<p className="text-xs text-muted-foreground">Remove duplicate entries automatically</p>
|
||||||
|
</div>
|
||||||
|
<Switch id="deduplicate" defaultChecked />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Card className="border-2 hover:border-highlight-border transition-all duration-200">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Data Sources</CardTitle>
|
||||||
|
<CardDescription>Add one or more data sources to your pipeline</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Accordion type="single" collapsible className="w-full" defaultValue="source-1">
|
||||||
|
<AccordionItem value="source-1" className="border rounded-md mb-4 data-source-card active">
|
||||||
|
<AccordionTrigger className="px-4">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Globe className="mr-2 h-5 w-5 text-primary" />
|
||||||
|
<span>Website Source #1</span>
|
||||||
|
</div>
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent className="px-4 pb-4">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="url-1">Website URL</Label>
|
||||||
|
<Input id="url-1" placeholder="https://example.com/listings" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label htmlFor="additional-urls-1">Additional URLs (optional)</Label>
|
||||||
|
<Badge variant="outline" className="text-xs">
|
||||||
|
Pattern Detection
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<Textarea
|
||||||
|
id="additional-urls-1"
|
||||||
|
placeholder="https://example.com/listings/page2 https://example.com/listings/page3"
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
Add multiple URLs from the same website (one per line)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button variant="outline" size="sm" className="text-destructive">
|
||||||
|
<Trash2 className="mr-2 h-4 w-4" />
|
||||||
|
Remove Source
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
|
||||||
|
<AccordionItem value="source-2" className="border rounded-md mb-4 data-source-card">
|
||||||
|
<AccordionTrigger className="px-4">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<FileUp className="mr-2 h-5 w-5 text-primary" />
|
||||||
|
<span>File Upload Source #1</span>
|
||||||
|
</div>
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent className="px-4 pb-4">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="file-upload-1">Upload File</Label>
|
||||||
|
<div className="flex items-center justify-center p-6 border-2 border-dashed rounded-lg">
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Drag and drop your file here, or click to browse
|
||||||
|
</p>
|
||||||
|
<Button variant="outline" className="mt-2">
|
||||||
|
Select File
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
Supported formats: CSV, JSON, Excel, XML (up to 50MB)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button variant="outline" size="sm" className="text-destructive">
|
||||||
|
<Trash2 className="mr-2 h-4 w-4" />
|
||||||
|
Remove Source
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
|
||||||
|
<AccordionItem value="source-3" className="border rounded-md mb-4 data-source-card">
|
||||||
|
<AccordionTrigger className="px-4">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<DatabaseIcon className="mr-2 h-5 w-5 text-primary" />
|
||||||
|
<span>API Source #1</span>
|
||||||
|
</div>
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent className="px-4 pb-4">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="api-url-1">API Endpoint URL</Label>
|
||||||
|
<Input id="api-url-1" placeholder="https://api.example.com/data" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="auth-type-1">Authentication Type</Label>
|
||||||
|
<select
|
||||||
|
id="auth-type-1"
|
||||||
|
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
>
|
||||||
|
<option value="none">None</option>
|
||||||
|
<option value="basic">Basic Auth</option>
|
||||||
|
<option value="bearer">Bearer Token</option>
|
||||||
|
<option value="api-key">API Key</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button variant="outline" size="sm" className="text-destructive">
|
||||||
|
<Trash2 className="mr-2 h-4 w-4" />
|
||||||
|
Remove Source
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2 mt-4">
|
||||||
|
<Button variant="outline" className="w-full justify-start gap-2">
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
Add Website Source
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" className="w-full justify-start gap-2">
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
Add File Upload Source
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" className="w-full justify-start gap-2">
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
Add API Source
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="mt-6 border-2 hover:border-highlight-border transition-all duration-200">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Schedule & Automation</CardTitle>
|
||||||
|
<CardDescription>Configure when and how your pipeline should run</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid gap-6 md:grid-cols-2">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="frequency">Run Frequency</Label>
|
||||||
|
<select
|
||||||
|
id="frequency"
|
||||||
|
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
>
|
||||||
|
<option value="manual">Manual (Run on demand)</option>
|
||||||
|
<option value="hourly">Hourly</option>
|
||||||
|
<option value="daily">Daily</option>
|
||||||
|
<option value="weekly">Weekly</option>
|
||||||
|
<option value="monthly">Monthly</option>
|
||||||
|
<option value="custom">Custom Schedule</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="timezone">Timezone</Label>
|
||||||
|
<select
|
||||||
|
id="timezone"
|
||||||
|
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
>
|
||||||
|
<option value="utc">UTC</option>
|
||||||
|
<option value="est">Eastern Time (ET)</option>
|
||||||
|
<option value="cst">Central Time (CT)</option>
|
||||||
|
<option value="mst">Mountain Time (MT)</option>
|
||||||
|
<option value="pst">Pacific Time (PT)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="max-records">Collection Limits</Label>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="limit-records"
|
||||||
|
className="h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary"
|
||||||
|
/>
|
||||||
|
<div className="flex-1">
|
||||||
|
<Label htmlFor="limit-records" className="text-sm font-normal">
|
||||||
|
Limit total records
|
||||||
|
</Label>
|
||||||
|
<Input id="max-records" type="number" placeholder="e.g., 1000" className="mt-1" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="stop-no-new"
|
||||||
|
className="h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary"
|
||||||
|
/>
|
||||||
|
<Label htmlFor="stop-no-new" className="text-sm font-normal">
|
||||||
|
Stop when no new records found
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="notifications">Notifications</Label>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="notify-complete"
|
||||||
|
className="h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary"
|
||||||
|
defaultChecked
|
||||||
|
/>
|
||||||
|
<Label htmlFor="notify-complete" className="text-sm font-normal">
|
||||||
|
Notify when pipeline completes
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="notify-error"
|
||||||
|
className="h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary"
|
||||||
|
defaultChecked
|
||||||
|
/>
|
||||||
|
<Label htmlFor="notify-error" className="text-sm font-normal">
|
||||||
|
Notify on errors
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2">
|
||||||
|
<Input id="email" type="email" placeholder="Email for notifications" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="retry-settings">Retry Settings</Label>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label htmlFor="retry-attempts" className="text-sm font-normal">
|
||||||
|
Retry Attempts
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="retry-attempts"
|
||||||
|
type="number"
|
||||||
|
placeholder="e.g., 3"
|
||||||
|
defaultValue="3"
|
||||||
|
className="w-24"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label htmlFor="retry-delay" className="text-sm font-normal">
|
||||||
|
Delay Between Retries (minutes)
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="retry-delay"
|
||||||
|
type="number"
|
||||||
|
placeholder="e.g., 5"
|
||||||
|
defaultValue="5"
|
||||||
|
className="w-24"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 flex justify-end space-x-4">
|
||||||
|
<Button variant="outline">Save as Draft</Button>
|
||||||
|
<Button>Create Pipeline</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
242
frontend/app/(routes)/data-pipeline/page.tsx
Normal file
242
frontend/app/(routes)/data-pipeline/page.tsx
Normal file
@ -0,0 +1,242 @@
|
|||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||||
|
import { Badge } from "@/components/ui/badge"
|
||||||
|
import { Clock, Database, Play, Plus, RefreshCw, Pause, AlertTriangle, Copy } from "lucide-react"
|
||||||
|
import Link from "next/link"
|
||||||
|
import PageHeader from "@/components/page-header"
|
||||||
|
|
||||||
|
export default function DataPipelinePage() {
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto p-6">
|
||||||
|
<PageHeader
|
||||||
|
title="Data Pipelines"
|
||||||
|
description="Manage your automated data collection pipelines"
|
||||||
|
breadcrumb={[
|
||||||
|
{ title: "Home", href: "/" },
|
||||||
|
{ title: "Data Pipeline", href: "/data-pipeline" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex justify-between items-center mt-6">
|
||||||
|
<Tabs defaultValue="active" className="w-full">
|
||||||
|
<TabsList>
|
||||||
|
<TabsTrigger value="active">Active Pipelines</TabsTrigger>
|
||||||
|
<TabsTrigger value="paused">Paused</TabsTrigger>
|
||||||
|
<TabsTrigger value="all">All Pipelines</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<div className="flex justify-end mt-4">
|
||||||
|
<Link href="/data-pipeline/create">
|
||||||
|
<Button className="gap-2">
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
Create Pipeline
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TabsContent value="active" className="mt-4">
|
||||||
|
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
<PipelineCard
|
||||||
|
title="Property Listings"
|
||||||
|
description="Scrapes real estate listings from multiple websites"
|
||||||
|
status="active"
|
||||||
|
lastRun="2 hours ago"
|
||||||
|
nextRun="Tomorrow at 9:00 AM"
|
||||||
|
sources={3}
|
||||||
|
records={1240}
|
||||||
|
aiPowered={true}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PipelineCard
|
||||||
|
title="Rental Market Data"
|
||||||
|
description="Collects rental prices and availability"
|
||||||
|
status="active"
|
||||||
|
lastRun="Yesterday"
|
||||||
|
nextRun="In 3 days"
|
||||||
|
sources={2}
|
||||||
|
records={830}
|
||||||
|
aiPowered={true}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PipelineCard
|
||||||
|
title="Price Comparison"
|
||||||
|
description="Tracks property price changes over time"
|
||||||
|
status="error"
|
||||||
|
lastRun="2 days ago"
|
||||||
|
nextRun="Scheduled retry in 12 hours"
|
||||||
|
sources={4}
|
||||||
|
records={1560}
|
||||||
|
error="Connection timeout on 1 source"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="paused" className="mt-4">
|
||||||
|
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
<PipelineCard
|
||||||
|
title="Commercial Properties"
|
||||||
|
description="Collects data on commercial real estate"
|
||||||
|
status="paused"
|
||||||
|
lastRun="1 week ago"
|
||||||
|
nextRun="Paused"
|
||||||
|
sources={2}
|
||||||
|
records={450}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="all" className="mt-4">
|
||||||
|
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
<PipelineCard
|
||||||
|
title="Property Listings"
|
||||||
|
description="Scrapes real estate listings from multiple websites"
|
||||||
|
status="active"
|
||||||
|
lastRun="2 hours ago"
|
||||||
|
nextRun="Tomorrow at 9:00 AM"
|
||||||
|
sources={3}
|
||||||
|
records={1240}
|
||||||
|
aiPowered={true}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PipelineCard
|
||||||
|
title="Rental Market Data"
|
||||||
|
description="Collects rental prices and availability"
|
||||||
|
status="active"
|
||||||
|
lastRun="Yesterday"
|
||||||
|
nextRun="In 3 days"
|
||||||
|
sources={2}
|
||||||
|
records={830}
|
||||||
|
aiPowered={true}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PipelineCard
|
||||||
|
title="Price Comparison"
|
||||||
|
description="Tracks property price changes over time"
|
||||||
|
status="error"
|
||||||
|
lastRun="2 days ago"
|
||||||
|
nextRun="Scheduled retry in 12 hours"
|
||||||
|
sources={4}
|
||||||
|
records={1560}
|
||||||
|
error="Connection timeout on 1 source"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PipelineCard
|
||||||
|
title="Commercial Properties"
|
||||||
|
description="Collects data on commercial real estate"
|
||||||
|
status="paused"
|
||||||
|
lastRun="1 week ago"
|
||||||
|
nextRun="Paused"
|
||||||
|
sources={2}
|
||||||
|
records={450}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PipelineCardProps {
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
status: "active" | "paused" | "error"
|
||||||
|
lastRun: string
|
||||||
|
nextRun: string
|
||||||
|
sources: number
|
||||||
|
records: number
|
||||||
|
error?: string
|
||||||
|
aiPowered?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
function PipelineCard({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
status,
|
||||||
|
lastRun,
|
||||||
|
nextRun,
|
||||||
|
sources,
|
||||||
|
records,
|
||||||
|
error,
|
||||||
|
aiPowered,
|
||||||
|
}: PipelineCardProps) {
|
||||||
|
return (
|
||||||
|
<Card className={`pipeline-card ${status === "active" ? "border-2 border-green-500 dark:border-green-600" : ""}`}>
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<div className="flex justify-between items-start">
|
||||||
|
<CardTitle className="text-lg">{title}</CardTitle>
|
||||||
|
<StatusBadge status={status} />
|
||||||
|
</div>
|
||||||
|
<CardDescription>{description}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center text-sm">
|
||||||
|
<Clock className="h-4 w-4 mr-2 text-muted-foreground" />
|
||||||
|
<span className="text-muted-foreground">Last run:</span>
|
||||||
|
<span className="ml-1 font-medium">{lastRun}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center text-sm">
|
||||||
|
<RefreshCw className="h-4 w-4 mr-2 text-muted-foreground" />
|
||||||
|
<span className="text-muted-foreground">Next run:</span>
|
||||||
|
<span className="ml-1 font-medium">{nextRun}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center text-sm">
|
||||||
|
<Database className="h-4 w-4 mr-2 text-muted-foreground" />
|
||||||
|
<span className="text-muted-foreground">Sources:</span>
|
||||||
|
<span className="ml-1 font-medium">{sources}</span>
|
||||||
|
<span className="mx-2">•</span>
|
||||||
|
<span className="text-muted-foreground">Records:</span>
|
||||||
|
<span className="ml-1 font-medium">{records}</span>
|
||||||
|
</div>
|
||||||
|
{error && (
|
||||||
|
<div className="flex items-center text-sm text-destructive mt-2">
|
||||||
|
<AlertTriangle className="h-4 w-4 mr-2" />
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter className="flex justify-between">
|
||||||
|
<Link href={`/data-pipeline/${title.toLowerCase().replace(/\s+/g, "-")}`}>
|
||||||
|
<Button variant="outline" size="sm">
|
||||||
|
View Details
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button variant="outline" size="icon" className="h-8 w-8 text-primary border-primary/20 hover:border-primary">
|
||||||
|
<Copy className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
{status === "active" ? (
|
||||||
|
<Button variant="outline" size="icon" className="h-8 w-8 border-primary/20 hover:border-primary">
|
||||||
|
<Pause className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button variant="outline" size="icon" className="h-8 w-8 border-primary/20 hover:border-primary">
|
||||||
|
<Play className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button variant="outline" size="icon" className="h-8 w-8 border-primary/20 hover:border-primary">
|
||||||
|
<RefreshCw className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function StatusBadge({ status }: { status: "active" | "paused" | "error" }) {
|
||||||
|
if (status === "active") {
|
||||||
|
return (
|
||||||
|
<Badge variant="default" className="bg-green-500 hover:bg-green-600 dark:bg-green-600 dark:hover:bg-green-700">
|
||||||
|
Active
|
||||||
|
</Badge>
|
||||||
|
)
|
||||||
|
} else if (status === "paused") {
|
||||||
|
return <Badge variant="secondary">Paused</Badge>
|
||||||
|
} else {
|
||||||
|
return <Badge variant="destructive">Error</Badge>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
687
frontend/app/(routes)/data-pipeline/property-listings/page.tsx
Normal file
687
frontend/app/(routes)/data-pipeline/property-listings/page.tsx
Normal file
@ -0,0 +1,687 @@
|
|||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||||
|
import { Badge } from "@/components/ui/badge"
|
||||||
|
import { ArrowLeft, Download, Edit, Play, Trash, Copy, Check, Plus } from "lucide-react"
|
||||||
|
import Link from "next/link"
|
||||||
|
import PageHeader from "@/components/page-header"
|
||||||
|
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
|
import { Label } from "@/components/ui/label"
|
||||||
|
|
||||||
|
export default function PipelineDetailsPage() {
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto p-6">
|
||||||
|
<PageHeader
|
||||||
|
title="Property Listings Pipeline"
|
||||||
|
breadcrumb={[
|
||||||
|
{ title: "Home", href: "/" },
|
||||||
|
{ title: "Data Pipeline", href: "/data-pipeline" },
|
||||||
|
{ title: "Property Listings", href: "/data-pipeline/property-listings" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex justify-between items-center mt-6">
|
||||||
|
<Link href="/data-pipeline">
|
||||||
|
<Button variant="outline">
|
||||||
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||||
|
Back to Pipelines
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<Button variant="outline" className="gap-2 border-primary/20 hover:border-primary">
|
||||||
|
<Copy className="h-4 w-4" />
|
||||||
|
Clone
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" className="gap-2 border-primary/20 hover:border-primary">
|
||||||
|
<Edit className="h-4 w-4" />
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" className="gap-2 border-primary/20 hover:border-primary">
|
||||||
|
<Play className="h-4 w-4" />
|
||||||
|
Run Now
|
||||||
|
</Button>
|
||||||
|
<Button variant="destructive" size="icon">
|
||||||
|
<Trash className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-6 md:grid-cols-3 mt-6">
|
||||||
|
<Card className="border-2 border-green-500 dark:border-green-600">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-lg">Pipeline Status</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-muted-foreground">Status:</span>
|
||||||
|
<Badge
|
||||||
|
variant="default"
|
||||||
|
className="bg-green-500 hover:bg-green-600 dark:bg-green-600 dark:hover:bg-green-700"
|
||||||
|
>
|
||||||
|
Active
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-muted-foreground">Last Run:</span>
|
||||||
|
<span>2 hours ago</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-muted-foreground">Next Run:</span>
|
||||||
|
<span>Tomorrow at 9:00 AM</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-muted-foreground">Run Frequency:</span>
|
||||||
|
<span>Daily</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-muted-foreground">Total Records:</span>
|
||||||
|
<span>1,240</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="border-2 hover:border-highlight-border transition-all duration-200">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-lg">Data Sources</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="p-3 border-2 rounded-md hover:border-highlight-border transition-all duration-200">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="font-medium">example-realty.com</span>
|
||||||
|
<Badge>Website</Badge>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">Last updated: 2 hours ago</p>
|
||||||
|
<p className="text-sm mt-1">540 records</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-3 border-2 rounded-md hover:border-highlight-border transition-all duration-200">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="font-medium">property-listings.com</span>
|
||||||
|
<Badge>Website</Badge>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">Last updated: 2 hours ago</p>
|
||||||
|
<p className="text-sm mt-1">420 records</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-3 border-2 rounded-md hover:border-highlight-border transition-all duration-200">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="font-medium">real-estate-api.com</span>
|
||||||
|
<Badge>API</Badge>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">Last updated: 2 hours ago</p>
|
||||||
|
<p className="text-sm mt-1">280 records</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="border-2 hover:border-highlight-border transition-all duration-200">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-lg">Export Options</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Accordion type="single" collapsible className="w-full" defaultValue="format-1">
|
||||||
|
<AccordionItem value="format-1" className="border-0">
|
||||||
|
<div className="border rounded-md p-2 hover:border-highlight-border transition-all duration-200">
|
||||||
|
<AccordionTrigger className="py-1 px-2">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Download className="mr-2 h-4 w-4 text-primary" />
|
||||||
|
<span>Export as JSON</span>
|
||||||
|
</div>
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent className="pt-2 pb-1 px-2">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<input type="checkbox" id="pretty-json" className="h-4 w-4" defaultChecked />
|
||||||
|
<label htmlFor="pretty-json" className="text-sm">
|
||||||
|
Pretty print
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<Button size="sm" className="w-full">
|
||||||
|
Download JSON
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</AccordionContent>
|
||||||
|
</div>
|
||||||
|
</AccordionItem>
|
||||||
|
|
||||||
|
<AccordionItem value="format-2" className="border-0">
|
||||||
|
<div className="border rounded-md p-2 hover:border-highlight-border transition-all duration-200">
|
||||||
|
<AccordionTrigger className="py-1 px-2">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Download className="mr-2 h-4 w-4 text-primary" />
|
||||||
|
<span>Export as CSV</span>
|
||||||
|
</div>
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent className="pt-2 pb-1 px-2">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<input type="checkbox" id="include-headers" className="h-4 w-4" defaultChecked />
|
||||||
|
<label htmlFor="include-headers" className="text-sm">
|
||||||
|
Include headers
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<Button size="sm" className="w-full">
|
||||||
|
Download CSV
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</AccordionContent>
|
||||||
|
</div>
|
||||||
|
</AccordionItem>
|
||||||
|
|
||||||
|
<AccordionItem value="format-3" className="border-0">
|
||||||
|
<div className="border rounded-md p-2 hover:border-highlight-border transition-all duration-200">
|
||||||
|
<AccordionTrigger className="py-1 px-2">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Download className="mr-2 h-4 w-4 text-primary" />
|
||||||
|
<span>Export as SQLite</span>
|
||||||
|
</div>
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent className="pt-2 pb-1 px-2">
|
||||||
|
<Button size="sm" className="w-full">
|
||||||
|
Download SQLite
|
||||||
|
</Button>
|
||||||
|
</AccordionContent>
|
||||||
|
</div>
|
||||||
|
</AccordionItem>
|
||||||
|
|
||||||
|
<AccordionItem value="format-4" className="border-0">
|
||||||
|
<div className="border rounded-md p-2 hover:border-highlight-border transition-all duration-200">
|
||||||
|
<AccordionTrigger className="py-1 px-2">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Download className="mr-2 h-4 w-4 text-primary" />
|
||||||
|
<span>Export as YAML</span>
|
||||||
|
</div>
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent className="pt-2 pb-1 px-2">
|
||||||
|
<Button size="sm" className="w-full">
|
||||||
|
Download YAML
|
||||||
|
</Button>
|
||||||
|
</AccordionContent>
|
||||||
|
</div>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6">
|
||||||
|
<Tabs defaultValue="schema">
|
||||||
|
<TabsList>
|
||||||
|
<TabsTrigger value="schema">Data Schema</TabsTrigger>
|
||||||
|
<TabsTrigger value="preview">Data Preview</TabsTrigger>
|
||||||
|
<TabsTrigger value="output">Output Configuration</TabsTrigger>
|
||||||
|
<TabsTrigger value="history">Run History</TabsTrigger>
|
||||||
|
<TabsTrigger value="settings">Settings</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value="schema" className="mt-4">
|
||||||
|
<Card className="border-2 hover:border-highlight-border transition-all duration-200">
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<div>
|
||||||
|
<CardTitle>Data Schema & Field Management</CardTitle>
|
||||||
|
<CardDescription>Customize fields detected from your data sources</CardDescription>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="text-sm font-medium">Detected Fields</h3>
|
||||||
|
<Button variant="outline" size="sm">
|
||||||
|
Refresh Detection
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border rounded-md p-4">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="field-mapping-item flex items-center">
|
||||||
|
<input type="checkbox" id="field-title" className="h-4 w-4 mr-3" defaultChecked />
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Label htmlFor="field-title" className="font-medium">
|
||||||
|
Title
|
||||||
|
</Label>
|
||||||
|
<Badge className="ml-2 bg-green-500 text-white">Auto-detected</Badge>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">Property title or name</p>
|
||||||
|
</div>
|
||||||
|
<select className="h-8 rounded-md border border-input bg-background px-2 py-1 text-xs">
|
||||||
|
<option>String</option>
|
||||||
|
<option>Number</option>
|
||||||
|
<option>Boolean</option>
|
||||||
|
<option>Date</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="field-mapping-item flex items-center">
|
||||||
|
<input type="checkbox" id="field-price" className="h-4 w-4 mr-3" defaultChecked />
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Label htmlFor="field-price" className="font-medium">
|
||||||
|
Price
|
||||||
|
</Label>
|
||||||
|
<Badge className="ml-2 bg-green-500 text-white">Auto-detected</Badge>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">Property price</p>
|
||||||
|
</div>
|
||||||
|
<select className="h-8 rounded-md border border-input bg-background px-2 py-1 text-xs">
|
||||||
|
<option>Number</option>
|
||||||
|
<option>String</option>
|
||||||
|
<option>Boolean</option>
|
||||||
|
<option>Date</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="field-mapping-item flex items-center">
|
||||||
|
<input type="checkbox" id="field-location" className="h-4 w-4 mr-3" defaultChecked />
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Label htmlFor="field-location" className="font-medium">
|
||||||
|
Location
|
||||||
|
</Label>
|
||||||
|
<Badge className="ml-2 bg-green-500 text-white">Auto-detected</Badge>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">Property location</p>
|
||||||
|
</div>
|
||||||
|
<select className="h-8 rounded-md border border-input bg-background px-2 py-1 text-xs">
|
||||||
|
<option>String</option>
|
||||||
|
<option>Number</option>
|
||||||
|
<option>Boolean</option>
|
||||||
|
<option>Date</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="field-mapping-item flex items-center">
|
||||||
|
<input type="checkbox" id="field-bedrooms" className="h-4 w-4 mr-3" defaultChecked />
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Label htmlFor="field-bedrooms" className="font-medium">
|
||||||
|
Bedrooms
|
||||||
|
</Label>
|
||||||
|
<Badge className="ml-2 bg-green-500 text-white">Auto-detected</Badge>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">Number of bedrooms</p>
|
||||||
|
</div>
|
||||||
|
<select className="h-8 rounded-md border border-input bg-background px-2 py-1 text-xs">
|
||||||
|
<option>Number</option>
|
||||||
|
<option>String</option>
|
||||||
|
<option>Boolean</option>
|
||||||
|
<option>Date</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="field-mapping-item flex items-center">
|
||||||
|
<input type="checkbox" id="field-bathrooms" className="h-4 w-4 mr-3" defaultChecked />
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Label htmlFor="field-bathrooms" className="font-medium">
|
||||||
|
Bathrooms
|
||||||
|
</Label>
|
||||||
|
<Badge className="ml-2 bg-green-500 text-white">Auto-detected</Badge>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">Number of bathrooms</p>
|
||||||
|
</div>
|
||||||
|
<select className="h-8 rounded-md border border-input bg-background px-2 py-1 text-xs">
|
||||||
|
<option>Number</option>
|
||||||
|
<option>String</option>
|
||||||
|
<option>Boolean</option>
|
||||||
|
<option>Date</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="field-mapping-item flex items-center border-dashed">
|
||||||
|
<input type="checkbox" id="field-custom" className="h-4 w-4 mr-3" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<Input placeholder="Add custom field" className="border-none text-sm p-0 h-6" />
|
||||||
|
</div>
|
||||||
|
<Button variant="ghost" size="sm" className="h-8 px-2">
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2 mt-4">
|
||||||
|
<Label htmlFor="derived-fields">Derived Fields</Label>
|
||||||
|
<Card className="border border-dashed">
|
||||||
|
<CardHeader className="py-3">
|
||||||
|
<CardTitle className="text-sm">Create calculated fields</CardTitle>
|
||||||
|
<CardDescription>Use formulas to generate new fields from existing data</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="py-0">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="field-mapping-item">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label className="font-medium">Price Per Square Foot</Label>
|
||||||
|
<Badge variant="outline">Derived</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center mt-2">
|
||||||
|
<span className="text-xs text-muted-foreground mr-2">Formula:</span>
|
||||||
|
<code className="text-xs bg-muted/50 p-1 rounded">price / squareFeet</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button variant="outline" size="sm" className="w-full">
|
||||||
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
|
Add Derived Field
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button variant="outline" className="gap-2 mr-2">
|
||||||
|
Reset to Default
|
||||||
|
</Button>
|
||||||
|
<Button>Save Field Configuration</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="preview" className="mt-4">
|
||||||
|
<Card className="border-2 hover:border-highlight-border transition-all duration-200">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Data Preview</CardTitle>
|
||||||
|
<CardDescription>Sample of the collected data</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="border rounded-md overflow-x-auto">
|
||||||
|
<table className="w-full">
|
||||||
|
<thead className="bg-muted">
|
||||||
|
<tr>
|
||||||
|
<th className="px-4 py-2 text-left text-sm font-medium">ID</th>
|
||||||
|
<th className="px-4 py-2 text-left text-sm font-medium">Title</th>
|
||||||
|
<th className="px-4 py-2 text-left text-sm font-medium">Price</th>
|
||||||
|
<th className="px-4 py-2 text-left text-sm font-medium">Bedrooms</th>
|
||||||
|
<th className="px-4 py-2 text-left text-sm font-medium">Bathrooms</th>
|
||||||
|
<th className="px-4 py-2 text-left text-sm font-medium">Location</th>
|
||||||
|
<th className="px-4 py-2 text-left text-sm font-medium">Sq. Ft.</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y">
|
||||||
|
<tr>
|
||||||
|
<td className="px-4 py-2 text-sm">P001</td>
|
||||||
|
<td className="px-4 py-2 text-sm">Modern Apartment</td>
|
||||||
|
<td className="px-4 py-2 text-sm">$350,000</td>
|
||||||
|
<td className="px-4 py-2 text-sm">2</td>
|
||||||
|
<td className="px-4 py-2 text-sm">2</td>
|
||||||
|
<td className="px-4 py-2 text-sm">Downtown</td>
|
||||||
|
<td className="px-4 py-2 text-sm">1,200</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className="px-4 py-2 text-sm">P002</td>
|
||||||
|
<td className="px-4 py-2 text-sm">Luxury Villa</td>
|
||||||
|
<td className="px-4 py-2 text-sm">$1,250,000</td>
|
||||||
|
<td className="px-4 py-2 text-sm">5</td>
|
||||||
|
<td className="px-4 py-2 text-sm">4</td>
|
||||||
|
<td className="px-4 py-2 text-sm">Suburbs</td>
|
||||||
|
<td className="px-4 py-2 text-sm">3,500</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className="px-4 py-2 text-sm">P003</td>
|
||||||
|
<td className="px-4 py-2 text-sm">Cozy Studio</td>
|
||||||
|
<td className="px-4 py-2 text-sm">$180,000</td>
|
||||||
|
<td className="px-4 py-2 text-sm">1</td>
|
||||||
|
<td className="px-4 py-2 text-sm">1</td>
|
||||||
|
<td className="px-4 py-2 text-sm">City Center</td>
|
||||||
|
<td className="px-4 py-2 text-sm">650</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="output" className="mt-4">
|
||||||
|
<Card className="border-2 hover:border-highlight-border transition-all duration-200">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Output Configuration</CardTitle>
|
||||||
|
<CardDescription>Configure how your data will be structured and exported</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Output Format</Label>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<div className="border rounded-md p-3 data-source-card active">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="font-medium">JSON</span>
|
||||||
|
<Check className="h-4 w-4 text-primary" />
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">Structured data format</p>
|
||||||
|
</div>
|
||||||
|
<div className="border rounded-md p-3 data-source-card">
|
||||||
|
<span className="font-medium">CSV</span>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">Spreadsheet compatible</p>
|
||||||
|
</div>
|
||||||
|
<div className="border rounded-md p-3 data-source-card">
|
||||||
|
<span className="font-medium">SQLite</span>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">Portable database</p>
|
||||||
|
</div>
|
||||||
|
<div className="border rounded-md p-3 data-source-card">
|
||||||
|
<span className="font-medium">YAML</span>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">Human-readable format</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label>Format Preview</Label>
|
||||||
|
<Badge variant="outline">Sample Data</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="bg-muted/50 p-3 rounded-md overflow-x-auto">
|
||||||
|
<pre className="text-xs">
|
||||||
|
{`{
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "P001",
|
||||||
|
"title": "Modern Apartment",
|
||||||
|
"price": 350000,
|
||||||
|
"bedrooms": 2,
|
||||||
|
"location": "Downtown"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "P002",
|
||||||
|
"title": "Luxury Villa",
|
||||||
|
"price": 1250000,
|
||||||
|
"bedrooms": 5,
|
||||||
|
"location": "Suburbs"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button className="gap-2">
|
||||||
|
<Download className="h-4 w-4" />
|
||||||
|
Export Data
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="history" className="mt-4">
|
||||||
|
<Card className="border-2 hover:border-highlight-border transition-all duration-200">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Run History</CardTitle>
|
||||||
|
<CardDescription>History of pipeline executions</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="border rounded-md overflow-hidden">
|
||||||
|
<table className="w-full">
|
||||||
|
<thead className="bg-muted">
|
||||||
|
<tr>
|
||||||
|
<th className="px-4 py-2 text-left text-sm font-medium">Run ID</th>
|
||||||
|
<th className="px-4 py-2 text-left text-sm font-medium">Date</th>
|
||||||
|
<th className="px-4 py-2 text-left text-sm font-medium">Status</th>
|
||||||
|
<th className="px-4 py-2 text-left text-sm font-medium">Duration</th>
|
||||||
|
<th className="px-4 py-2 text-left text-sm font-medium">Records</th>
|
||||||
|
<th className="px-4 py-2 text-left text-sm font-medium">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y">
|
||||||
|
<tr>
|
||||||
|
<td className="px-4 py-2 text-sm">RUN-123</td>
|
||||||
|
<td className="px-4 py-2 text-sm">Today, 10:30 AM</td>
|
||||||
|
<td className="px-4 py-2 text-sm">
|
||||||
|
<Badge variant="default" className="bg-success hover:bg-success">
|
||||||
|
Success
|
||||||
|
</Badge>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-2 text-sm">2m 15s</td>
|
||||||
|
<td className="px-4 py-2 text-sm">1,240</td>
|
||||||
|
<td className="px-4 py-2 text-sm">
|
||||||
|
<Button variant="outline" size="sm">
|
||||||
|
View Log
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className="px-4 py-2 text-sm">RUN-122</td>
|
||||||
|
<td className="px-4 py-2 text-sm">Yesterday, 10:30 AM</td>
|
||||||
|
<td className="px-4 py-2 text-sm">
|
||||||
|
<Badge variant="default" className="bg-success hover:bg-success">
|
||||||
|
Success
|
||||||
|
</Badge>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-2 text-sm">2m 10s</td>
|
||||||
|
<td className="px-4 py-2 text-sm">1,235</td>
|
||||||
|
<td className="px-4 py-2 text-sm">
|
||||||
|
<Button variant="outline" size="sm">
|
||||||
|
View Log
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className="px-4 py-2 text-sm">RUN-121</td>
|
||||||
|
<td className="px-4 py-2 text-sm">2 days ago, 10:30 AM</td>
|
||||||
|
<td className="px-4 py-2 text-sm">
|
||||||
|
<Badge variant="default" className="bg-success hover:bg-success">
|
||||||
|
Success
|
||||||
|
</Badge>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-2 text-sm">2m 05s</td>
|
||||||
|
<td className="px-4 py-2 text-sm">1,228</td>
|
||||||
|
<td className="px-4 py-2 text-sm">
|
||||||
|
<Button variant="outline" size="sm">
|
||||||
|
View Log
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="settings" className="mt-4">
|
||||||
|
<Card className="border-2 hover:border-highlight-border transition-all duration-200">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Pipeline Settings</CardTitle>
|
||||||
|
<CardDescription>Configure pipeline behavior</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h3 className="text-lg font-medium">Scheduling</h3>
|
||||||
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="frequency">Run Frequency</Label>
|
||||||
|
<select
|
||||||
|
id="frequency"
|
||||||
|
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
defaultValue="daily"
|
||||||
|
>
|
||||||
|
<option value="manual">Manual (Run on demand)</option>
|
||||||
|
<option value="hourly">Hourly</option>
|
||||||
|
<option value="daily">Daily</option>
|
||||||
|
<option value="weekly">Weekly</option>
|
||||||
|
<option value="monthly">Monthly</option>
|
||||||
|
<option value="custom">Custom Schedule</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="time">Run Time</Label>
|
||||||
|
<Input id="time" type="time" defaultValue="09:00" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h3 className="text-lg font-medium">Data Collection</h3>
|
||||||
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="max-records">Maximum Records</Label>
|
||||||
|
<Input id="max-records" type="number" defaultValue="2000" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="retry-attempts">Retry Attempts</Label>
|
||||||
|
<Input id="retry-attempts" type="number" defaultValue="3" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h3 className="text-lg font-medium">Notifications</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="notify-complete"
|
||||||
|
className="h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary"
|
||||||
|
defaultChecked
|
||||||
|
/>
|
||||||
|
<Label htmlFor="notify-complete" className="text-sm font-normal">
|
||||||
|
Notify when pipeline completes
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="notify-error"
|
||||||
|
className="h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary"
|
||||||
|
defaultChecked
|
||||||
|
/>
|
||||||
|
<Label htmlFor="notify-error" className="text-sm font-normal">
|
||||||
|
Notify on errors
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button>Save Settings</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
4
frontend/app/(routes)/documentation/loading.tsx
Normal file
4
frontend/app/(routes)/documentation/loading.tsx
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export default function Loading() {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
408
frontend/app/(routes)/documentation/models/page.tsx
Normal file
408
frontend/app/(routes)/documentation/models/page.tsx
Normal file
@ -0,0 +1,408 @@
|
|||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
|
import { Badge } from "@/components/ui/badge"
|
||||||
|
import {
|
||||||
|
ArrowLeft,
|
||||||
|
BrainCircuit,
|
||||||
|
Database,
|
||||||
|
Play,
|
||||||
|
Sliders,
|
||||||
|
ArrowRight,
|
||||||
|
Info,
|
||||||
|
AlertTriangle,
|
||||||
|
HelpCircle,
|
||||||
|
} from "lucide-react"
|
||||||
|
import Link from "next/link"
|
||||||
|
import PageHeader from "@/components/page-header"
|
||||||
|
|
||||||
|
export default function ModelsDocumentationPage() {
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto p-6">
|
||||||
|
<PageHeader
|
||||||
|
title="Models Documentation"
|
||||||
|
description="Learn how to use and train AI models for property analysis"
|
||||||
|
breadcrumb={[
|
||||||
|
{ title: "Home", href: "/" },
|
||||||
|
{ title: "Documentation", href: "/documentation" },
|
||||||
|
{ title: "Models", href: "/documentation/models" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex mb-6">
|
||||||
|
<Link href="/documentation">
|
||||||
|
<Button variant="outline" size="sm" className="gap-2">
|
||||||
|
<ArrowLeft className="h-4 w-4" />
|
||||||
|
Back to Documentation
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-6 md:grid-cols-3">
|
||||||
|
<div className="md:col-span-2 space-y-6">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Understanding Models</CardTitle>
|
||||||
|
<CardDescription>Learn about the different types of models available</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<p>
|
||||||
|
BorBann uses machine learning models to analyze property data and make predictions. These models are
|
||||||
|
trained on historical property data and can be used to predict property prices, identify trends, and
|
||||||
|
provide insights.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3 className="text-lg font-medium mt-4">Types of Models</h3>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="p-3 border rounded-md">
|
||||||
|
<h4 className="font-medium flex items-center gap-2">
|
||||||
|
Regression Models
|
||||||
|
<Badge variant="outline">Standard ML Model v2.4</Badge>
|
||||||
|
</h4>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
Used for predicting continuous values like property prices. These models analyze various features to
|
||||||
|
estimate a property's value.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-3 border rounded-md">
|
||||||
|
<h4 className="font-medium flex items-center gap-2">
|
||||||
|
Neural Networks
|
||||||
|
<Badge variant="outline">Enhanced Neural Network v1.8</Badge>
|
||||||
|
</h4>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
Deep learning models that can capture complex patterns in property data. Ideal for analyzing
|
||||||
|
multiple factors simultaneously.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-3 border rounded-md">
|
||||||
|
<h4 className="font-medium flex items-center gap-2">
|
||||||
|
Geospatial Models
|
||||||
|
<Badge variant="outline">Geospatial Regression v3.1</Badge>
|
||||||
|
</h4>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
Specialized models that incorporate location data and spatial relationships between properties.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-3 border rounded-md">
|
||||||
|
<h4 className="font-medium flex items-center gap-2">
|
||||||
|
Time Series Models
|
||||||
|
<Badge variant="outline">Time Series Forecast v2.0</Badge>
|
||||||
|
</h4>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
Models designed to analyze property price trends over time and make future predictions.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 bg-muted/50 rounded-lg border mt-4">
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<Info className="h-5 w-5 text-primary mt-0.5" />
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium">System vs. Custom Models</h4>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
<strong>System Models</strong> are pre-trained models provided by BorBann. They are regularly
|
||||||
|
updated and maintained by our team.
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
<strong>Custom Models</strong> are models that you train using your own data pipelines. These can
|
||||||
|
be tailored to your specific needs.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Using Models</CardTitle>
|
||||||
|
<CardDescription>How to select and use models for property analysis</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<h3 className="text-lg font-medium">Selecting a Model</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
You can select different models when using the Maps or Price Prediction features. Look for the model
|
||||||
|
selector dropdown in the interface.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="border rounded-md p-4 mt-2">
|
||||||
|
<h4 className="font-medium mb-2">Step-by-Step Guide</h4>
|
||||||
|
<ol className="space-y-2 text-sm">
|
||||||
|
<li className="flex items-start gap-2">
|
||||||
|
<div className="h-5 w-5 rounded-full bg-primary/10 text-primary flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||||
|
1
|
||||||
|
</div>
|
||||||
|
<span>Navigate to the Maps or Price Prediction page</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-2">
|
||||||
|
<div className="h-5 w-5 rounded-full bg-primary/10 text-primary flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||||
|
2
|
||||||
|
</div>
|
||||||
|
<span>Look for the model selector dropdown in the top navigation bar</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-2">
|
||||||
|
<div className="h-5 w-5 rounded-full bg-primary/10 text-primary flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||||
|
3
|
||||||
|
</div>
|
||||||
|
<span>Click on the dropdown to see available models</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-2">
|
||||||
|
<div className="h-5 w-5 rounded-full bg-primary/10 text-primary flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||||
|
4
|
||||||
|
</div>
|
||||||
|
<span>Select the model that best suits your analysis needs</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-2">
|
||||||
|
<div className="h-5 w-5 rounded-full bg-primary/10 text-primary flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||||
|
5
|
||||||
|
</div>
|
||||||
|
<span>The page will update to use the selected model for analysis</span>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 className="text-lg font-medium mt-4">Understanding Model Results</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Different models may produce slightly different results. Here's how to interpret them:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-2">
|
||||||
|
<div className="p-3 border rounded-md">
|
||||||
|
<h4 className="font-medium">Price Predictions</h4>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
Models provide a predicted price along with a confidence level. The higher the confidence, the more
|
||||||
|
reliable the prediction.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-3 border rounded-md">
|
||||||
|
<h4 className="font-medium">Feature Importance</h4>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
Models show which features (location, size, etc.) have the most impact on the property price.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-3 border rounded-md">
|
||||||
|
<h4 className="font-medium">Price Range</h4>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
Models provide a range of possible prices based on the confidence level.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-3 border rounded-md">
|
||||||
|
<h4 className="font-medium">Environmental Impact</h4>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
Models analyze how environmental factors affect property values in the area.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Training Custom Models</CardTitle>
|
||||||
|
<CardDescription>Learn how to create and train your own models</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<p>
|
||||||
|
You can train custom models using your own data pipelines. This allows you to create models tailored to
|
||||||
|
your specific needs.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="border rounded-md p-4 mt-2">
|
||||||
|
<h4 className="font-medium mb-2">Step-by-Step Guide</h4>
|
||||||
|
<ol className="space-y-2 text-sm">
|
||||||
|
<li className="flex items-start gap-2">
|
||||||
|
<div className="h-5 w-5 rounded-full bg-primary/10 text-primary flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||||
|
1
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="font-medium">Navigate to the Models page</span>
|
||||||
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
|
Go to the Models section from the sidebar navigation
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-2">
|
||||||
|
<div className="h-5 w-5 rounded-full bg-primary/10 text-primary flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||||
|
2
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="font-medium">Click "Train New Model"</span>
|
||||||
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
|
This will take you to the model training interface
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-2">
|
||||||
|
<div className="h-5 w-5 rounded-full bg-primary/10 text-primary flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||||
|
3
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="font-medium">Configure your model</span>
|
||||||
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
|
Enter a name, description, and select the model type
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-2">
|
||||||
|
<div className="h-5 w-5 rounded-full bg-primary/10 text-primary flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||||
|
4
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="font-medium">Select a data pipeline</span>
|
||||||
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
|
Choose which data pipeline to use for training the model
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-2">
|
||||||
|
<div className="h-5 w-5 rounded-full bg-primary/10 text-primary flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||||
|
5
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="font-medium">Start training</span>
|
||||||
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
|
Click the "Start Training" button to begin the training process
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-2">
|
||||||
|
<div className="h-5 w-5 rounded-full bg-primary/10 text-primary flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||||
|
6
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="font-medium">Monitor training progress</span>
|
||||||
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
|
The system will show you the training progress and notify you when it's complete
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 bg-amber-50 dark:bg-amber-900/20 text-amber-600 dark:text-amber-400 rounded-md flex items-start gap-2 mt-4">
|
||||||
|
<AlertTriangle className="h-5 w-5 mt-0.5" />
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium">Important Notes</h4>
|
||||||
|
<ul className="text-sm mt-1 space-y-1 list-disc list-inside">
|
||||||
|
<li>Training a model requires a data pipeline with sufficient data</li>
|
||||||
|
<li>The training process may take several minutes depending on the data size</li>
|
||||||
|
<li>You can cancel training at any time if needed</li>
|
||||||
|
<li>Models with more data generally produce more accurate results</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="md:col-span-1 space-y-6">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">Quick Links</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<nav className="space-y-2">
|
||||||
|
<Link
|
||||||
|
href="#understanding-models"
|
||||||
|
className="flex items-center gap-2 text-sm p-2 rounded-md hover:bg-muted"
|
||||||
|
>
|
||||||
|
<BrainCircuit className="h-4 w-4 text-primary" />
|
||||||
|
<span>Understanding Models</span>
|
||||||
|
</Link>
|
||||||
|
<Link href="#using-models" className="flex items-center gap-2 text-sm p-2 rounded-md hover:bg-muted">
|
||||||
|
<Play className="h-4 w-4 text-primary" />
|
||||||
|
<span>Using Models</span>
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="#training-custom-models"
|
||||||
|
className="flex items-center gap-2 text-sm p-2 rounded-md hover:bg-muted"
|
||||||
|
>
|
||||||
|
<Database className="h-4 w-4 text-primary" />
|
||||||
|
<span>Training Custom Models</span>
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="#model-parameters"
|
||||||
|
className="flex items-center gap-2 text-sm p-2 rounded-md hover:bg-muted"
|
||||||
|
>
|
||||||
|
<Sliders className="h-4 w-4 text-primary" />
|
||||||
|
<span>Model Parameters</span>
|
||||||
|
</Link>
|
||||||
|
</nav>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">Related Guides</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Link
|
||||||
|
href="/documentation/price-prediction"
|
||||||
|
className="block p-3 border rounded-md hover:border-primary/50 transition-colors"
|
||||||
|
>
|
||||||
|
<h4 className="font-medium">Price Prediction</h4>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
Learn how to use models for property price prediction
|
||||||
|
</p>
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/documentation/data-pipeline"
|
||||||
|
className="block p-3 border rounded-md hover:border-primary/50 transition-colors"
|
||||||
|
>
|
||||||
|
<h4 className="font-medium">Data Pipelines</h4>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">Set up data pipelines for model training</p>
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/documentation/maps"
|
||||||
|
className="block p-3 border rounded-md hover:border-primary/50 transition-colors"
|
||||||
|
>
|
||||||
|
<h4 className="font-medium">Maps & Geospatial</h4>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">Use models with the interactive map</p>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base flex items-center gap-2">
|
||||||
|
<HelpCircle className="h-4 w-4 text-primary" />
|
||||||
|
Need Help?
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
|
Can't find what you're looking for? Our support team is here to help.
|
||||||
|
</p>
|
||||||
|
<Button className="w-full" asChild>
|
||||||
|
<Link href="/support">Contact Support</Link>
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8 flex justify-between">
|
||||||
|
<Button variant="outline" asChild>
|
||||||
|
<Link href="/documentation">
|
||||||
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||||
|
Back to Documentation
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" asChild>
|
||||||
|
<Link href="/documentation/price-prediction">
|
||||||
|
Next: Price Prediction
|
||||||
|
<ArrowRight className="ml-2 h-4 w-4" />
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
454
frontend/app/(routes)/documentation/page.tsx
Normal file
454
frontend/app/(routes)/documentation/page.tsx
Normal file
@ -0,0 +1,454 @@
|
|||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
|
import {
|
||||||
|
ArrowRight,
|
||||||
|
Map,
|
||||||
|
Database,
|
||||||
|
BrainCircuit,
|
||||||
|
BarChart2,
|
||||||
|
Building,
|
||||||
|
Search,
|
||||||
|
BookOpen,
|
||||||
|
Lightbulb,
|
||||||
|
HelpCircle,
|
||||||
|
Zap,
|
||||||
|
Video,
|
||||||
|
FileQuestion,
|
||||||
|
Play,
|
||||||
|
} from "lucide-react"
|
||||||
|
import Link from "next/link"
|
||||||
|
import PageHeader from "@/components/page-header"
|
||||||
|
|
||||||
|
export default function DocumentationPage() {
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto p-6">
|
||||||
|
<PageHeader
|
||||||
|
title="Documentation"
|
||||||
|
description="Learn how to use BorBann's property analytics platform"
|
||||||
|
breadcrumb={[
|
||||||
|
{ title: "Home", href: "/" },
|
||||||
|
{ title: "Documentation", href: "/documentation" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="mt-6 mb-10">
|
||||||
|
<div className="bg-muted/50 p-6 rounded-lg border">
|
||||||
|
<div className="flex flex-col md:flex-row gap-6 items-center">
|
||||||
|
<div className="md:w-2/3">
|
||||||
|
<h2 className="text-2xl font-bold mb-2">Welcome to BorBann Documentation</h2>
|
||||||
|
<p className="text-muted-foreground mb-4">
|
||||||
|
This documentation will help you get the most out of our property analytics platform. No coding
|
||||||
|
experience is required to use our tools.
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<Button asChild>
|
||||||
|
<Link href="#getting-started">
|
||||||
|
Get Started <ArrowRight className="ml-2 h-4 w-4" />
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" asChild>
|
||||||
|
<Link href="#tutorials">View Tutorials</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="md:w-1/3 flex justify-center">
|
||||||
|
<BookOpen className="h-24 w-24 text-primary/20" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section id="getting-started" className="mb-12">
|
||||||
|
<h2 className="text-2xl font-bold mb-6 flex items-center gap-2">
|
||||||
|
<Lightbulb className="h-6 w-6 text-primary" />
|
||||||
|
Getting Started
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Map className="h-5 w-5 text-primary" />
|
||||||
|
Maps & Geospatial
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>Learn how to use the interactive map</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
|
Explore properties on our interactive map, apply filters, and analyze environmental factors.
|
||||||
|
</p>
|
||||||
|
<ul className="space-y-2 text-sm">
|
||||||
|
<li className="flex items-center gap-2">
|
||||||
|
<div className="h-1.5 w-1.5 rounded-full bg-primary"></div>
|
||||||
|
<span>Navigating the map interface</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center gap-2">
|
||||||
|
<div className="h-1.5 w-1.5 rounded-full bg-primary"></div>
|
||||||
|
<span>Applying filters and radius search</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center gap-2">
|
||||||
|
<div className="h-1.5 w-1.5 rounded-full bg-primary"></div>
|
||||||
|
<span>Understanding property markers</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter>
|
||||||
|
<Button variant="ghost" className="w-full" asChild>
|
||||||
|
<Link href="/documentation/maps">
|
||||||
|
Read Guide <ArrowRight className="ml-2 h-4 w-4" />
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<BarChart2 className="h-5 w-5 text-primary" />
|
||||||
|
Price Prediction
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>Understand property price predictions</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
|
Learn how our AI models predict property prices and how to interpret the results.
|
||||||
|
</p>
|
||||||
|
<ul className="space-y-2 text-sm">
|
||||||
|
<li className="flex items-center gap-2">
|
||||||
|
<div className="h-1.5 w-1.5 rounded-full bg-primary"></div>
|
||||||
|
<span>Understanding prediction factors</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center gap-2">
|
||||||
|
<div className="h-1.5 w-1.5 rounded-full bg-primary"></div>
|
||||||
|
<span>Adjusting parameters</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center gap-2">
|
||||||
|
<div className="h-1.5 w-1.5 rounded-full bg-primary"></div>
|
||||||
|
<span>Generating and using reports</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter>
|
||||||
|
<Button variant="ghost" className="w-full" asChild>
|
||||||
|
<Link href="/documentation/price-prediction">
|
||||||
|
Read Guide <ArrowRight className="ml-2 h-4 w-4" />
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Database className="h-5 w-5 text-primary" />
|
||||||
|
Data Pipelines
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>Set up automated data collection</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
|
Learn how to create and manage data pipelines for property data collection.
|
||||||
|
</p>
|
||||||
|
<ul className="space-y-2 text-sm">
|
||||||
|
<li className="flex items-center gap-2">
|
||||||
|
<div className="h-1.5 w-1.5 rounded-full bg-primary"></div>
|
||||||
|
<span>Creating your first pipeline</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center gap-2">
|
||||||
|
<div className="h-1.5 w-1.5 rounded-full bg-primary"></div>
|
||||||
|
<span>Configuring data sources</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center gap-2">
|
||||||
|
<div className="h-1.5 w-1.5 rounded-full bg-primary"></div>
|
||||||
|
<span>Scheduling and automation</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter>
|
||||||
|
<Button variant="ghost" className="w-full" asChild>
|
||||||
|
<Link href="/documentation/data-pipeline">
|
||||||
|
Read Guide <ArrowRight className="ml-2 h-4 w-4" />
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<BrainCircuit className="h-5 w-5 text-primary" />
|
||||||
|
Models
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>Work with AI models</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
|
Understand how to use and train custom AI models for property analysis.
|
||||||
|
</p>
|
||||||
|
<ul className="space-y-2 text-sm">
|
||||||
|
<li className="flex items-center gap-2">
|
||||||
|
<div className="h-1.5 w-1.5 rounded-full bg-primary"></div>
|
||||||
|
<span>Selecting the right model</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center gap-2">
|
||||||
|
<div className="h-1.5 w-1.5 rounded-full bg-primary"></div>
|
||||||
|
<span>Training custom models</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center gap-2">
|
||||||
|
<div className="h-1.5 w-1.5 rounded-full bg-primary"></div>
|
||||||
|
<span>Understanding model parameters</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter>
|
||||||
|
<Button variant="ghost" className="w-full" asChild>
|
||||||
|
<Link href="/documentation/models">
|
||||||
|
Read Guide <ArrowRight className="ml-2 h-4 w-4" />
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Building className="h-5 w-5 text-primary" />
|
||||||
|
Property Listings
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>Browse and analyze properties</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
|
Learn how to browse property listings and analyze property details.
|
||||||
|
</p>
|
||||||
|
<ul className="space-y-2 text-sm">
|
||||||
|
<li className="flex items-center gap-2">
|
||||||
|
<div className="h-1.5 w-1.5 rounded-full bg-primary"></div>
|
||||||
|
<span>Filtering and sorting properties</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center gap-2">
|
||||||
|
<div className="h-1.5 w-1.5 rounded-full bg-primary"></div>
|
||||||
|
<span>Understanding property analytics</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center gap-2">
|
||||||
|
<div className="h-1.5 w-1.5 rounded-full bg-primary"></div>
|
||||||
|
<span>Exporting property data</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter>
|
||||||
|
<Button variant="ghost" className="w-full" asChild>
|
||||||
|
<Link href="/documentation/properties">
|
||||||
|
Read Guide <ArrowRight className="ml-2 h-4 w-4" />
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Search className="h-5 w-5 text-primary" />
|
||||||
|
Search & Filters
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>Find exactly what you need</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
|
Master the search and filtering capabilities across the platform.
|
||||||
|
</p>
|
||||||
|
<ul className="space-y-2 text-sm">
|
||||||
|
<li className="flex items-center gap-2">
|
||||||
|
<div className="h-1.5 w-1.5 rounded-full bg-primary"></div>
|
||||||
|
<span>Advanced search techniques</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center gap-2">
|
||||||
|
<div className="h-1.5 w-1.5 rounded-full bg-primary"></div>
|
||||||
|
<span>Creating and saving filters</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center gap-2">
|
||||||
|
<div className="h-1.5 w-1.5 rounded-full bg-primary"></div>
|
||||||
|
<span>Combining multiple filters</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter>
|
||||||
|
<Button variant="ghost" className="w-full" asChild>
|
||||||
|
<Link href="/documentation/search">
|
||||||
|
Read Guide <ArrowRight className="ml-2 h-4 w-4" />
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="tutorials" className="mb-12">
|
||||||
|
<h2 className="text-2xl font-bold mb-6 flex items-center gap-2">
|
||||||
|
<Video className="h-6 w-6 text-primary" />
|
||||||
|
Video Tutorials
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
<Card>
|
||||||
|
<div className="aspect-video bg-muted/50 rounded-t-lg flex items-center justify-center">
|
||||||
|
<Play className="h-12 w-12 text-primary/30" />
|
||||||
|
</div>
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-base">Getting Started with BorBann</CardTitle>
|
||||||
|
<CardDescription>5:32 • Beginner</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
A complete overview of the platform and its main features.
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter>
|
||||||
|
<Button variant="ghost" className="w-full">
|
||||||
|
Watch Tutorial
|
||||||
|
</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<div className="aspect-video bg-muted/50 rounded-t-lg flex items-center justify-center">
|
||||||
|
<Play className="h-12 w-12 text-primary/30" />
|
||||||
|
</div>
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-base">Creating Your First Data Pipeline</CardTitle>
|
||||||
|
<CardDescription>8:45 • Beginner</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Step-by-step guide to setting up automated data collection.
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter>
|
||||||
|
<Button variant="ghost" className="w-full">
|
||||||
|
Watch Tutorial
|
||||||
|
</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<div className="aspect-video bg-muted/50 rounded-t-lg flex items-center justify-center">
|
||||||
|
<Play className="h-12 w-12 text-primary/30" />
|
||||||
|
</div>
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-base">Advanced Map Analysis</CardTitle>
|
||||||
|
<CardDescription>12:20 • Intermediate</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Learn how to use advanced map features for property analysis.
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter>
|
||||||
|
<Button variant="ghost" className="w-full">
|
||||||
|
Watch Tutorial
|
||||||
|
</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 text-center">
|
||||||
|
<Button variant="outline" asChild>
|
||||||
|
<Link href="/documentation/tutorials">
|
||||||
|
View All Tutorials <ArrowRight className="ml-2 h-4 w-4" />
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="faq" className="mb-12">
|
||||||
|
<h2 className="text-2xl font-bold mb-6 flex items-center gap-2">
|
||||||
|
<HelpCircle className="h-6 w-6 text-primary" />
|
||||||
|
Frequently Asked Questions
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-2 gap-6">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">How accurate are the price predictions?</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Our price predictions typically have an accuracy of 85-95% depending on the model used and the data
|
||||||
|
available. System models are regularly updated to maintain accuracy.
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">Can I export data for use in other tools?</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Yes, you can export data in various formats including CSV, JSON, and PDF reports. Look for the export
|
||||||
|
options in the property details and analytics pages.
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">Do I need coding experience to use this platform?</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
No, our platform is designed to be user-friendly for non-technical users. All features can be accessed
|
||||||
|
through the intuitive user interface without any coding.
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">How often is the property data updated?</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Data update frequency depends on your data pipeline configuration. System data is typically updated
|
||||||
|
daily, while custom pipelines can be scheduled according to your needs.
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 text-center">
|
||||||
|
<Button variant="outline" asChild>
|
||||||
|
<Link href="/documentation/faq">
|
||||||
|
View All FAQs <ArrowRight className="ml-2 h-4 w-4" />
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="mb-6">
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<div className="flex flex-col md:flex-row gap-6 items-center">
|
||||||
|
<div className="md:w-2/3">
|
||||||
|
<h2 className="text-xl font-bold mb-2 flex items-center gap-2">
|
||||||
|
<FileQuestion className="h-5 w-5 text-primary" />
|
||||||
|
Need More Help?
|
||||||
|
</h2>
|
||||||
|
<p className="text-muted-foreground mb-4">
|
||||||
|
Can't find what you're looking for? Our support team is here to help.
|
||||||
|
</p>
|
||||||
|
<Button asChild>
|
||||||
|
<Link href="/support">Contact Support</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="md:w-1/3 flex justify-center">
|
||||||
|
<Zap className="h-20 w-20 text-primary/20" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@ -1,703 +0,0 @@
|
|||||||
/*
|
|
||||||
========================================
|
|
||||||
File: frontend/app/(routes)/model-explanation/page.tsx
|
|
||||||
========================================
|
|
||||||
*/
|
|
||||||
"use client";
|
|
||||||
|
|
||||||
import React, { useState, useEffect } from "react";
|
|
||||||
import Link from "next/link";
|
|
||||||
import {
|
|
||||||
ChevronRight,
|
|
||||||
Info,
|
|
||||||
ArrowRight,
|
|
||||||
Home,
|
|
||||||
Building,
|
|
||||||
Ruler,
|
|
||||||
Calendar,
|
|
||||||
Coins,
|
|
||||||
Droplets,
|
|
||||||
Wind,
|
|
||||||
Sun,
|
|
||||||
Car,
|
|
||||||
School,
|
|
||||||
ShoppingBag,
|
|
||||||
} from "lucide-react";
|
|
||||||
|
|
||||||
// Common UI components
|
|
||||||
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 { Skeleton } from "@/components/ui/skeleton";
|
|
||||||
// Removed SidebarProvider & ThemeProvider - should be in root layout
|
|
||||||
// Removed MapSidebar - assuming it's not needed here or use a common one
|
|
||||||
|
|
||||||
// Feature-specific components
|
|
||||||
import { FeatureImportanceChart } from "@/features/model-explanation/components/feature-importance-chart";
|
|
||||||
import { PriceComparisonChart } from "@/features/model-explanation/components/price-comparison-chart";
|
|
||||||
|
|
||||||
// Feature-specific API and types
|
|
||||||
import { fetchModelExplanation } from "@/features/model-explanation/api/explanationApi";
|
|
||||||
import type { ModelExplanationData, PropertyBaseDetails } from "@/features/model-explanation/types";
|
|
||||||
|
|
||||||
export default function ModelExplanationPage() {
|
|
||||||
const [activeStep, setActiveStep] = useState(1);
|
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
|
||||||
const [explanationData, setExplanationData] = useState<ModelExplanationData | null>(null);
|
|
||||||
|
|
||||||
// State for interactive elements based on fetched data
|
|
||||||
const [propertySize, setPropertySize] = useState<number>(0);
|
|
||||||
const [propertyAge, setPropertyAge] = useState<number>(0);
|
|
||||||
|
|
||||||
// Fetch data on mount
|
|
||||||
useEffect(() => {
|
|
||||||
async function loadExplanation() {
|
|
||||||
setIsLoading(true);
|
|
||||||
// TODO: Get actual property ID from route params or state
|
|
||||||
const propertyId = "dummy-prop-123";
|
|
||||||
const response = await fetchModelExplanation(propertyId);
|
|
||||||
if (response.success && response.data) {
|
|
||||||
setExplanationData(response.data);
|
|
||||||
// Initialize sliders with fetched data
|
|
||||||
setPropertySize(response.data.propertyDetails.size);
|
|
||||||
setPropertyAge(response.data.propertyDetails.age);
|
|
||||||
} else {
|
|
||||||
console.error("Failed to load model explanation:", response.error);
|
|
||||||
// Handle error state in UI
|
|
||||||
}
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
loadExplanation();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Stepper configuration
|
|
||||||
const steps = [
|
|
||||||
{ id: 1, title: "Property Details", icon: Home },
|
|
||||||
{ id: 2, title: "Feature Analysis", icon: Ruler },
|
|
||||||
{ id: 3, title: "Market Comparison", icon: Building },
|
|
||||||
{ id: 4, title: "Environmental Factors", icon: Droplets },
|
|
||||||
{ id: 5, title: "Final Prediction", icon: Coins },
|
|
||||||
];
|
|
||||||
|
|
||||||
// Calculate adjusted price based on slider interaction
|
|
||||||
const calculateAdjustedPrice = () => {
|
|
||||||
if (!explanationData) return 0;
|
|
||||||
// Simple formula for demonstration - refine with actual logic if possible
|
|
||||||
const basePrice = explanationData.propertyDetails.predictedPrice;
|
|
||||||
const baseSize = explanationData.propertyDetails.size;
|
|
||||||
const baseAge = explanationData.propertyDetails.age;
|
|
||||||
|
|
||||||
const sizeImpact = (propertySize - baseSize) * 50000; // 50,000 THB per sqm diff
|
|
||||||
const ageImpact = (baseAge - propertyAge) * 200000; // 200,000 THB per year newer
|
|
||||||
|
|
||||||
return basePrice + sizeImpact + ageImpact;
|
|
||||||
};
|
|
||||||
|
|
||||||
const adjustedPrice = explanationData ? calculateAdjustedPrice() : 0;
|
|
||||||
|
|
||||||
// Loading State UI
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-1 flex-col overflow-hidden p-6">
|
|
||||||
<div className="mx-auto w-full max-w-7xl">
|
|
||||||
<Skeleton className="h-10 w-1/3 mb-2" />
|
|
||||||
<Skeleton className="h-4 w-2/3 mb-8" />
|
|
||||||
<Skeleton className="h-10 w-full mb-4" />
|
|
||||||
<Skeleton className="h-2 w-full mb-8" />
|
|
||||||
<div className="grid gap-6 md:grid-cols-3">
|
|
||||||
<div className="space-y-6">
|
|
||||||
<Skeleton className="h-48 w-full" />
|
|
||||||
<Skeleton className="h-64 w-full" />
|
|
||||||
</div>
|
|
||||||
<div className="md:col-span-2 space-y-6">
|
|
||||||
<Skeleton className="h-96 w-full" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error State UI
|
|
||||||
if (!explanationData) {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-1 flex-col items-center justify-center p-6">
|
|
||||||
<p className="text-destructive">Failed to load model explanation.</p>
|
|
||||||
<Link href="/map">
|
|
||||||
<Button variant="link" className="mt-4">
|
|
||||||
Back to Map
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Main Content UI
|
|
||||||
const { propertyDetails, features, similarProperties, environmentalFactors, confidence, priceRange } =
|
|
||||||
explanationData;
|
|
||||||
|
|
||||||
return (
|
|
||||||
// Assuming ThemeProvider is in the root layout
|
|
||||||
// Assuming SidebarProvider and a common sidebar are in root layout or parent layout
|
|
||||||
<div className="flex flex-1 flex-col overflow-hidden">
|
|
||||||
{" "}
|
|
||||||
{/* Adjusted for page content */}
|
|
||||||
{/* Header */}
|
|
||||||
<header className="flex h-14 items-center justify-between border-b px-4 bg-background shrink-0">
|
|
||||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
||||||
<Link href="/map" className="hover:text-foreground">
|
|
||||||
Map
|
|
||||||
</Link>
|
|
||||||
<ChevronRight className="h-4 w-4" />
|
|
||||||
<span className="font-medium text-foreground">Price Prediction Model</span>
|
|
||||||
</div>
|
|
||||||
{/* Add any specific header actions if needed */}
|
|
||||||
</header>
|
|
||||||
{/* Main content */}
|
|
||||||
<div className="flex-1 overflow-auto p-6">
|
|
||||||
{" "}
|
|
||||||
{/* Make content area scrollable */}
|
|
||||||
<div className="mx-auto w-full max-w-7xl">
|
|
||||||
<div className="mb-8">
|
|
||||||
<h1 className="text-3xl font-bold tracking-tight">Explainable Price Prediction Model</h1>
|
|
||||||
<p className="mt-2 text-muted-foreground">
|
|
||||||
Understand how our AI model predicts property prices and what factors influence the valuation.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Steps navigation */}
|
|
||||||
<div className="mb-8">
|
|
||||||
<div className="flex flex-wrap gap-4">
|
|
||||||
{steps.map((step) => (
|
|
||||||
<Button
|
|
||||||
key={step.id}
|
|
||||||
variant={activeStep === step.id ? "default" : "outline"}
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
onClick={() => setActiveStep(step.id)}>
|
|
||||||
<step.icon className="h-4 w-4" />
|
|
||||||
<span>{step.title}</span>
|
|
||||||
</Button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="mt-4">
|
|
||||||
<Progress value={(activeStep / steps.length) * 100} className="h-2" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Step content */}
|
|
||||||
<div className="grid gap-6 md:grid-cols-3">
|
|
||||||
{/* --- Left Column: Property Details & Interaction --- */}
|
|
||||||
<div className="space-y-6 md:sticky md:top-6">
|
|
||||||
{" "}
|
|
||||||
{/* Make left column sticky */}
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Property Details</CardTitle>
|
|
||||||
<CardDescription>{propertyDetails.address}</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-2 text-sm">
|
|
||||||
{/* Dynamically display details */}
|
|
||||||
<DetailRow label="Type" value={propertyDetails.type} />
|
|
||||||
<DetailRow label="Size" value={`${propertySize} sqm`} />
|
|
||||||
{propertyDetails.bedrooms && <DetailRow label="Bedrooms" value={propertyDetails.bedrooms} />}
|
|
||||||
{propertyDetails.bathrooms && <DetailRow label="Bathrooms" value={propertyDetails.bathrooms} />}
|
|
||||||
<DetailRow label="Age" value={`${propertyAge} years`} />
|
|
||||||
{propertyDetails.floor && <DetailRow label="Floor" value={propertyDetails.floor} />}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Adjust Parameters</CardTitle>
|
|
||||||
<CardDescription>See how changes affect the prediction</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-6">
|
|
||||||
{/* Size Slider */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<Label htmlFor="prop-size-slider" className="text-sm font-medium">
|
|
||||||
Property Size
|
|
||||||
</Label>
|
|
||||||
<span className="text-sm text-muted-foreground">{propertySize} sqm</span>
|
|
||||||
</div>
|
|
||||||
<Slider
|
|
||||||
id="prop-size-slider"
|
|
||||||
value={[propertySize]}
|
|
||||||
min={50} // Example range
|
|
||||||
max={300} // Example range
|
|
||||||
step={5}
|
|
||||||
onValueChange={(value) => setPropertySize(value[0])}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/* Age Slider */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<Label htmlFor="prop-age-slider" className="text-sm font-medium">
|
|
||||||
Property Age
|
|
||||||
</Label>
|
|
||||||
<span className="text-sm text-muted-foreground">{propertyAge} years</span>
|
|
||||||
</div>
|
|
||||||
<Slider
|
|
||||||
id="prop-age-slider"
|
|
||||||
value={[propertyAge]}
|
|
||||||
min={0} // Example range
|
|
||||||
max={50} // Example range
|
|
||||||
step={1}
|
|
||||||
onValueChange={(value) => setPropertyAge(value[0])}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
<CardFooter>
|
|
||||||
<div className="w-full">
|
|
||||||
<div className="flex justify-between items-baseline">
|
|
||||||
<span className="text-sm text-muted-foreground">Adjusted Price</span>
|
|
||||||
<span className="text-xl font-bold">{formatCurrency(adjustedPrice)}</span>
|
|
||||||
</div>
|
|
||||||
{/* Show difference */}
|
|
||||||
{propertyDetails.predictedPrice !== adjustedPrice && (
|
|
||||||
<div className="mt-2 text-xs text-muted-foreground">
|
|
||||||
{adjustedPrice > propertyDetails.predictedPrice ? "↑" : "↓"}
|
|
||||||
{Math.abs(adjustedPrice - propertyDetails.predictedPrice).toLocaleString()} THB from original
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* --- Right Column: Step Content --- */}
|
|
||||||
<div className="md:col-span-2 space-y-6">
|
|
||||||
{activeStep === 1 && <Step1Content propertyDetails={propertyDetails} setActiveStep={setActiveStep} />}
|
|
||||||
{activeStep === 2 && <Step2Content features={features} setActiveStep={setActiveStep} />}
|
|
||||||
{activeStep === 3 && (
|
|
||||||
<Step3Content
|
|
||||||
property={propertyDetails}
|
|
||||||
comparisons={similarProperties}
|
|
||||||
setActiveStep={setActiveStep}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{activeStep === 4 && <Step4Content factors={environmentalFactors} setActiveStep={setActiveStep} />}
|
|
||||||
{activeStep === 5 && (
|
|
||||||
<Step5Content
|
|
||||||
predictedPrice={adjustedPrice}
|
|
||||||
confidence={confidence}
|
|
||||||
priceRange={priceRange}
|
|
||||||
setActiveStep={setActiveStep}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Helper Components for Steps ---
|
|
||||||
|
|
||||||
function DetailRow({ label, value }: { label: string; value: string | number }) {
|
|
||||||
return (
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="text-muted-foreground">{label}</span>
|
|
||||||
<span className="font-medium">{value}</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatCurrency(value: number): string {
|
|
||||||
return new Intl.NumberFormat("th-TH", {
|
|
||||||
style: "currency",
|
|
||||||
currency: "THB",
|
|
||||||
minimumFractionDigits: 0,
|
|
||||||
maximumFractionDigits: 0,
|
|
||||||
}).format(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 1 Component
|
|
||||||
function Step1Content({
|
|
||||||
propertyDetails,
|
|
||||||
setActiveStep,
|
|
||||||
}: {
|
|
||||||
propertyDetails: PropertyBaseDetails;
|
|
||||||
setActiveStep: (step: number) => void;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Property Overview</CardTitle>
|
|
||||||
<CardDescription>Basic information used in our prediction model</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<p>
|
|
||||||
Our AI model begins by analyzing the core attributes of your property. These fundamental characteristics
|
|
||||||
form the baseline for our prediction.
|
|
||||||
</p>
|
|
||||||
<div className="grid gap-4 md:grid-cols-2">
|
|
||||||
<InfoCard
|
|
||||||
icon={Home}
|
|
||||||
title="Property Type"
|
|
||||||
description={`${propertyDetails.type} properties in this area have specific market dynamics`}
|
|
||||||
/>
|
|
||||||
<InfoCard
|
|
||||||
icon={Ruler}
|
|
||||||
title="Size & Layout"
|
|
||||||
description={`${propertyDetails.size} sqm${
|
|
||||||
propertyDetails.bedrooms ? ` with ${propertyDetails.bedrooms} beds` : ""
|
|
||||||
}${propertyDetails.bathrooms ? ` and ${propertyDetails.bathrooms} baths` : ""}`}
|
|
||||||
/>
|
|
||||||
<InfoCard
|
|
||||||
icon={Calendar}
|
|
||||||
title="Property Age"
|
|
||||||
description={`Built ${propertyDetails.age} years ago, affecting depreciation calculations`}
|
|
||||||
/>
|
|
||||||
{propertyDetails.floor && (
|
|
||||||
<InfoCard
|
|
||||||
icon={Building}
|
|
||||||
title="Floor & View"
|
|
||||||
description={`Located on floor ${propertyDetails.floor}, impacting value`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
<StepFooter onNext={() => setActiveStep(2)} />
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 2 Component
|
|
||||||
function Step2Content({
|
|
||||||
features,
|
|
||||||
setActiveStep,
|
|
||||||
}: {
|
|
||||||
features: ModelExplanationData["features"];
|
|
||||||
setActiveStep: (step: number) => void;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Feature Analysis</CardTitle>
|
|
||||||
<CardDescription>How different features impact the predicted price</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="space-y-6">
|
|
||||||
<p>
|
|
||||||
Our model analyzes various features and determines how each contributes to the price prediction. Below is a
|
|
||||||
breakdown of the most important factors.
|
|
||||||
</p>
|
|
||||||
<div className="h-[300px]">
|
|
||||||
<FeatureImportanceChart features={features} />
|
|
||||||
</div>
|
|
||||||
<div className="space-y-4">
|
|
||||||
{features.map((feature) => (
|
|
||||||
<div key={feature.name} className="space-y-1">
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="text-sm font-medium">{feature.name}</span>
|
|
||||||
<span
|
|
||||||
className={`text-sm font-semibold ${
|
|
||||||
feature.impact === "positive"
|
|
||||||
? "text-green-600 dark:text-green-400"
|
|
||||||
: feature.impact === "negative"
|
|
||||||
? "text-red-600 dark:text-red-400"
|
|
||||||
: "text-yellow-600 dark:text-yellow-400"
|
|
||||||
}`}>
|
|
||||||
{feature.impact === "positive"
|
|
||||||
? "↑ Positive"
|
|
||||||
: feature.impact === "negative"
|
|
||||||
? "↓ Negative"
|
|
||||||
: "→ Neutral"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Progress
|
|
||||||
value={feature.importance}
|
|
||||||
className="h-2"
|
|
||||||
aria-label={`${feature.name} importance ${feature.importance}%`}
|
|
||||||
/>
|
|
||||||
<span className="text-sm text-muted-foreground">{feature.importance}%</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-muted-foreground">{feature.value}</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
<StepFooter onPrev={() => setActiveStep(1)} onNext={() => setActiveStep(3)} />
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 3 Component
|
|
||||||
function Step3Content({
|
|
||||||
property,
|
|
||||||
comparisons,
|
|
||||||
setActiveStep,
|
|
||||||
}: {
|
|
||||||
property: PropertyBaseDetails;
|
|
||||||
comparisons: ModelExplanationData["similarProperties"];
|
|
||||||
setActiveStep: (step: number) => void;
|
|
||||||
}) {
|
|
||||||
// Prepare data for the chart, ensuring the main property is clearly labeled
|
|
||||||
const chartProperty = {
|
|
||||||
name: "Your Property",
|
|
||||||
price: property.predictedPrice,
|
|
||||||
size: property.size,
|
|
||||||
age: property.age,
|
|
||||||
};
|
|
||||||
const chartComparisons = comparisons.map((p, i) => ({
|
|
||||||
name: `Comp ${i + 1}`,
|
|
||||||
address: p.address,
|
|
||||||
price: p.price,
|
|
||||||
size: p.size,
|
|
||||||
age: p.age,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Market Comparison</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
How your property compares to similar properties recently analyzed or sold in the area
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="space-y-6">
|
|
||||||
<p>
|
|
||||||
We analyze recent data from similar properties to establish a baseline. This ensures our prediction aligns
|
|
||||||
with current market conditions.
|
|
||||||
</p>
|
|
||||||
<div className="h-[300px]">
|
|
||||||
<PriceComparisonChart property={chartProperty} comparisons={chartComparisons} />
|
|
||||||
</div>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<h4 className="font-medium">Similar Properties Details</h4>
|
|
||||||
<div className="grid gap-4 md:grid-cols-3">
|
|
||||||
{comparisons.map((p, index) => (
|
|
||||||
<div key={index} className="rounded-lg border p-3 text-xs">
|
|
||||||
<div className="font-medium truncate" title={p.address}>
|
|
||||||
{p.address}
|
|
||||||
</div>
|
|
||||||
<div className="mt-1 text-muted-foreground">
|
|
||||||
{p.size} sqm, {p.age} years old
|
|
||||||
</div>
|
|
||||||
<div className="mt-2 font-bold">{formatCurrency(p.price)}</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
<StepFooter onPrev={() => setActiveStep(2)} onNext={() => setActiveStep(4)} />
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 4 Component
|
|
||||||
function Step4Content({
|
|
||||||
factors,
|
|
||||||
setActiveStep,
|
|
||||||
}: {
|
|
||||||
factors: ModelExplanationData["environmentalFactors"];
|
|
||||||
setActiveStep: (step: number) => void;
|
|
||||||
}) {
|
|
||||||
const factorDetails = {
|
|
||||||
floodRisk: {
|
|
||||||
icon: Droplets,
|
|
||||||
color: factors.floodRisk === "low" ? "green" : factors.floodRisk === "moderate" ? "yellow" : "red",
|
|
||||||
text: "Historical data suggests this level of risk.",
|
|
||||||
},
|
|
||||||
airQuality: {
|
|
||||||
icon: Wind,
|
|
||||||
color: factors.airQuality === "good" ? "green" : factors.airQuality === "moderate" ? "yellow" : "red",
|
|
||||||
text: "Compared to city average.",
|
|
||||||
},
|
|
||||||
noiseLevel: {
|
|
||||||
icon: Sun,
|
|
||||||
color: factors.noiseLevel === "low" ? "green" : factors.noiseLevel === "moderate" ? "yellow" : "red",
|
|
||||||
text: "Based on proximity to major roads/sources.",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Environmental & Location Factors</CardTitle>
|
|
||||||
<CardDescription>How surrounding conditions and amenities affect value</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="space-y-6">
|
|
||||||
<p>
|
|
||||||
Environmental conditions and nearby amenities significantly impact desirability and value. Our model
|
|
||||||
considers these external factors.
|
|
||||||
</p>
|
|
||||||
<div className="grid gap-4 md:grid-cols-3">
|
|
||||||
{/* Environmental Factors */}
|
|
||||||
<FactorCard title="Flood Risk" factor={factors.floodRisk} details={factorDetails.floodRisk} />
|
|
||||||
<FactorCard title="Air Quality" factor={factors.airQuality} details={factorDetails.airQuality} />
|
|
||||||
<FactorCard title="Noise Level" factor={factors.noiseLevel} details={factorDetails.noiseLevel} />
|
|
||||||
</div>
|
|
||||||
{/* Proximity Example */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<h4 className="font-medium">Proximity to Amenities</h4>
|
|
||||||
<div className="grid gap-3 md:grid-cols-2">
|
|
||||||
<ProximityItem icon={Car} text="Public Transport: 300m" />
|
|
||||||
<ProximityItem icon={School} text="Schools: 1.2km" />
|
|
||||||
<ProximityItem icon={ShoppingBag} text="Shopping: 500m" />
|
|
||||||
<ProximityItem icon={Building} text="Hospitals: 2.5km" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
<StepFooter onPrev={() => setActiveStep(3)} onNext={() => setActiveStep(5)} />
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 5 Component
|
|
||||||
function Step5Content({
|
|
||||||
predictedPrice,
|
|
||||||
confidence,
|
|
||||||
priceRange,
|
|
||||||
setActiveStep,
|
|
||||||
}: {
|
|
||||||
predictedPrice: number;
|
|
||||||
confidence: number;
|
|
||||||
priceRange: { lower: number; upper: number };
|
|
||||||
setActiveStep: (step: number) => void;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Final Prediction</CardTitle>
|
|
||||||
<CardDescription>The AI-predicted price based on all analyzed factors</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="space-y-6">
|
|
||||||
{/* Price Box */}
|
|
||||||
<div className="rounded-lg bg-muted p-6 text-center">
|
|
||||||
<h3 className="text-lg font-medium text-muted-foreground">Predicted Price</h3>
|
|
||||||
<div className="mt-2 text-4xl font-bold">{formatCurrency(predictedPrice)}</div>
|
|
||||||
<div className="mt-2 text-sm text-muted-foreground">Confidence Level: {(confidence * 100).toFixed(0)}%</div>
|
|
||||||
</div>
|
|
||||||
{/* Price Range Box */}
|
|
||||||
<div className="rounded-lg border p-4">
|
|
||||||
<h4 className="font-medium flex items-center gap-2">
|
|
||||||
<Info className="h-4 w-4 text-primary" /> Price Range
|
|
||||||
</h4>
|
|
||||||
<p className="mt-2 text-sm text-muted-foreground">
|
|
||||||
Based on our model's confidence, the likely market range is:
|
|
||||||
</p>
|
|
||||||
<div className="mt-3 flex justify-between text-sm">
|
|
||||||
<div>
|
|
||||||
<div className="font-medium">Lower Bound</div>
|
|
||||||
<div className="text-muted-foreground">{formatCurrency(priceRange.lower)}</div>
|
|
||||||
</div>
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="font-medium">Prediction</div>
|
|
||||||
<div className="text-primary font-bold">{formatCurrency(predictedPrice)}</div>
|
|
||||||
</div>
|
|
||||||
<div className="text-right">
|
|
||||||
<div className="font-medium">Upper Bound</div>
|
|
||||||
<div className="text-muted-foreground">{formatCurrency(priceRange.upper)}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* Summary */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<h4 className="font-medium">Summary of Factors</h4>
|
|
||||||
<p className="text-sm text-muted-foreground">This prediction considers:</p>
|
|
||||||
<ul className="mt-2 space-y-1 text-sm list-disc list-inside">
|
|
||||||
<li>Property characteristics (size, age, layout)</li>
|
|
||||||
<li>Location and neighborhood profile</li>
|
|
||||||
<li>Recent market trends and comparable sales</li>
|
|
||||||
<li>Environmental factors and amenity access</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
<CardFooter className="flex justify-between">
|
|
||||||
<Button variant="outline" onClick={() => setActiveStep(4)}>
|
|
||||||
Previous
|
|
||||||
</Button>
|
|
||||||
<Link href="/map">
|
|
||||||
<Button variant="default">Back to Map</Button>
|
|
||||||
</Link>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Sub-components for Steps ---
|
|
||||||
function InfoCard({ icon: Icon, title, description }: { icon: React.ElementType; title: string; description: string }) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-start gap-3 rounded-lg border p-3">
|
|
||||||
<Icon className="mt-0.5 h-5 w-5 text-primary shrink-0" />
|
|
||||||
<div>
|
|
||||||
<h4 className="font-medium text-sm">{title}</h4>
|
|
||||||
<p className="text-xs text-muted-foreground">{description}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function FactorCard({
|
|
||||||
title,
|
|
||||||
factor,
|
|
||||||
details,
|
|
||||||
}: {
|
|
||||||
title: string;
|
|
||||||
factor: string;
|
|
||||||
details: { icon: React.ElementType; color: string; text: string };
|
|
||||||
}) {
|
|
||||||
const Icon = details.icon;
|
|
||||||
const colorClass = `bg-${details.color}-500`; // Requires Tailwind JIT or safelisting
|
|
||||||
const textColorClass = `text-${details.color}-500`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col items-center rounded-lg border p-4 text-center">
|
|
||||||
<Icon className={`h-8 w-8 mb-2 ${textColorClass}`} />
|
|
||||||
<h4 className="font-medium text-sm">{title}</h4>
|
|
||||||
<div className={`mt-2 flex items-center gap-2`}>
|
|
||||||
{/* Explicit colors might be safer than dynamic Tailwind classes */}
|
|
||||||
<div
|
|
||||||
className={`h-3 w-3 rounded-full`}
|
|
||||||
style={{
|
|
||||||
backgroundColor: details.color === "green" ? "#22c55e" : details.color === "yellow" ? "#eab308" : "#ef4444",
|
|
||||||
}}></div>
|
|
||||||
<span className="text-sm capitalize">{factor}</span>
|
|
||||||
</div>
|
|
||||||
<p className="mt-2 text-xs text-muted-foreground">{details.text}</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ProximityItem({ icon: Icon, text }: { icon: React.ElementType; text: string }) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-2 rounded-lg border p-2 text-xs">
|
|
||||||
<Icon className="h-4 w-4 text-primary shrink-0" />
|
|
||||||
<div>{text}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function StepFooter({ onPrev, onNext }: { onPrev?: () => void; onNext?: () => void }) {
|
|
||||||
return (
|
|
||||||
<CardFooter className="flex justify-between">
|
|
||||||
{onPrev ? (
|
|
||||||
<Button variant="outline" onClick={onPrev}>
|
|
||||||
Previous
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<div></div>
|
|
||||||
)}
|
|
||||||
{onNext ? (
|
|
||||||
<Button onClick={onNext}>
|
|
||||||
Next Step <ArrowRight className="ml-2 h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<div></div>
|
|
||||||
)}
|
|
||||||
</CardFooter>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
/*
|
|
||||||
========================================
|
|
||||||
File: frontend/app/(routes)/map/layout.tsx
|
|
||||||
========================================
|
|
||||||
*/
|
|
||||||
import type React from "react";
|
|
||||||
// import { PageLayout } from "@/components/common/PageLayout"; // Example using a common layout
|
|
||||||
|
|
||||||
// This layout is specific to the map feature's route group
|
|
||||||
export default function MapFeatureLayout({ children }: { children: React.ReactNode }) {
|
|
||||||
return (
|
|
||||||
// <PageLayout className="flex flex-row"> {/* Example using common layout */}
|
|
||||||
// The MapSidebar might be rendered here if it's part of the layout
|
|
||||||
<div className="relative flex-1 h-full w-full">
|
|
||||||
{" "}
|
|
||||||
{/* Ensure content takes up space */}
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
// </PageLayout>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,73 +0,0 @@
|
|||||||
/*
|
|
||||||
========================================
|
|
||||||
File: frontend/app/(routes)/map/page.tsx
|
|
||||||
========================================
|
|
||||||
*/
|
|
||||||
"use client";
|
|
||||||
|
|
||||||
import React, { useState } from "react";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { ArrowRight } from "lucide-react";
|
|
||||||
|
|
||||||
// Import common components
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
// NOTE: ThemeProvider and ThemeController are in the root layout or a higher common layout now
|
|
||||||
|
|
||||||
// Import feature-specific components/contexts/types
|
|
||||||
import { MapContainer } from "@/features/map/components/map-container";
|
|
||||||
// MapSidebar might be part of the layout now, if shared, otherwise import here
|
|
||||||
// import { MapSidebar } from "@/features/map/components/map-sidebar";
|
|
||||||
import { MapHeader } from "@/features/map/components/map-header"; // Map specific header
|
|
||||||
import { OverlayProvider } from "@/features/map/components/overlay-system/overlay-context";
|
|
||||||
import { OverlayDock } from "@/features/map/components/overlay-system/overlay-dock";
|
|
||||||
import { AnalyticsOverlay } from "@/features/map/components/analytics-overlay";
|
|
||||||
import { FiltersOverlay } from "@/features/map/components/filters-overlay";
|
|
||||||
import { ChatOverlay } from "@/features/map/components/chat-overlay";
|
|
||||||
import type { MapLocation } from "@/features/map/types";
|
|
||||||
|
|
||||||
export default function MapPage() {
|
|
||||||
const [selectedLocation, setSelectedLocation] = useState<MapLocation>({
|
|
||||||
lat: 13.7563,
|
|
||||||
lng: 100.5018,
|
|
||||||
name: "Bangkok",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Main page structure remains similar, but imports are updated
|
|
||||||
return (
|
|
||||||
// ThemeProvider/Controller likely moved to root layout
|
|
||||||
// SidebarProvider might be moved too, depending on its scope
|
|
||||||
// Assuming OverlayProvider is specific to this map page context
|
|
||||||
<OverlayProvider>
|
|
||||||
{/* The outer div with flex, h-screen etc. should be handled by the layout file or a common PageLayout */}
|
|
||||||
<div className="flex h-full w-full flex-col overflow-hidden">
|
|
||||||
{" "}
|
|
||||||
{/* Simplified for page content */}
|
|
||||||
<MapHeader />
|
|
||||||
<div className="relative flex-1 overflow-hidden">
|
|
||||||
<MapContainer selectedLocation={selectedLocation} />
|
|
||||||
|
|
||||||
{/* Prediction model banner */}
|
|
||||||
<div className="absolute left-1/2 top-4 -translate-x-1/2 z-10">
|
|
||||||
<div className="flex items-center gap-2 rounded-lg bg-card/95 backdrop-blur-xs border border-border/50 px-4 py-2 shadow-lg">
|
|
||||||
<div>
|
|
||||||
<h3 className="font-medium">Price Prediction: 15,000,000 ฿</h3>
|
|
||||||
<p className="text-xs text-muted-foreground">Based on our AI model analysis</p>
|
|
||||||
</div>
|
|
||||||
<Link href="/model-explanation">
|
|
||||||
<Button size="sm" variant="outline" className="gap-1">
|
|
||||||
Explain <ArrowRight className="h-3 w-3" />
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Overlay System */}
|
|
||||||
<AnalyticsOverlay />
|
|
||||||
<FiltersOverlay />
|
|
||||||
<ChatOverlay />
|
|
||||||
<OverlayDock position="bottom" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</OverlayProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
720
frontend/app/(routes)/maps/page.tsx
Normal file
720
frontend/app/(routes)/maps/page.tsx
Normal file
@ -0,0 +1,720 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useState, useRef } from "react"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Card, CardContent } from "@/components/ui/card"
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||||
|
import { Badge } from "@/components/ui/badge"
|
||||||
|
import { Slider } from "@/components/ui/slider"
|
||||||
|
import { Switch } from "@/components/ui/switch"
|
||||||
|
import {
|
||||||
|
MapPin,
|
||||||
|
Home,
|
||||||
|
BarChart2,
|
||||||
|
Filter,
|
||||||
|
MessageCircle,
|
||||||
|
X,
|
||||||
|
Plus,
|
||||||
|
Minus,
|
||||||
|
Droplets,
|
||||||
|
Wind,
|
||||||
|
Sun,
|
||||||
|
LineChart,
|
||||||
|
Send,
|
||||||
|
Newspaper,
|
||||||
|
Building,
|
||||||
|
BedDouble,
|
||||||
|
Bath,
|
||||||
|
Star,
|
||||||
|
Clock,
|
||||||
|
ChevronDown,
|
||||||
|
Check,
|
||||||
|
RefreshCw,
|
||||||
|
} from "lucide-react"
|
||||||
|
import Link from "next/link"
|
||||||
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
||||||
|
|
||||||
|
export default function MapsPage() {
|
||||||
|
const [showFilters, setShowFilters] = useState(false)
|
||||||
|
const [showAnalytics, setShowAnalytics] = useState(false)
|
||||||
|
const [showChat, setShowChat] = useState(false)
|
||||||
|
const [showPropertyInfo, setShowPropertyInfo] = useState(false)
|
||||||
|
const [activeTab, setActiveTab] = useState("basic")
|
||||||
|
const [priceRange, setPriceRange] = useState([5000000, 20000000])
|
||||||
|
const [radius, setRadius] = useState(30)
|
||||||
|
const [message, setMessage] = useState("")
|
||||||
|
const [messages, setMessages] = useState([{ role: "assistant", content: "Hi! How can I help you today?" }])
|
||||||
|
const [mapZoom, setMapZoom] = useState(14)
|
||||||
|
const [selectedModel, setSelectedModel] = useState("Standard ML Model v2.4")
|
||||||
|
const mapRef = useRef(null)
|
||||||
|
|
||||||
|
const models = [
|
||||||
|
"Standard ML Model v2.4",
|
||||||
|
"Enhanced Neural Network v1.8",
|
||||||
|
"Geospatial Regression v3.1",
|
||||||
|
"Time Series Forecast v2.0",
|
||||||
|
"Custom Model (User #1242)",
|
||||||
|
]
|
||||||
|
|
||||||
|
const handleSendMessage = () => {
|
||||||
|
if (message.trim()) {
|
||||||
|
setMessages([...messages, { role: "user", content: message }])
|
||||||
|
// Simulate AI response
|
||||||
|
setTimeout(() => {
|
||||||
|
setMessages((prev) => [
|
||||||
|
...prev,
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content:
|
||||||
|
"I can provide information about properties in this area. Would you like to know about flood risks, air quality, or nearby amenities?",
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}, 1000)
|
||||||
|
setMessage("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleZoomIn = () => {
|
||||||
|
setMapZoom((prev) => Math.min(prev + 1, 20))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleZoomOut = () => {
|
||||||
|
setMapZoom((prev) => Math.max(prev - 1, 10))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePropertyClick = () => {
|
||||||
|
setShowPropertyInfo(true)
|
||||||
|
setShowFilters(false)
|
||||||
|
setShowChat(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative h-screen w-full overflow-hidden bg-gray-100 dark:bg-gray-900">
|
||||||
|
{/* Map Container */}
|
||||||
|
<div className="absolute inset-0 bg-[url('/map.png')] bg-cover bg-center">
|
||||||
|
{/* Map Placeholder - In a real implementation, this would be a map component */}
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
|
<div className="text-2xl text-muted-foreground opacity-0">Map View</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sample Property Markers */}
|
||||||
|
<div className="absolute left-1/4 top-1/3 text-primary cursor-pointer group" onClick={handlePropertyClick}>
|
||||||
|
<div className="relative transition-transform transform group-hover:scale-125">
|
||||||
|
<div className="absolute inset-0 w-10 h-10 bg-green-500 opacity-30 blur-lg rounded-full"></div>
|
||||||
|
<MapPin className="h-10 w-10 text-green-500 drop-shadow-xl" />
|
||||||
|
<div className="absolute -top-2 -right-2 h-5 w-5 bg-green-500 rounded-full border-2 border-white animate-pulse"></div>
|
||||||
|
<span className="absolute top-12 left-1/2 -translate-x-1/2 hidden group-hover:flex bg-black text-white text-xs font-bold px-3 py-1 rounded-lg shadow-lg">
|
||||||
|
Available
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="absolute left-1/2 top-1/2 text-primary cursor-pointer group" onClick={handlePropertyClick}>
|
||||||
|
<div className="relative transition-transform transform group-hover:scale-125">
|
||||||
|
<div className="absolute inset-0 w-10 h-10 bg-yellow-500 opacity-30 blur-lg rounded-full"></div>
|
||||||
|
<MapPin className="h-10 w-10 text-yellow-500 drop-shadow-xl" />
|
||||||
|
<div className="absolute -top-2 -right-2 h-5 w-5 bg-amber-500 rounded-full border-2 border-white animate-pulse"></div>
|
||||||
|
<span className="absolute top-12 left-1/2 -translate-x-1/2 hidden group-hover:flex bg-black text-white text-xs font-bold px-3 py-1 rounded-lg shadow-lg">
|
||||||
|
Pending
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="absolute right-1/4 top-2/3 text-primary cursor-pointer group" onClick={handlePropertyClick}>
|
||||||
|
<div className="relative transition-transform transform group-hover:scale-125">
|
||||||
|
<div className="absolute inset-0 w-10 h-10 bg-red-500 opacity-30 blur-lg rounded-full"></div>
|
||||||
|
<MapPin className="h-10 w-10 text-red-500 drop-shadow-xl" />
|
||||||
|
<div className="absolute -top-2 -right-2 h-5 w-5 bg-red-500 rounded-full border-2 border-white animate-pulse"></div>
|
||||||
|
<span className="absolute top-12 left-1/2 -translate-x-1/2 hidden group-hover:flex bg-black text-white text-xs font-bold px-3 py-1 rounded-lg shadow-lg">
|
||||||
|
Sold
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Top Navigation Bar */}
|
||||||
|
<div className="absolute top-0 left-0 right-0 bg-background/95 backdrop-blur-sm p-4 flex items-center justify-between z-10 border-b">
|
||||||
|
<Link href="/" className="flex items-center gap-2">
|
||||||
|
<Home className="h-5 w-5" />
|
||||||
|
<span className="font-semibold">BorBann</span>
|
||||||
|
</Link>
|
||||||
|
<div className="flex-1 max-w-md mx-4">
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search locations..."
|
||||||
|
className="w-full h-10 px-4 rounded-md border border-input bg-background"
|
||||||
|
/>
|
||||||
|
<div className="absolute right-3 top-1/2 -translate-y-1/2">
|
||||||
|
<MapPin className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="outline" size="sm" className="gap-1">
|
||||||
|
<BarChart2 className="h-4 w-4" />
|
||||||
|
<span className="hidden sm:inline">{selectedModel}</span>
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" className="w-[240px]">
|
||||||
|
{models.map((model) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
key={model}
|
||||||
|
onClick={() => setSelectedModel(model)}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
{model === selectedModel && <Check className="h-4 w-4 text-primary" />}
|
||||||
|
<span className={model === selectedModel ? "font-medium" : ""}>{model}</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
))}
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<Link href="/models" className="flex items-center w-full">
|
||||||
|
<span className="text-primary">Manage Models...</span>
|
||||||
|
</Link>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
|
||||||
|
<Link href="/properties">
|
||||||
|
<Button variant="outline" size="sm" className="gap-1">
|
||||||
|
<Building className="h-4 w-4" />
|
||||||
|
<span className="hidden sm:inline">Properties</span>
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
<Link href="/price-prediction">
|
||||||
|
<Button variant="outline" size="sm" className="gap-1">
|
||||||
|
<BarChart2 className="h-4 w-4" />
|
||||||
|
<span className="hidden sm:inline">Price Prediction</span>
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Map Controls */}
|
||||||
|
<div className="absolute top-20 right-4 flex flex-col gap-2 z-10">
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="icon"
|
||||||
|
className="h-10 w-10 rounded-full bg-background/95 backdrop-blur-sm shadow-md"
|
||||||
|
onClick={handleZoomIn}
|
||||||
|
>
|
||||||
|
<Plus className="h-5 w-5" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="icon"
|
||||||
|
className="h-10 w-10 rounded-full bg-background/95 backdrop-blur-sm shadow-md"
|
||||||
|
onClick={handleZoomOut}
|
||||||
|
>
|
||||||
|
<Minus className="h-5 w-5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Map Overlay Controls */}
|
||||||
|
<div className="absolute bottom-8 left-1/2 -translate-x-1/2 flex gap-2 z-10">
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="icon"
|
||||||
|
className={`h-12 w-12 rounded-full bg-background/95 backdrop-blur-sm shadow-md ${showAnalytics ? "bg-primary text-primary-foreground" : ""}`}
|
||||||
|
onClick={() => {
|
||||||
|
setShowAnalytics(!showAnalytics)
|
||||||
|
if (showAnalytics) {
|
||||||
|
setShowFilters(false)
|
||||||
|
setShowPropertyInfo(false)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<BarChart2 className="h-5 w-5" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="icon"
|
||||||
|
className={`h-12 w-12 rounded-full bg-background/95 backdrop-blur-sm shadow-md ${showFilters ? "bg-primary text-primary-foreground" : ""}`}
|
||||||
|
onClick={() => {
|
||||||
|
setShowFilters(!showFilters)
|
||||||
|
if (showFilters) {
|
||||||
|
setShowAnalytics(false)
|
||||||
|
setShowPropertyInfo(false)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Filter className="h-5 w-5" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="icon"
|
||||||
|
className={`h-12 w-12 rounded-full bg-background/95 backdrop-blur-sm shadow-md ${showChat ? "bg-primary text-primary-foreground" : ""}`}
|
||||||
|
onClick={() => {
|
||||||
|
setShowChat(!showChat)
|
||||||
|
if (showChat) {
|
||||||
|
setShowPropertyInfo(false)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MessageCircle className="h-5 w-5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Property Info Panel */}
|
||||||
|
{showPropertyInfo && (
|
||||||
|
<div className="absolute top-20 right-4 w-96 map-overlay z-20">
|
||||||
|
<div className="map-overlay-header">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Building className="h-5 w-5 text-primary" />
|
||||||
|
<span className="font-medium">Property Details</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => setShowPropertyInfo(false)}>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="map-overlay-content">
|
||||||
|
<div className="relative mb-4">
|
||||||
|
<img
|
||||||
|
src="/placeholder.svg?height=200&width=400"
|
||||||
|
alt="Property"
|
||||||
|
className="w-full h-40 object-cover rounded-md"
|
||||||
|
/>
|
||||||
|
<div className="absolute top-2 left-2 flex gap-1">
|
||||||
|
<Badge className="bg-primary">Condominium</Badge>
|
||||||
|
<Badge className="bg-amber-500">
|
||||||
|
<Star className="h-3 w-3 mr-1" /> Premium
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 className="font-medium text-lg mb-1">Modern Condominium</h3>
|
||||||
|
<div className="flex items-center text-muted-foreground text-sm mb-2">
|
||||||
|
<MapPin className="h-3.5 w-3.5 mr-1 flex-shrink-0" />
|
||||||
|
<span className="truncate">Sukhumvit, Bangkok</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4 text-sm mb-3">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<BedDouble className="h-4 w-4 mr-1 text-primary" />
|
||||||
|
<span>3 Beds</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Bath className="h-4 w-4 mr-1 text-primary" />
|
||||||
|
<span>2 Baths</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Home className="h-4 w-4 mr-1 text-primary" />
|
||||||
|
<span>150 m²</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="font-semibold text-lg mb-4">฿15,000,000</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium text-sm mb-2">Environmental Factors</h4>
|
||||||
|
<div className="grid grid-cols-3 gap-2">
|
||||||
|
<div className="flex flex-col items-center p-2 border rounded-md">
|
||||||
|
<Droplets className="h-5 w-5 text-blue-500 mb-1" />
|
||||||
|
<span className="text-xs font-medium">Flood Risk</span>
|
||||||
|
<Badge className="mt-1 text-xs bg-amber-500">Moderate</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center p-2 border rounded-md">
|
||||||
|
<Wind className="h-5 w-5 text-purple-500 mb-1" />
|
||||||
|
<span className="text-xs font-medium">Air Quality</span>
|
||||||
|
<Badge className="mt-1 text-xs bg-destructive">Poor</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center p-2 border rounded-md">
|
||||||
|
<Sun className="h-5 w-5 text-amber-500 mb-1" />
|
||||||
|
<span className="text-xs font-medium">Noise</span>
|
||||||
|
<Badge className="mt-1 text-xs bg-green-500">Low</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium text-sm mb-2">Nearby Facilities</h4>
|
||||||
|
<div className="space-y-1 text-sm">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>BTS Phrom Phong</span>
|
||||||
|
<span className="text-muted-foreground">300m</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>EmQuartier Mall</span>
|
||||||
|
<span className="text-muted-foreground">500m</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>Benchasiri Park</span>
|
||||||
|
<span className="text-muted-foreground">700m</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Link href="/properties/prop1" className="flex-1">
|
||||||
|
<Button className="w-full">View Details</Button>
|
||||||
|
</Link>
|
||||||
|
<Link href="/price-prediction" className="flex-1">
|
||||||
|
<Button variant="outline" className="w-full">
|
||||||
|
Price Analysis
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Analytics Panel */}
|
||||||
|
{showAnalytics && (
|
||||||
|
<div className="absolute top-20 right-4 w-96 max-h-[800px] overflow-y-auto z-20 map-overlay">
|
||||||
|
<div className="map-overlay-header">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<BarChart2 className="h-5 w-5 text-primary" />
|
||||||
|
<span className="font-medium">Analytics</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-8 w-8 flex items-center justify-center"
|
||||||
|
onClick={() => setSelectedModel(selectedModel)}
|
||||||
|
>
|
||||||
|
<RefreshCw className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => setShowAnalytics(false)}>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="map-overlay-content">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<p className="text-sm text-muted-foreground">Information in radius will be analyzed</p>
|
||||||
|
<Badge variant="outline" className="text-xs">
|
||||||
|
Using: {selectedModel}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card className="mb-4">
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<LineChart className="h-4 w-4 text-primary" />
|
||||||
|
<span className="font-medium">Area Price History</span>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-2xl font-bold mb-1">10,000,000 Baht</h3>
|
||||||
|
<p className="text-xs text-muted-foreground mb-3">Overall Price History of this area</p>
|
||||||
|
|
||||||
|
<div className="h-20 w-full relative">
|
||||||
|
{/* Simple line chart simulation */}
|
||||||
|
<div className="absolute bottom-0 left-0 w-full h-px bg-border"></div>
|
||||||
|
<div className="absolute bottom-0 left-0 h-full flex items-end">
|
||||||
|
<div className="w-1/6 h-8 border-b-2 border-r-2 border-primary rounded-br"></div>
|
||||||
|
<div className="w-1/6 h-6 border-b-2 border-r-2 border-primary rounded-br"></div>
|
||||||
|
<div className="w-1/6 h-7 border-b-2 border-r-2 border-primary rounded-br"></div>
|
||||||
|
<div className="w-1/6 h-10 border-b-2 border-r-2 border-primary rounded-br"></div>
|
||||||
|
<div className="w-1/6 h-12 border-b-2 border-r-2 border-primary rounded-br"></div>
|
||||||
|
<div className="w-1/6 h-16 border-b-2 border-r-2 border-primary rounded-br"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="mb-4">
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<LineChart className="h-4 w-4 text-primary" />
|
||||||
|
<span className="font-medium">Price Prediction</span>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-2xl font-bold mb-1">15,000,000 Baht</h3>
|
||||||
|
<p className="text-xs text-muted-foreground mb-3">The estimated price based on various factors.</p>
|
||||||
|
|
||||||
|
<div className="h-20 w-full relative">
|
||||||
|
{/* Simple line chart simulation */}
|
||||||
|
<div className="absolute bottom-0 left-0 w-full h-px bg-border"></div>
|
||||||
|
<div className="absolute bottom-0 left-0 h-full flex items-end">
|
||||||
|
<div className="w-1/6 h-4 border-b-2 border-r-2 border-green-500 rounded-br"></div>
|
||||||
|
<div className="w-1/6 h-6 border-b-2 border-r-2 border-green-500 rounded-br"></div>
|
||||||
|
<div className="w-1/6 h-8 border-b-2 border-r-2 border-green-500 rounded-br"></div>
|
||||||
|
<div className="w-1/6 h-10 border-b-2 border-r-2 border-green-500 rounded-br"></div>
|
||||||
|
<div className="w-1/6 h-14 border-b-2 border-r-2 border-green-500 rounded-br"></div>
|
||||||
|
<div className="w-1/6 h-18 border-b-2 border-r-2 border-green-500 rounded-br"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-2 mb-4">
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-3">
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<Droplets className="h-5 w-5 text-blue-500 mb-1" />
|
||||||
|
<span className="text-sm font-medium">Flood Factor</span>
|
||||||
|
<Badge className="mt-1 bg-amber-500">Moderate</Badge>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-3">
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<Wind className="h-5 w-5 text-purple-500 mb-1" />
|
||||||
|
<span className="text-sm font-medium">Air Factor</span>
|
||||||
|
<Badge className="mt-1 bg-destructive">Bad</Badge>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Local News Section */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<h4 className="font-medium text-sm mb-2 flex items-center">
|
||||||
|
<Newspaper className="h-4 w-4 mr-1 text-primary" />
|
||||||
|
Local News
|
||||||
|
</h4>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-3">
|
||||||
|
<h5 className="text-sm font-medium">New BTS Extension Planned</h5>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
The BTS Skytrain will be extended to cover more areas in Sukhumvit by 2025.
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center text-xs text-muted-foreground mt-1">
|
||||||
|
<Clock className="h-3 w-3 mr-1" />
|
||||||
|
<span>2 days ago</span>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-3">
|
||||||
|
<h5 className="text-sm font-medium">Property Tax Changes</h5>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
New property tax regulations will take effect next month affecting luxury condominiums.
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center text-xs text-muted-foreground mt-1">
|
||||||
|
<Clock className="h-3 w-3 mr-1" />
|
||||||
|
<span>1 week ago</span>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button variant="outline" className="w-full gap-2">
|
||||||
|
<MessageCircle className="h-4 w-4" />
|
||||||
|
Chat With AI
|
||||||
|
</Button>
|
||||||
|
<Link href="/price-prediction" className="flex-1">
|
||||||
|
<Button className="w-full">Full Analysis</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Filters Panel */}
|
||||||
|
{showFilters && (
|
||||||
|
<div className="absolute top-20 right-4 w-96 map-overlay z-20">
|
||||||
|
<div className="map-overlay-header">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Filter className="h-5 w-5 text-primary" />
|
||||||
|
<span className="font-medium">Property Filters</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => setShowFilters(false)}>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Tabs defaultValue="basic" value={activeTab} onValueChange={setActiveTab}>
|
||||||
|
<TabsList className="w-full grid grid-cols-2">
|
||||||
|
<TabsTrigger value="basic">Basic</TabsTrigger>
|
||||||
|
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value="basic" className="p-4">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium mb-1.5 block">Area Radius</label>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Slider
|
||||||
|
defaultValue={[30]}
|
||||||
|
max={50}
|
||||||
|
min={1}
|
||||||
|
step={1}
|
||||||
|
onValueChange={(value) => setRadius(value[0])}
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
<span className="text-sm w-16 text-right">{radius} km</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium mb-1.5 block">Time Period</label>
|
||||||
|
<select className="w-full h-10 rounded-md border border-input bg-background px-3 py-2 text-sm">
|
||||||
|
<option value="all">All Time</option>
|
||||||
|
<option value="1m">Last Month</option>
|
||||||
|
<option value="3m">Last 3 Months</option>
|
||||||
|
<option value="6m">Last 6 Months</option>
|
||||||
|
<option value="1y">Last Year</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium mb-1.5 block">Property Type</label>
|
||||||
|
<select className="w-full h-10 rounded-md border border-input bg-background px-3 py-2 text-sm">
|
||||||
|
<option value="any">Any Type</option>
|
||||||
|
<option value="house">House</option>
|
||||||
|
<option value="condo">Condominium</option>
|
||||||
|
<option value="townhouse">Townhouse</option>
|
||||||
|
<option value="land">Land</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button className="w-full">Apply Filters</Button>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="advanced" className="p-4">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium mb-1.5 block">Price Range</label>
|
||||||
|
<div className="flex items-center justify-between text-xs text-muted-foreground mb-2">
|
||||||
|
<span>฿{priceRange[0].toLocaleString()}</span>
|
||||||
|
<span>฿{priceRange[1].toLocaleString()}</span>
|
||||||
|
</div>
|
||||||
|
<Slider
|
||||||
|
defaultValue={[5000000, 20000000]}
|
||||||
|
max={50000000}
|
||||||
|
min={1000000}
|
||||||
|
step={1000000}
|
||||||
|
onValueChange={(value) => setPriceRange(value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium mb-1.5 block">Environmental Factors</label>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm">Low Flood Risk</span>
|
||||||
|
<Switch />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm">Good Air Quality</span>
|
||||||
|
<Switch />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm">Low Noise Pollution</span>
|
||||||
|
<Switch />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium mb-1.5 block">Facilities Nearby</label>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<input type="checkbox" id="bts" className="h-4 w-4" />
|
||||||
|
<label htmlFor="bts" className="text-sm">
|
||||||
|
BTS/MRT Station
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<input type="checkbox" id="school" className="h-4 w-4" />
|
||||||
|
<label htmlFor="school" className="text-sm">
|
||||||
|
Schools
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<input type="checkbox" id="hospital" className="h-4 w-4" />
|
||||||
|
<label htmlFor="hospital" className="text-sm">
|
||||||
|
Hospitals
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<input type="checkbox" id="mall" className="h-4 w-4" />
|
||||||
|
<label htmlFor="mall" className="text-sm">
|
||||||
|
Shopping Malls
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button className="w-full">Apply Filters</Button>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Chat Panel */}
|
||||||
|
{showChat && (
|
||||||
|
<div className="absolute top-20 right-4 w-96 h-[500px] map-overlay z-20 flex flex-col">
|
||||||
|
<div className="map-overlay-header">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<MessageCircle className="h-5 w-5 text-primary" />
|
||||||
|
<span className="font-medium">Chat Assistant</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => setShowChat(false)}>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-y-auto p-3 space-y-3">
|
||||||
|
{messages.map((msg, index) => (
|
||||||
|
<div key={index} className={`flex ${msg.role === "user" ? "justify-end" : "justify-start"}`}>
|
||||||
|
<div
|
||||||
|
className={`max-w-[80%] rounded-lg px-3 py-2 ${
|
||||||
|
msg.role === "user" ? "bg-primary text-primary-foreground" : "bg-muted"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{msg.content}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-3 border-t">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Type your message..."
|
||||||
|
className="flex-1 h-10 px-3 rounded-md border border-input bg-background"
|
||||||
|
value={message}
|
||||||
|
onChange={(e) => setMessage(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") handleSendMessage()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button variant="default" size="icon" className="h-10 w-10" onClick={handleSendMessage}>
|
||||||
|
<Send className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Map Legend */}
|
||||||
|
<div className="absolute bottom-8 left-4 bg-background/95 backdrop-blur-sm p-2 rounded-lg shadow-md z-10">
|
||||||
|
<div className="text-xs font-medium mb-1">Property Status</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<div className="h-3 w-3 bg-green-500 rounded-full"></div>
|
||||||
|
<span className="text-xs">Available</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<div className="h-3 w-3 bg-amber-500 rounded-full"></div>
|
||||||
|
<span className="text-xs">Pending</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<div className="h-3 w-3 bg-red-500 rounded-full"></div>
|
||||||
|
<span className="text-xs">Sold</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
500
frontend/app/(routes)/models/page.tsx
Normal file
500
frontend/app/(routes)/models/page.tsx
Normal file
@ -0,0 +1,500 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { Progress } from "@/components/ui/progress";
|
||||||
|
import {
|
||||||
|
BrainCircuit,
|
||||||
|
Clock,
|
||||||
|
Database,
|
||||||
|
Play,
|
||||||
|
Plus,
|
||||||
|
Settings,
|
||||||
|
Sliders,
|
||||||
|
Trash2,
|
||||||
|
AlertTriangle,
|
||||||
|
Check,
|
||||||
|
ArrowRight,
|
||||||
|
Info,
|
||||||
|
} from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import PageHeader from "@/components/page-header";
|
||||||
|
|
||||||
|
export default function ModelsPage() {
|
||||||
|
const [activeTab, setActiveTab] = useState("my-models");
|
||||||
|
const [selectedPipeline, setSelectedPipeline] = useState<string | null>(null);
|
||||||
|
const [trainingProgress, setTrainingProgress] = useState(0);
|
||||||
|
const [isTraining, setIsTraining] = useState(false);
|
||||||
|
const [modelName, setModelName] = useState("");
|
||||||
|
const [modelDescription, setModelDescription] = useState("");
|
||||||
|
|
||||||
|
const dataPipelines = [
|
||||||
|
{ id: "pipeline-1", name: "Property Listings", records: 1240, lastUpdated: "2 hours ago" },
|
||||||
|
{ id: "pipeline-2", name: "Rental Market Data", records: 830, lastUpdated: "Yesterday" },
|
||||||
|
{ id: "pipeline-3", name: "Price Comparison", records: 1560, lastUpdated: "2 days ago" },
|
||||||
|
{ id: "pipeline-4", name: "Commercial Properties", records: 450, lastUpdated: "1 week ago" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const models = [
|
||||||
|
{
|
||||||
|
id: "model-1",
|
||||||
|
name: "Standard ML Model v2.4",
|
||||||
|
type: "Regression",
|
||||||
|
hyperparameters: {
|
||||||
|
learningRate: "0.01",
|
||||||
|
maxDepth: "6",
|
||||||
|
numEstimators: "100",
|
||||||
|
},
|
||||||
|
dataSource: "System Base Model",
|
||||||
|
status: "active",
|
||||||
|
lastUpdated: "3 days ago",
|
||||||
|
isSystem: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "model-2",
|
||||||
|
name: "Enhanced Neural Network v1.8",
|
||||||
|
type: "Neural Network",
|
||||||
|
hyperparameters: {
|
||||||
|
layers: "4",
|
||||||
|
neurons: "128,64,32,16",
|
||||||
|
dropout: "0.2",
|
||||||
|
},
|
||||||
|
dataSource: "System Base Model",
|
||||||
|
status: "active",
|
||||||
|
lastUpdated: "1 week ago",
|
||||||
|
isSystem: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "model-3",
|
||||||
|
name: "Geospatial Regression v3.1",
|
||||||
|
type: "Geospatial",
|
||||||
|
hyperparameters: {
|
||||||
|
spatialWeight: "0.7",
|
||||||
|
kernelType: "gaussian",
|
||||||
|
bandwidth: "adaptive",
|
||||||
|
},
|
||||||
|
dataSource: "System Base Model",
|
||||||
|
status: "active",
|
||||||
|
lastUpdated: "2 weeks ago",
|
||||||
|
isSystem: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "model-4",
|
||||||
|
name: "Time Series Forecast v2.0",
|
||||||
|
type: "Time Series",
|
||||||
|
hyperparameters: {
|
||||||
|
p: "2",
|
||||||
|
d: "1",
|
||||||
|
q: "2",
|
||||||
|
seasonal: "true",
|
||||||
|
},
|
||||||
|
dataSource: "System Base Model",
|
||||||
|
status: "active",
|
||||||
|
lastUpdated: "1 month ago",
|
||||||
|
isSystem: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "model-5",
|
||||||
|
name: "Custom Model (User #1242)",
|
||||||
|
type: "Ensemble",
|
||||||
|
hyperparameters: {
|
||||||
|
baseEstimators: "3",
|
||||||
|
votingMethod: "weighted",
|
||||||
|
weights: "0.4,0.4,0.2",
|
||||||
|
},
|
||||||
|
dataSource: "Property Listings Pipeline",
|
||||||
|
status: "active",
|
||||||
|
lastUpdated: "5 days ago",
|
||||||
|
isSystem: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleStartTraining = () => {
|
||||||
|
if (!selectedPipeline || !modelName) return;
|
||||||
|
|
||||||
|
setIsTraining(true);
|
||||||
|
setTrainingProgress(0);
|
||||||
|
|
||||||
|
// Simulate training progress
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setTrainingProgress((prev) => {
|
||||||
|
if (prev >= 100) {
|
||||||
|
clearInterval(interval);
|
||||||
|
setIsTraining(false);
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
return prev + 5;
|
||||||
|
});
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto p-6">
|
||||||
|
<PageHeader
|
||||||
|
title="Model Management"
|
||||||
|
description="Train, manage, and deploy machine learning models for property analysis"
|
||||||
|
breadcrumb={[
|
||||||
|
{ title: "Home", href: "/" },
|
||||||
|
{ title: "Models", href: "/models" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Tabs defaultValue="my-models" className="mt-6" onValueChange={setActiveTab}>
|
||||||
|
<div className="flex justify-between items-center mb-6">
|
||||||
|
<TabsList>
|
||||||
|
<TabsTrigger value="my-models">My Models</TabsTrigger>
|
||||||
|
<TabsTrigger value="system-models">System Models</TabsTrigger>
|
||||||
|
<TabsTrigger value="train-model">Train New Model</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
{activeTab !== "train-model" && (
|
||||||
|
<Button onClick={() => setActiveTab("train-model")} className="gap-2">
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
Train New Model
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TabsContent value="my-models">
|
||||||
|
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{models
|
||||||
|
.filter((model) => !model.isSystem)
|
||||||
|
.map((model) => (
|
||||||
|
<ModelCard key={model.id} model={model} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{models.filter((model) => !model.isSystem).length === 0 && (
|
||||||
|
<Card className="border-dashed">
|
||||||
|
<CardContent className="pt-6 text-center">
|
||||||
|
<BrainCircuit className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
|
||||||
|
<h3 className="text-lg font-medium mb-2">No Custom Models Yet</h3>
|
||||||
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
|
Train your first custom model to get started with personalized property predictions.
|
||||||
|
</p>
|
||||||
|
<Button onClick={() => setActiveTab("train-model")}>Train Your First Model</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="system-models">
|
||||||
|
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{models
|
||||||
|
.filter((model) => model.isSystem)
|
||||||
|
.map((model) => (
|
||||||
|
<ModelCard key={model.id} model={model} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="train-model">
|
||||||
|
<div className="grid gap-6 md:grid-cols-3">
|
||||||
|
<div className="md:col-span-1">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Training Configuration</CardTitle>
|
||||||
|
<CardDescription>Configure your new model</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="model-name">Model Name</Label>
|
||||||
|
<Input
|
||||||
|
id="model-name"
|
||||||
|
placeholder="e.g., My Custom Model v1.0"
|
||||||
|
value={modelName}
|
||||||
|
onChange={(e) => setModelName(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="model-description">Description (Optional)</Label>
|
||||||
|
<Textarea
|
||||||
|
id="model-description"
|
||||||
|
placeholder="Describe the purpose of this model..."
|
||||||
|
rows={3}
|
||||||
|
value={modelDescription}
|
||||||
|
onChange={(e) => setModelDescription(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Model Type</Label>
|
||||||
|
<select className="w-full h-10 rounded-md border border-input bg-background px-3 py-2 text-sm">
|
||||||
|
<option value="regression">Regression (Default)</option>
|
||||||
|
<option value="neural-network">Neural Network</option>
|
||||||
|
<option value="ensemble">Ensemble</option>
|
||||||
|
<option value="geospatial">Geospatial</option>
|
||||||
|
<option value="time-series">Time Series</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-2">
|
||||||
|
<h3 className="text-sm font-medium mb-2">Advanced Settings</h3>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<Label htmlFor="feature-selection">Automatic Feature Selection</Label>
|
||||||
|
<p className="text-xs text-muted-foreground">Let AI select the most relevant features</p>
|
||||||
|
</div>
|
||||||
|
<Switch id="feature-selection" defaultChecked />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<Label htmlFor="hyperparameter-tuning">Hyperparameter Tuning</Label>
|
||||||
|
<p className="text-xs text-muted-foreground">Optimize model parameters automatically</p>
|
||||||
|
</div>
|
||||||
|
<Switch id="hyperparameter-tuning" defaultChecked />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<Label htmlFor="cross-validation">Cross-Validation</Label>
|
||||||
|
<p className="text-xs text-muted-foreground">Use k-fold cross-validation</p>
|
||||||
|
</div>
|
||||||
|
<Switch id="cross-validation" defaultChecked />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="md:col-span-2">
|
||||||
|
<Card className="mb-6">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Select Data Source</CardTitle>
|
||||||
|
<CardDescription>Choose a data pipeline to train your model</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{dataPipelines.map((pipeline) => (
|
||||||
|
<div
|
||||||
|
key={pipeline.id}
|
||||||
|
className={`p-4 border rounded-lg cursor-pointer transition-all ${
|
||||||
|
selectedPipeline === pipeline.id ? "border-primary bg-primary/5" : "hover:border-primary/50"
|
||||||
|
}`}
|
||||||
|
onClick={() => setSelectedPipeline(pipeline.id)}>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Database className="h-5 w-5 text-primary" />
|
||||||
|
<div>
|
||||||
|
<h3 className="font-medium">{pipeline.name}</h3>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{pipeline.records.toLocaleString()} records • Updated {pipeline.lastUpdated}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{selectedPipeline === pipeline.id && <Check className="h-5 w-5 text-primary" />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Training Process</CardTitle>
|
||||||
|
<CardDescription>Monitor and control the training process</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{isTraining ? (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between items-center text-sm">
|
||||||
|
<span>Training Progress</span>
|
||||||
|
<span>{trainingProgress}%</span>
|
||||||
|
</div>
|
||||||
|
<Progress value={trainingProgress} className="h-2" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="text-sm font-medium">Current Step:</div>
|
||||||
|
<div className="text-sm">
|
||||||
|
{trainingProgress < 20
|
||||||
|
? "Preparing data..."
|
||||||
|
: trainingProgress < 40
|
||||||
|
? "Feature engineering..."
|
||||||
|
: trainingProgress < 60
|
||||||
|
? "Training model..."
|
||||||
|
: trainingProgress < 80
|
||||||
|
? "Evaluating performance..."
|
||||||
|
: trainingProgress < 100
|
||||||
|
? "Finalizing model..."
|
||||||
|
: "Training complete!"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{trainingProgress < 100 && (
|
||||||
|
<Button variant="outline" className="w-full" onClick={() => setIsTraining(false)}>
|
||||||
|
Cancel Training
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{trainingProgress === 100 && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="p-3 bg-green-50 dark:bg-green-900/20 text-green-600 dark:text-green-400 rounded-md flex items-center gap-2">
|
||||||
|
<Check className="h-5 w-5" />
|
||||||
|
<span>Training completed successfully!</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button variant="outline" className="flex-1">
|
||||||
|
View Details
|
||||||
|
</Button>
|
||||||
|
<Button className="flex-1">Use Model</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="p-4 border border-dashed rounded-lg">
|
||||||
|
<div className="flex items-center gap-3 mb-3">
|
||||||
|
<BrainCircuit className="h-8 w-8 text-primary" />
|
||||||
|
<div>
|
||||||
|
<h3 className="font-medium">Ready to Train</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">Configure your settings and start training</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!selectedPipeline && (
|
||||||
|
<div className="p-3 bg-amber-50 dark:bg-amber-900/20 text-amber-600 dark:text-amber-400 rounded-md flex items-center gap-2 text-sm">
|
||||||
|
<AlertTriangle className="h-4 w-4" />
|
||||||
|
<span>Please select a data pipeline first</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!modelName && (
|
||||||
|
<div className="p-3 bg-amber-50 dark:bg-amber-900/20 text-amber-600 dark:text-amber-400 rounded-md flex items-center gap-2 text-sm mt-2">
|
||||||
|
<AlertTriangle className="h-4 w-4" />
|
||||||
|
<span>Please enter a model name</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button variant="outline" className="flex-1" onClick={() => setActiveTab("my-models")}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="flex-1 gap-2"
|
||||||
|
onClick={handleStartTraining}
|
||||||
|
disabled={!selectedPipeline || !modelName}>
|
||||||
|
<Play className="h-4 w-4" />
|
||||||
|
Start Training
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ModelCardProps {
|
||||||
|
model: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
hyperparameters: {
|
||||||
|
[key: string]: string;
|
||||||
|
};
|
||||||
|
dataSource: string;
|
||||||
|
status: string;
|
||||||
|
lastUpdated: string;
|
||||||
|
isSystem: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function ModelCard({ model }: ModelCardProps) {
|
||||||
|
return (
|
||||||
|
<Card className={model.isSystem ? "border-primary/20" : ""}>
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<div className="flex justify-between items-start">
|
||||||
|
<CardTitle className="text-lg">{model.name}</CardTitle>
|
||||||
|
<Badge variant={model.status === "active" ? "default" : "secondary"}>
|
||||||
|
{model.status === "active" ? "Active" : "Inactive"}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<CardDescription>{model.type} Model</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-1 mb-1">
|
||||||
|
<Database className="h-4 w-4 text-primary" />
|
||||||
|
<span className="text-sm font-medium">Data Source:</span>
|
||||||
|
</div>
|
||||||
|
{model.isSystem ? (
|
||||||
|
<div className="flex items-center gap-1 text-sm">
|
||||||
|
<Badge variant="outline" className="bg-primary/5">
|
||||||
|
System Base Model
|
||||||
|
</Badge>
|
||||||
|
<Info
|
||||||
|
className="h-4 w-4 text-muted-foreground cursor-help"
|
||||||
|
title="This is a pre-trained system model"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span className="text-sm">{model.dataSource}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-1 mb-1">
|
||||||
|
<Sliders className="h-4 w-4 text-primary" />
|
||||||
|
<span className="text-sm font-medium">Hyperparameters:</span>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 gap-1">
|
||||||
|
{Object.entries(model.hyperparameters).map(([key, value]) => (
|
||||||
|
<div key={key} className="flex justify-between text-xs">
|
||||||
|
<span className="text-muted-foreground">{key}:</span>
|
||||||
|
<span>{value}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center text-sm">
|
||||||
|
<Clock className="h-4 w-4 mr-2 text-muted-foreground" />
|
||||||
|
<span className="text-muted-foreground">Last updated:</span>
|
||||||
|
<span className="ml-1 font-medium">{model.lastUpdated}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter className="flex justify-between">
|
||||||
|
<Button variant="outline" size="sm" asChild>
|
||||||
|
<Link href={model.isSystem ? "/documentation/models" : "/models/details"}>View Details</Link>
|
||||||
|
</Button>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button variant="outline" size="icon" className="h-8 w-8 text-primary border-primary/20 hover:border-primary">
|
||||||
|
<Settings className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
{!model.isSystem && (
|
||||||
|
<Button variant="outline" size="icon" className="h-8 w-8 border-primary/20 hover:border-primary">
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button variant="outline" size="icon" className="h-8 w-8 border-primary/20 hover:border-primary">
|
||||||
|
<ArrowRight className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
704
frontend/app/(routes)/price-prediction/page.tsx
Normal file
704
frontend/app/(routes)/price-prediction/page.tsx
Normal file
@ -0,0 +1,704 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Progress } from "@/components/ui/progress";
|
||||||
|
import { Slider } from "@/components/ui/slider";
|
||||||
|
import {
|
||||||
|
ChevronRight,
|
||||||
|
Home,
|
||||||
|
BarChart2,
|
||||||
|
LineChart,
|
||||||
|
Droplets,
|
||||||
|
Wind,
|
||||||
|
Sun,
|
||||||
|
MapPin,
|
||||||
|
Bus,
|
||||||
|
School,
|
||||||
|
ShoppingBag,
|
||||||
|
Building,
|
||||||
|
ArrowRight,
|
||||||
|
Info,
|
||||||
|
FileText,
|
||||||
|
RefreshCw,
|
||||||
|
Check,
|
||||||
|
ChevronDown,
|
||||||
|
} from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import PageHeader from "@/components/page-header";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
|
||||||
|
export default function PricePredictionPage() {
|
||||||
|
const [propertySize, setPropertySize] = useState(150);
|
||||||
|
const [propertyAge, setPropertyAge] = useState(5);
|
||||||
|
const [adjustedPrice, setAdjustedPrice] = useState(15000000);
|
||||||
|
const [activeTab, setActiveTab] = useState("property-details");
|
||||||
|
const [selectedModel, setSelectedModel] = useState("Standard ML Model v2.4");
|
||||||
|
|
||||||
|
const models = [
|
||||||
|
"Standard ML Model v2.4",
|
||||||
|
"Enhanced Neural Network v1.8",
|
||||||
|
"Geospatial Regression v3.1",
|
||||||
|
"Time Series Forecast v2.0",
|
||||||
|
"Custom Model (User #1242)",
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleSizeChange = (value: number[]) => {
|
||||||
|
setPropertySize(value[0]);
|
||||||
|
// Simple calculation for demo purposes
|
||||||
|
const newPrice = 15000000 + (value[0] - 150) * 50000;
|
||||||
|
setAdjustedPrice(newPrice);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAgeChange = (value: number[]) => {
|
||||||
|
setPropertyAge(value[0]);
|
||||||
|
// Simple calculation for demo purposes
|
||||||
|
const newPrice = 15000000 - (value[0] - 5) * 200000;
|
||||||
|
setAdjustedPrice(newPrice);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGenerateReport = () => {
|
||||||
|
// In a real implementation, this would generate and download a PDF
|
||||||
|
alert("Generating PDF report with the selected model: " + selectedModel);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto p-6">
|
||||||
|
<div className="flex items-center text-sm text-muted-foreground mb-4">
|
||||||
|
<Link href="/maps" className="hover:text-foreground transition-colors">
|
||||||
|
Map
|
||||||
|
</Link>
|
||||||
|
<ChevronRight className="h-4 w-4 mx-1" />
|
||||||
|
<span className="font-medium text-foreground">Price Prediction Model</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<PageHeader
|
||||||
|
title="Explainable Price Prediction Model"
|
||||||
|
description="Understand how our AI model predicts property prices and what factors influence the valuation."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="outline" className="gap-2">
|
||||||
|
<BarChart2 className="h-4 w-4" />
|
||||||
|
{selectedModel}
|
||||||
|
<ChevronDown className="h-4 w-4 ml-2" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" className="w-[240px]">
|
||||||
|
{models.map((model) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
key={model}
|
||||||
|
onClick={() => setSelectedModel(model)}
|
||||||
|
className="flex items-center gap-2">
|
||||||
|
{model === selectedModel && <Check className="h-4 w-4 text-primary" />}
|
||||||
|
<span className={model === selectedModel ? "font-medium" : ""}>{model}</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
))}
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<Link href="/models" className="flex items-center w-full">
|
||||||
|
<span className="text-primary">Manage Models...</span>
|
||||||
|
</Link>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
|
||||||
|
<Button variant="outline" className="gap-2" onClick={() => setSelectedModel(selectedModel)}>
|
||||||
|
<RefreshCw className="h-4 w-4" />
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Tabs defaultValue="property-details" className="mt-6" onValueChange={setActiveTab}>
|
||||||
|
<TabsList className="grid grid-cols-5 mb-8">
|
||||||
|
<TabsTrigger value="property-details" className="flex items-center gap-2">
|
||||||
|
<Home className="h-4 w-4" />
|
||||||
|
<span>Property Details</span>
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="feature-analysis" className="flex items-center gap-2">
|
||||||
|
<BarChart2 className="h-4 w-4" />
|
||||||
|
<span>Feature Analysis</span>
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="market-comparison" className="flex items-center gap-2">
|
||||||
|
<LineChart className="h-4 w-4" />
|
||||||
|
<span>Market Comparison</span>
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="environmental-factors" className="flex items-center gap-2">
|
||||||
|
<Wind className="h-4 w-4" />
|
||||||
|
<span>Environmental Factors</span>
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="final-prediction" className="flex items-center gap-2">
|
||||||
|
<ArrowRight className="h-4 w-4" />
|
||||||
|
<span>Final Prediction</span>
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<div className="grid gap-6 md:grid-cols-3">
|
||||||
|
<div className="md:col-span-1">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Property Details</CardTitle>
|
||||||
|
<CardDescription>123 Sukhumvit Road, Bangkok</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-muted-foreground">Type</span>
|
||||||
|
<span className="font-medium">Condominium</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-muted-foreground">Size</span>
|
||||||
|
<span className="font-medium">{propertySize} sqm</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-muted-foreground">Bedrooms</span>
|
||||||
|
<span className="font-medium">3</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-muted-foreground">Bathrooms</span>
|
||||||
|
<span className="font-medium">2</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-muted-foreground">Age</span>
|
||||||
|
<span className="font-medium">{propertyAge} years</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-muted-foreground">Floor</span>
|
||||||
|
<span className="font-medium">15</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="mt-6">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Adjust Parameters</CardTitle>
|
||||||
|
<CardDescription>See how changes affect the prediction</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-sm font-medium">Property Size</span>
|
||||||
|
<span className="text-sm text-muted-foreground">{propertySize} sqm</span>
|
||||||
|
</div>
|
||||||
|
<Slider
|
||||||
|
defaultValue={[150]}
|
||||||
|
max={300}
|
||||||
|
min={50}
|
||||||
|
step={10}
|
||||||
|
onValueChange={handleSizeChange}
|
||||||
|
className="py-2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-sm font-medium">Property Age</span>
|
||||||
|
<span className="text-sm text-muted-foreground">{propertyAge} years</span>
|
||||||
|
</div>
|
||||||
|
<Slider
|
||||||
|
defaultValue={[5]}
|
||||||
|
max={20}
|
||||||
|
min={0}
|
||||||
|
step={1}
|
||||||
|
onValueChange={handleAgeChange}
|
||||||
|
className="py-2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-4 border-t">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-sm font-medium">Adjusted Price</span>
|
||||||
|
<span className="text-xl font-bold">฿{adjustedPrice.toLocaleString()}</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
{adjustedPrice > 15000000 ? "+" : ""}
|
||||||
|
{adjustedPrice - 15000000 === 0 ? "±0" : (adjustedPrice - 15000000).toLocaleString()} THB from
|
||||||
|
original prediction
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="md:col-span-2">
|
||||||
|
<TabsContent value="property-details" className="mt-0">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Property Overview</CardTitle>
|
||||||
|
<CardDescription>Basic information used in our prediction model</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="mb-6">
|
||||||
|
Our AI model begins by analyzing the core attributes of your property. These fundamental
|
||||||
|
characteristics form the baseline for our prediction.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Home className="h-5 w-5 text-primary mt-0.5" />
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium">Property Type</h4>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Condominium properties in this area have specific market dynamics
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<BarChart2 className="h-5 w-5 text-primary mt-0.5" />
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium">Size & Layout</h4>
|
||||||
|
<p className="text-sm text-muted-foreground">150 sqm with 3 bedrooms and 2 bathrooms</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Building className="h-5 w-5 text-primary mt-0.5" />
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium">Property Age</h4>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Built 5 years ago, affecting depreciation calculations
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<MapPin className="h-5 w-5 text-primary mt-0.5" />
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium">Floor & View</h4>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Located on floor 15, impacting value and desirability
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end mt-6">
|
||||||
|
<Button onClick={() => setActiveTab("feature-analysis")}>
|
||||||
|
Next Step <ArrowRight className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="feature-analysis" className="mt-0">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Feature Analysis</CardTitle>
|
||||||
|
<CardDescription>How different features impact the predicted price</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="mb-6">
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="space-y-4 mb-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="w-32 text-sm">Location</span>
|
||||||
|
<div className="flex-1 mx-4">
|
||||||
|
<Progress value={35} className="h-6 bg-muted" indicatorClassName="bg-emerald-500" />
|
||||||
|
</div>
|
||||||
|
<span className="w-12 text-right text-sm">+35%</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="w-32 text-sm">Size</span>
|
||||||
|
<div className="flex-1 mx-4">
|
||||||
|
<Progress value={25} className="h-6 bg-muted" indicatorClassName="bg-emerald-500" />
|
||||||
|
</div>
|
||||||
|
<span className="w-12 text-right text-sm">+25%</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="w-32 text-sm">Age</span>
|
||||||
|
<div className="flex-1 mx-4">
|
||||||
|
<Progress value={15} className="h-6 bg-muted" indicatorClassName="bg-red-500" />
|
||||||
|
</div>
|
||||||
|
<span className="w-12 text-right text-sm">-15%</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="w-32 text-sm">Amenities</span>
|
||||||
|
<div className="flex-1 mx-4">
|
||||||
|
<Progress value={10} className="h-6 bg-muted" indicatorClassName="bg-emerald-500" />
|
||||||
|
</div>
|
||||||
|
<span className="w-12 text-right text-sm">+10%</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="w-32 text-sm">Floor</span>
|
||||||
|
<div className="flex-1 mx-4">
|
||||||
|
<Progress value={8} className="h-6 bg-muted" indicatorClassName="bg-emerald-500" />
|
||||||
|
</div>
|
||||||
|
<span className="w-12 text-right text-sm">+8%</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="w-32 text-sm">Air Quality</span>
|
||||||
|
<div className="flex-1 mx-4">
|
||||||
|
<Progress value={7} className="h-6 bg-muted" indicatorClassName="bg-red-500" />
|
||||||
|
</div>
|
||||||
|
<span className="w-12 text-right text-sm">-7%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-muted/30 p-4 rounded-lg border mb-6">
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<Info className="h-5 w-5 text-primary mt-0.5 flex-shrink-0" />
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium">Key Insights</h4>
|
||||||
|
<ul className="text-sm text-muted-foreground mt-1 space-y-1 list-disc list-inside">
|
||||||
|
<li>Location is the strongest factor, contributing +35% to the price</li>
|
||||||
|
<li>The property's size (150 sqm) positively impacts the valuation</li>
|
||||||
|
<li>The age of the property (5 years) has a moderate negative impact (-15%)</li>
|
||||||
|
<li>Poor air quality in the area reduces the value by 7%</li>
|
||||||
|
<li>High floor (15) adds a premium of 8% to the property value</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button onClick={() => setActiveTab("market-comparison")}>
|
||||||
|
Next Step <ArrowRight className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="market-comparison" className="mt-0">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Market Comparison</CardTitle>
|
||||||
|
<CardDescription>How your property compares to similar properties in the area</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="mb-6">
|
||||||
|
Our model analyzes comparable properties in the same area to provide context for your property's
|
||||||
|
valuation. This helps ensure the prediction is aligned with current market conditions.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="overflow-x-auto mb-6">
|
||||||
|
<table className="w-full border-collapse">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-muted/50">
|
||||||
|
<th className="px-4 py-2 text-left text-sm font-medium">Property</th>
|
||||||
|
<th className="px-4 py-2 text-left text-sm font-medium">Size</th>
|
||||||
|
<th className="px-4 py-2 text-left text-sm font-medium">Bedrooms</th>
|
||||||
|
<th className="px-4 py-2 text-left text-sm font-medium">Age</th>
|
||||||
|
<th className="px-4 py-2 text-left text-sm font-medium">Floor</th>
|
||||||
|
<th className="px-4 py-2 text-left text-sm font-medium">Price</th>
|
||||||
|
<th className="px-4 py-2 text-left text-sm font-medium">Price/sqm</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y">
|
||||||
|
<tr className="bg-primary/5 font-medium">
|
||||||
|
<td className="px-4 py-2 text-sm">Your Property</td>
|
||||||
|
<td className="px-4 py-2 text-sm">150 sqm</td>
|
||||||
|
<td className="px-4 py-2 text-sm">3</td>
|
||||||
|
<td className="px-4 py-2 text-sm">5 years</td>
|
||||||
|
<td className="px-4 py-2 text-sm">15</td>
|
||||||
|
<td className="px-4 py-2 text-sm">฿15,000,000</td>
|
||||||
|
<td className="px-4 py-2 text-sm">฿100,000</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className="px-4 py-2 text-sm">Comp #1</td>
|
||||||
|
<td className="px-4 py-2 text-sm">145 sqm</td>
|
||||||
|
<td className="px-4 py-2 text-sm">3</td>
|
||||||
|
<td className="px-4 py-2 text-sm">4 years</td>
|
||||||
|
<td className="px-4 py-2 text-sm">12</td>
|
||||||
|
<td className="px-4 py-2 text-sm">฿14,500,000</td>
|
||||||
|
<td className="px-4 py-2 text-sm">฿100,000</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className="px-4 py-2 text-sm">Comp #2</td>
|
||||||
|
<td className="px-4 py-2 text-sm">160 sqm</td>
|
||||||
|
<td className="px-4 py-2 text-sm">3</td>
|
||||||
|
<td className="px-4 py-2 text-sm">6 years</td>
|
||||||
|
<td className="px-4 py-2 text-sm">18</td>
|
||||||
|
<td className="px-4 py-2 text-sm">฿15,800,000</td>
|
||||||
|
<td className="px-4 py-2 text-sm">฿98,750</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className="px-4 py-2 text-sm">Comp #3</td>
|
||||||
|
<td className="px-4 py-2 text-sm">140 sqm</td>
|
||||||
|
<td className="px-4 py-2 text-sm">2</td>
|
||||||
|
<td className="px-4 py-2 text-sm">3 years</td>
|
||||||
|
<td className="px-4 py-2 text-sm">10</td>
|
||||||
|
<td className="px-4 py-2 text-sm">฿13,800,000</td>
|
||||||
|
<td className="px-4 py-2 text-sm">฿98,571</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className="px-4 py-2 text-sm">Comp #4</td>
|
||||||
|
<td className="px-4 py-2 text-sm">155 sqm</td>
|
||||||
|
<td className="px-4 py-2 text-sm">3</td>
|
||||||
|
<td className="px-4 py-2 text-sm">7 years</td>
|
||||||
|
<td className="px-4 py-2 text-sm">14</td>
|
||||||
|
<td className="px-4 py-2 text-sm">฿14,700,000</td>
|
||||||
|
<td className="px-4 py-2 text-sm">฿94,839</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-muted/30 p-4 rounded-lg border mb-6">
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<Info className="h-5 w-5 text-primary mt-0.5 flex-shrink-0" />
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium">Market Analysis</h4>
|
||||||
|
<ul className="text-sm text-muted-foreground mt-1 space-y-1 list-disc list-inside">
|
||||||
|
<li>Your property's predicted price is in line with comparable properties</li>
|
||||||
|
<li>The price per square meter (฿100,000) is slightly above the area average</li>
|
||||||
|
<li>Properties on higher floors command a 3-5% premium in this building</li>
|
||||||
|
<li>Recent sales in this area show a stable market with slight appreciation</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button onClick={() => setActiveTab("environmental-factors")}>
|
||||||
|
Next Step <ArrowRight className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="environmental-factors" className="mt-0">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Environmental Factors</CardTitle>
|
||||||
|
<CardDescription>How environmental conditions affect the property value</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="mb-6">
|
||||||
|
Environmental factors can significantly impact property values. Our model considers various
|
||||||
|
environmental conditions to provide a more accurate prediction.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex flex-col items-center text-center">
|
||||||
|
<Droplets className="h-8 w-8 text-blue-500 mb-2" />
|
||||||
|
<h4 className="font-medium">Flood Risk</h4>
|
||||||
|
<Badge className="mt-2 bg-amber-500">Moderate</Badge>
|
||||||
|
<p className="text-xs text-muted-foreground mt-2">
|
||||||
|
Historical data shows moderate flood risk in this area
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex flex-col items-center text-center">
|
||||||
|
<Wind className="h-8 w-8 text-purple-500 mb-2" />
|
||||||
|
<h4 className="font-medium">Air Quality</h4>
|
||||||
|
<Badge className="mt-2 bg-destructive">Poor</Badge>
|
||||||
|
<p className="text-xs text-muted-foreground mt-2">
|
||||||
|
Air quality is below average, affecting property value
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex flex-col items-center text-center">
|
||||||
|
<Sun className="h-8 w-8 text-amber-500 mb-2" />
|
||||||
|
<h4 className="font-medium">Noise Level</h4>
|
||||||
|
<Badge className="mt-2 bg-green-500">Low</Badge>
|
||||||
|
<p className="text-xs text-muted-foreground mt-2">
|
||||||
|
The area has relatively low noise pollution
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h4 className="font-medium text-lg mb-3">Proximity to Amenities</h4>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-4 flex items-center gap-3">
|
||||||
|
<Bus className="h-5 w-5 text-primary" />
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium">Public Transport</h4>
|
||||||
|
<p className="text-sm text-muted-foreground">300m</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-4 flex items-center gap-3">
|
||||||
|
<School className="h-5 w-5 text-primary" />
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium">Schools</h4>
|
||||||
|
<p className="text-sm text-muted-foreground">1.2km</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-4 flex items-center gap-3">
|
||||||
|
<ShoppingBag className="h-5 w-5 text-primary" />
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium">Shopping</h4>
|
||||||
|
<p className="text-sm text-muted-foreground">500m</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-4 flex items-center gap-3">
|
||||||
|
<Building className="h-5 w-5 text-primary" />
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium">Hospitals</h4>
|
||||||
|
<p className="text-sm text-muted-foreground">2.5km</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-muted/30 p-4 rounded-lg border mb-6">
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<Info className="h-5 w-5 text-primary mt-0.5 flex-shrink-0" />
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium">Environmental Impact Analysis</h4>
|
||||||
|
<ul className="text-sm text-muted-foreground mt-1 space-y-1 list-disc list-inside">
|
||||||
|
<li>The moderate flood risk reduces the property value by approximately 3%</li>
|
||||||
|
<li>Poor air quality has a negative impact of about 7% on the valuation</li>
|
||||||
|
<li>Excellent proximity to public transport adds a 4% premium</li>
|
||||||
|
<li>Overall, environmental factors have a -6% net impact on the property value</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button onClick={() => setActiveTab("final-prediction")}>
|
||||||
|
Next Step <ArrowRight className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="final-prediction" className="mt-0">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Final Prediction</CardTitle>
|
||||||
|
<CardDescription>The predicted price and confidence level</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="bg-muted/30 p-6 rounded-lg text-center mb-6">
|
||||||
|
<h3 className="text-lg text-muted-foreground mb-1">Predicted Price</h3>
|
||||||
|
<div className="text-4xl font-bold mb-2">฿15,000,000</div>
|
||||||
|
<p className="text-sm text-muted-foreground">Confidence Level: 92%</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card className="mb-6">
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Info className="h-5 w-5 text-primary" />
|
||||||
|
<CardTitle className="text-base">Price Range</CardTitle>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
|
Based on our model's confidence level, the price could range between:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-3 gap-4 text-center">
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-medium text-muted-foreground">Lower Bound</h4>
|
||||||
|
<p className="font-medium">฿14,250,000</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-medium text-muted-foreground">Prediction</h4>
|
||||||
|
<p className="font-bold text-primary">฿15,000,000</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-medium text-muted-foreground">Upper Bound</h4>
|
||||||
|
<p className="font-medium">฿15,750,000</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<h3 className="text-lg font-medium mb-3">Summary of Factors</h3>
|
||||||
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
|
The final prediction is based on a combination of all factors analyzed in previous steps:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul className="space-y-2 mb-6">
|
||||||
|
<li className="flex items-start gap-2">
|
||||||
|
<div className="h-5 w-5 rounded-full bg-blue-500 flex-shrink-0 mt-0.5"></div>
|
||||||
|
<span>Property characteristics (size, age, layout)</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-2">
|
||||||
|
<div className="h-5 w-5 rounded-full bg-green-500 flex-shrink-0 mt-0.5"></div>
|
||||||
|
<span>Location and neighborhood analysis</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-2">
|
||||||
|
<div className="h-5 w-5 rounded-full bg-amber-500 flex-shrink-0 mt-0.5"></div>
|
||||||
|
<span>Market trends and comparable properties</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-2">
|
||||||
|
<div className="h-5 w-5 rounded-full bg-red-500 flex-shrink-0 mt-0.5"></div>
|
||||||
|
<span>Environmental factors and amenities</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div className="flex flex-col md:flex-row gap-4 justify-between">
|
||||||
|
<Button variant="outline" onClick={() => setActiveTab("environmental-factors")}>
|
||||||
|
Previous
|
||||||
|
</Button>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button variant="outline" className="gap-2" onClick={handleGenerateReport}>
|
||||||
|
<FileText className="h-4 w-4" />
|
||||||
|
Generate PDF Report
|
||||||
|
</Button>
|
||||||
|
<Button variant="default" asChild>
|
||||||
|
<Link href="/maps">Back to Map</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
609
frontend/app/(routes)/properties/[id]/page.tsx
Normal file
609
frontend/app/(routes)/properties/[id]/page.tsx
Normal file
@ -0,0 +1,609 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useState } from "react"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
|
||||||
|
import { Badge } from "@/components/ui/badge"
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||||
|
import {
|
||||||
|
MapPin,
|
||||||
|
Building,
|
||||||
|
Bath,
|
||||||
|
BedDouble,
|
||||||
|
Share,
|
||||||
|
ChevronRight,
|
||||||
|
Info,
|
||||||
|
Ruler,
|
||||||
|
Clock,
|
||||||
|
Star,
|
||||||
|
Droplets,
|
||||||
|
Wind,
|
||||||
|
Sun,
|
||||||
|
BarChart2,
|
||||||
|
LineChart,
|
||||||
|
Calendar,
|
||||||
|
Download,
|
||||||
|
FileText,
|
||||||
|
} from "lucide-react"
|
||||||
|
import Link from "next/link"
|
||||||
|
|
||||||
|
export default function PropertyDetailPage({ params }: { params: { id: string } }) {
|
||||||
|
const [liked, setLiked] = useState(false)
|
||||||
|
|
||||||
|
// This would normally come from an API call using the ID
|
||||||
|
const property = {
|
||||||
|
id: params.id,
|
||||||
|
title: "Modern Condominium in Sukhumvit",
|
||||||
|
price: 15000000,
|
||||||
|
location: "Sukhumvit Soi 24, Bangkok",
|
||||||
|
bedrooms: 3,
|
||||||
|
bathrooms: 2,
|
||||||
|
area: 150,
|
||||||
|
type: "Condominium",
|
||||||
|
description:
|
||||||
|
"This stunning modern condominium is located in the heart of Sukhumvit, one of Bangkok's most vibrant neighborhoods. The property features 3 spacious bedrooms, 2 bathrooms, and a large living area with floor-to-ceiling windows offering panoramic city views. The unit comes fully furnished with high-end appliances and fixtures. The building amenities include a swimming pool, fitness center, sauna, and 24-hour security.",
|
||||||
|
features: [
|
||||||
|
"Floor-to-ceiling windows",
|
||||||
|
"Fully furnished",
|
||||||
|
"High-end appliances",
|
||||||
|
"Marble countertops",
|
||||||
|
"Hardwood floors",
|
||||||
|
"Central air conditioning",
|
||||||
|
"Walk-in closet",
|
||||||
|
"Balcony with city view",
|
||||||
|
],
|
||||||
|
amenities: [
|
||||||
|
"Swimming pool",
|
||||||
|
"Fitness center",
|
||||||
|
"Sauna",
|
||||||
|
"24-hour security",
|
||||||
|
"Parking",
|
||||||
|
"Garden",
|
||||||
|
"Playground",
|
||||||
|
"BBQ area",
|
||||||
|
],
|
||||||
|
images: [
|
||||||
|
"/placeholder.svg?height=500&width=800",
|
||||||
|
"/placeholder.svg?height=500&width=800",
|
||||||
|
"/placeholder.svg?height=500&width=800",
|
||||||
|
"/placeholder.svg?height=500&width=800",
|
||||||
|
],
|
||||||
|
yearBuilt: 2018,
|
||||||
|
floorLevel: 15,
|
||||||
|
totalFloors: 32,
|
||||||
|
parkingSpaces: 1,
|
||||||
|
furnished: "Fully Furnished",
|
||||||
|
ownership: "Freehold",
|
||||||
|
availableFrom: "Immediate",
|
||||||
|
premium: true,
|
||||||
|
priceHistory: [
|
||||||
|
{ date: "2018", price: 12000000 },
|
||||||
|
{ date: "2020", price: 13500000 },
|
||||||
|
{ date: "2022", price: 14800000 },
|
||||||
|
{ date: "2024", price: 15000000 },
|
||||||
|
],
|
||||||
|
marketTrends: {
|
||||||
|
areaGrowth: 5.2,
|
||||||
|
similarProperties: 8,
|
||||||
|
averagePrice: 14500000,
|
||||||
|
pricePerSqm: 100000,
|
||||||
|
},
|
||||||
|
environmentalFactors: {
|
||||||
|
floodRisk: "Moderate",
|
||||||
|
airQuality: "Poor",
|
||||||
|
noiseLevel: "Low",
|
||||||
|
},
|
||||||
|
nearbyFacilities: [
|
||||||
|
{ name: "BTS Phrom Phong Station", distance: 300 },
|
||||||
|
{ name: "EmQuartier Shopping Mall", distance: 500 },
|
||||||
|
{ name: "Benchasiri Park", distance: 700 },
|
||||||
|
{ name: "Samitivej Hospital", distance: 1200 },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto p-6">
|
||||||
|
<div className="flex items-center text-sm text-muted-foreground mb-4">
|
||||||
|
<Link href="/" className="hover:text-foreground transition-colors">
|
||||||
|
Home
|
||||||
|
</Link>
|
||||||
|
<ChevronRight className="h-4 w-4 mx-1" />
|
||||||
|
<Link href="/properties" className="hover:text-foreground transition-colors">
|
||||||
|
Properties
|
||||||
|
</Link>
|
||||||
|
<ChevronRight className="h-4 w-4 mx-1" />
|
||||||
|
<span className="font-medium text-foreground">{property.title}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col lg:flex-row gap-6">
|
||||||
|
<div className="lg:w-2/3">
|
||||||
|
{/* Property Images */}
|
||||||
|
<div className="relative mb-6 rounded-lg overflow-hidden">
|
||||||
|
<img
|
||||||
|
src={property.images[0] || "/placeholder.svg"}
|
||||||
|
alt={property.title}
|
||||||
|
className="w-full h-[400px] object-cover"
|
||||||
|
/>
|
||||||
|
<div className="absolute bottom-4 right-4 flex gap-2">
|
||||||
|
<Button variant="secondary" size="sm" className="h-8 gap-1">
|
||||||
|
<Share className="h-4 w-4" />
|
||||||
|
<span>Share</span>
|
||||||
|
</Button>
|
||||||
|
<Button variant="secondary" size="sm" className="h-8 gap-1">
|
||||||
|
<Download className="h-4 w-4" />
|
||||||
|
<span>Export Data</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="absolute top-4 left-4 flex gap-1">
|
||||||
|
<Badge className="bg-primary">{property.type}</Badge>
|
||||||
|
{property.premium && (
|
||||||
|
<Badge className="bg-amber-500">
|
||||||
|
<Star className="h-3 w-3 mr-1" /> Premium
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Thumbnail Images */}
|
||||||
|
<div className="grid grid-cols-4 gap-2 mb-6">
|
||||||
|
{property.images.map((image, index) => (
|
||||||
|
<div key={index} className="rounded-md overflow-hidden">
|
||||||
|
<img
|
||||||
|
src={image || "/placeholder.svg"}
|
||||||
|
alt={`${property.title} - Image ${index + 1}`}
|
||||||
|
className="w-full h-24 object-cover cursor-pointer hover:opacity-80 transition-opacity"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Property Details */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-2 mb-2">
|
||||||
|
<h1 className="text-2xl font-semibold">{property.title}</h1>
|
||||||
|
<div className="text-2xl font-bold">฿{property.price.toLocaleString()}</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center text-muted-foreground text-sm mb-4">
|
||||||
|
<MapPin className="h-4 w-4 mr-1 flex-shrink-0" />
|
||||||
|
<span>{property.location}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap items-center gap-6 mb-6">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<BedDouble className="h-5 w-5 mr-2 text-primary" />
|
||||||
|
<div>
|
||||||
|
<div className="font-medium">{property.bedrooms}</div>
|
||||||
|
<div className="text-xs text-muted-foreground">Bedrooms</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Bath className="h-5 w-5 mr-2 text-primary" />
|
||||||
|
<div>
|
||||||
|
<div className="font-medium">{property.bathrooms}</div>
|
||||||
|
<div className="text-xs text-muted-foreground">Bathrooms</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Ruler className="h-5 w-5 mr-2 text-primary" />
|
||||||
|
<div>
|
||||||
|
<div className="font-medium">{property.area} m²</div>
|
||||||
|
<div className="text-xs text-muted-foreground">Area</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Building className="h-5 w-5 mr-2 text-primary" />
|
||||||
|
<div>
|
||||||
|
<div className="font-medium">Floor {property.floorLevel}</div>
|
||||||
|
<div className="text-xs text-muted-foreground">of {property.totalFloors}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Calendar className="h-5 w-5 mr-2 text-primary" />
|
||||||
|
<div>
|
||||||
|
<div className="font-medium">{property.yearBuilt}</div>
|
||||||
|
<div className="text-xs text-muted-foreground">Year Built</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Tabs defaultValue="analytics">
|
||||||
|
<TabsList className="grid grid-cols-4 mb-4">
|
||||||
|
<TabsTrigger value="description">Description</TabsTrigger>
|
||||||
|
<TabsTrigger value="features">Features</TabsTrigger>
|
||||||
|
<TabsTrigger value="location">Location</TabsTrigger>
|
||||||
|
<TabsTrigger value="analytics">Analytics</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value="description" className="mt-0">
|
||||||
|
<p className="text-sm leading-relaxed mb-4">{property.description}</p>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-x-8 gap-y-2 mt-6">
|
||||||
|
<div className="flex justify-between py-2 border-b text-sm">
|
||||||
|
<span className="text-muted-foreground">Property Type</span>
|
||||||
|
<span className="font-medium">{property.type}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between py-2 border-b text-sm">
|
||||||
|
<span className="text-muted-foreground">Furnishing</span>
|
||||||
|
<span className="font-medium">{property.furnished}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between py-2 border-b text-sm">
|
||||||
|
<span className="text-muted-foreground">Ownership</span>
|
||||||
|
<span className="font-medium">{property.ownership}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between py-2 border-b text-sm">
|
||||||
|
<span className="text-muted-foreground">Available From</span>
|
||||||
|
<span className="font-medium">{property.availableFrom}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between py-2 border-b text-sm">
|
||||||
|
<span className="text-muted-foreground">Parking Spaces</span>
|
||||||
|
<span className="font-medium">{property.parkingSpaces}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="features" className="mt-0">
|
||||||
|
<div className="mb-4">
|
||||||
|
<h3 className="font-medium mb-2">Property Features</h3>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||||
|
{property.features.map((feature, index) => (
|
||||||
|
<div key={index} className="flex items-center text-sm">
|
||||||
|
<div className="h-1.5 w-1.5 rounded-full bg-primary mr-2"></div>
|
||||||
|
{feature}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="font-medium mb-2">Building Amenities</h3>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||||
|
{property.amenities.map((amenity, index) => (
|
||||||
|
<div key={index} className="flex items-center text-sm">
|
||||||
|
<div className="h-1.5 w-1.5 rounded-full bg-primary mr-2"></div>
|
||||||
|
{amenity}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="location" className="mt-0">
|
||||||
|
<div className="bg-muted h-[300px] rounded-lg flex items-center justify-center mb-4">
|
||||||
|
<Link href={`/maps?property=${property.id}`}>
|
||||||
|
<Button>View on Map</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<h3 className="font-medium mb-2">Nearby Facilities</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{property.nearbyFacilities.map((facility, index) => (
|
||||||
|
<div key={index} className="flex items-center justify-between text-sm">
|
||||||
|
<span>{facility.name}</span>
|
||||||
|
<span className="text-muted-foreground">{facility.distance}m</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="font-medium mb-2">Environmental Factors</h3>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Droplets className="h-4 w-4 text-blue-500 mr-2" />
|
||||||
|
<span className="text-sm">Flood Risk</span>
|
||||||
|
</div>
|
||||||
|
<Badge className="bg-amber-500">{property.environmentalFactors.floodRisk}</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Wind className="h-4 w-4 text-purple-500 mr-2" />
|
||||||
|
<span className="text-sm">Air Quality</span>
|
||||||
|
</div>
|
||||||
|
<Badge className="bg-destructive">{property.environmentalFactors.airQuality}</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Sun className="h-4 w-4 text-amber-500 mr-2" />
|
||||||
|
<span className="text-sm">Noise Level</span>
|
||||||
|
</div>
|
||||||
|
<Badge className="bg-green-500">{property.environmentalFactors.noiseLevel}</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="analytics" className="mt-0">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-base flex items-center">
|
||||||
|
<BarChart2 className="h-4 w-4 mr-2 text-primary" />
|
||||||
|
Price Prediction
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold mb-1">฿15,000,000</div>
|
||||||
|
<p className="text-sm text-muted-foreground mb-3">The estimated price based on various factors</p>
|
||||||
|
|
||||||
|
<Link href="/price-prediction">
|
||||||
|
<Button size="sm" className="gap-1">
|
||||||
|
<Info className="h-4 w-4" />
|
||||||
|
<span>View Detailed Analysis</span>
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-base flex items-center">
|
||||||
|
<LineChart className="h-4 w-4 mr-2 text-primary" />
|
||||||
|
Market Trends
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex items-center justify-between mb-1">
|
||||||
|
<span className="text-sm">Area Price Trend</span>
|
||||||
|
<Badge className="bg-green-500">Rising</Badge>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground mb-3">
|
||||||
|
Prices in this area have increased by {property.marketTrends.areaGrowth}% in the last year
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between text-sm mb-1">
|
||||||
|
<span>Average Price in Area</span>
|
||||||
|
<span className="font-medium">฿{property.marketTrends.averagePrice.toLocaleString()}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between text-sm">
|
||||||
|
<span>Price per sqm</span>
|
||||||
|
<span className="font-medium">฿{property.marketTrends.pricePerSqm.toLocaleString()}</span>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card className="mb-6">
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-base flex items-center">
|
||||||
|
<LineChart className="h-4 w-4 mr-2 text-primary" />
|
||||||
|
Price History
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="h-40 w-full relative mb-4">
|
||||||
|
{/* Simple line chart simulation */}
|
||||||
|
<div className="absolute bottom-0 left-0 w-full h-px bg-border"></div>
|
||||||
|
<div className="absolute bottom-0 left-0 h-full flex items-end w-full">
|
||||||
|
{property.priceHistory.map((item, index) => (
|
||||||
|
<div key={index} className="flex-1 flex flex-col items-center">
|
||||||
|
<div
|
||||||
|
className="w-full border-b-2 border-r-2 border-primary rounded-br"
|
||||||
|
style={{
|
||||||
|
height: `${(item.price / 15000000) * 100}%`,
|
||||||
|
maxHeight: "90%",
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
<div className="text-xs mt-1">{item.date}</div>
|
||||||
|
<div className="text-xs text-muted-foreground">฿{(item.price / 1000000).toFixed(1)}M</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center text-xs text-muted-foreground">
|
||||||
|
<Clock className="h-3 w-3 mr-1" />
|
||||||
|
<span>Last updated: 2 days ago</span>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-base flex items-center">
|
||||||
|
<FileText className="h-4 w-4 mr-2 text-primary" />
|
||||||
|
Data Reports
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
|
<Button variant="outline" size="sm" className="justify-start">
|
||||||
|
<Download className="h-4 w-4 mr-2" />
|
||||||
|
Property Analysis Report
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" size="sm" className="justify-start">
|
||||||
|
<Download className="h-4 w-4 mr-2" />
|
||||||
|
Market Comparison Data
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" size="sm" className="justify-start">
|
||||||
|
<Download className="h-4 w-4 mr-2" />
|
||||||
|
Environmental Assessment
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" size="sm" className="justify-start">
|
||||||
|
<Download className="h-4 w-4 mr-2" />
|
||||||
|
Historical Price Data
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="lg:w-1/3">
|
||||||
|
{/* Analytics Summary Card */}
|
||||||
|
<Card className="mb-6 sticky top-6">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-lg">Analytics Summary</CardTitle>
|
||||||
|
<CardDescription>Key insights about this property</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-medium mb-2">Price Analysis</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-sm">Current Price</span>
|
||||||
|
<span className="font-medium">฿{property.price.toLocaleString()}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-sm">Price per sqm</span>
|
||||||
|
<span className="font-medium">฿{property.marketTrends.pricePerSqm.toLocaleString()}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-sm">Area Average</span>
|
||||||
|
<span className="font-medium">฿{property.marketTrends.averagePrice.toLocaleString()}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-sm">Price Trend</span>
|
||||||
|
<Badge className="bg-green-500">+{property.marketTrends.areaGrowth}%</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-medium mb-2">Environmental Assessment</h3>
|
||||||
|
<div className="grid grid-cols-3 gap-2">
|
||||||
|
<div className="flex flex-col items-center p-2 border rounded-md">
|
||||||
|
<Droplets className="h-5 w-5 text-blue-500 mb-1" />
|
||||||
|
<span className="text-xs font-medium">Flood Risk</span>
|
||||||
|
<Badge className="mt-1 text-xs bg-amber-500">{property.environmentalFactors.floodRisk}</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center p-2 border rounded-md">
|
||||||
|
<Wind className="h-5 w-5 text-purple-500 mb-1" />
|
||||||
|
<span className="text-xs font-medium">Air Quality</span>
|
||||||
|
<Badge className="mt-1 text-xs bg-destructive">{property.environmentalFactors.airQuality}</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center p-2 border rounded-md">
|
||||||
|
<Sun className="h-5 w-5 text-amber-500 mb-1" />
|
||||||
|
<span className="text-xs font-medium">Noise</span>
|
||||||
|
<Badge className="mt-1 text-xs bg-green-500">{property.environmentalFactors.noiseLevel}</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-medium mb-2">Nearby Facilities</h3>
|
||||||
|
<div className="space-y-1 text-sm">
|
||||||
|
{property.nearbyFacilities.map((facility, index) => (
|
||||||
|
<div key={index} className="flex justify-between">
|
||||||
|
<span>{facility.name}</span>
|
||||||
|
<span className="text-muted-foreground">{facility.distance}m</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-2">
|
||||||
|
<Link href="/price-prediction">
|
||||||
|
<Button className="w-full gap-2">
|
||||||
|
<BarChart2 className="h-4 w-4" />
|
||||||
|
View Full Price Analysis
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Link href={`/maps?property=${property.id}`}>
|
||||||
|
<Button variant="outline" className="w-full gap-2">
|
||||||
|
<MapPin className="h-4 w-4" />
|
||||||
|
View on Map
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Similar Properties */}
|
||||||
|
<div>
|
||||||
|
<h3 className="font-medium mb-3">Similar Properties</h3>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<SimilarPropertyCard
|
||||||
|
id="sim1"
|
||||||
|
title="Luxury Condo in Asoke"
|
||||||
|
price={13500000}
|
||||||
|
location="Asoke, Bangkok"
|
||||||
|
bedrooms={2}
|
||||||
|
bathrooms={2}
|
||||||
|
area={120}
|
||||||
|
image="/placeholder.svg?height=80&width=120"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SimilarPropertyCard
|
||||||
|
id="sim2"
|
||||||
|
title="Modern Apartment in Thonglor"
|
||||||
|
price={16800000}
|
||||||
|
location="Thonglor, Bangkok"
|
||||||
|
bedrooms={3}
|
||||||
|
bathrooms={2}
|
||||||
|
area={160}
|
||||||
|
image="/placeholder.svg?height=80&width=120"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SimilarPropertyCard
|
||||||
|
id="sim3"
|
||||||
|
title="Spacious Condo with City View"
|
||||||
|
price={14200000}
|
||||||
|
location="Phrom Phong, Bangkok"
|
||||||
|
bedrooms={2}
|
||||||
|
bathrooms={2}
|
||||||
|
area={135}
|
||||||
|
image="/placeholder.svg?height=80&width=120"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SimilarPropertyCardProps {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
price: number
|
||||||
|
location: string
|
||||||
|
bedrooms: number
|
||||||
|
bathrooms: number
|
||||||
|
area: number
|
||||||
|
image: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function SimilarPropertyCard({
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
price,
|
||||||
|
location,
|
||||||
|
bedrooms,
|
||||||
|
bathrooms,
|
||||||
|
area,
|
||||||
|
image,
|
||||||
|
}: SimilarPropertyCardProps) {
|
||||||
|
return (
|
||||||
|
<Link href={`/properties/${id}`}>
|
||||||
|
<div className="property-card">
|
||||||
|
<div className="flex">
|
||||||
|
<div className="w-24 h-20 flex-shrink-0">
|
||||||
|
<img src={image || "/placeholder.svg"} alt={title} className="w-full h-full object-cover rounded-l-lg" />
|
||||||
|
</div>
|
||||||
|
<div className="p-2 flex-1">
|
||||||
|
<h4 className="font-medium text-sm line-clamp-1">{title}</h4>
|
||||||
|
<div className="text-xs text-muted-foreground mb-1">{location}</div>
|
||||||
|
<div className="flex items-center gap-2 text-xs">
|
||||||
|
<span>{bedrooms} bd</span>
|
||||||
|
<span>•</span>
|
||||||
|
<span>{bathrooms} ba</span>
|
||||||
|
<span>•</span>
|
||||||
|
<span>{area} m²</span>
|
||||||
|
</div>
|
||||||
|
<div className="font-medium text-sm mt-1">฿{price.toLocaleString()}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
4
frontend/app/(routes)/properties/loading.tsx
Normal file
4
frontend/app/(routes)/properties/loading.tsx
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export default function Loading() {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
457
frontend/app/(routes)/properties/page.tsx
Normal file
457
frontend/app/(routes)/properties/page.tsx
Normal file
@ -0,0 +1,457 @@
|
|||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Card, CardContent } from "@/components/ui/card"
|
||||||
|
import { Badge } from "@/components/ui/badge"
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||||
|
import { Slider } from "@/components/ui/slider"
|
||||||
|
import { MapPin, Home, Bath, BedDouble, ArrowRight, Search, Star } from "lucide-react"
|
||||||
|
import Link from "next/link"
|
||||||
|
import PageHeader from "@/components/page-header"
|
||||||
|
|
||||||
|
export default function PropertiesPage() {
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto p-6">
|
||||||
|
<PageHeader
|
||||||
|
title="Property Listings"
|
||||||
|
description="Browse and filter available properties"
|
||||||
|
breadcrumb={[
|
||||||
|
{ title: "Home", href: "/" },
|
||||||
|
{ title: "Properties", href: "/properties" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex flex-col lg:flex-row gap-6 mt-6">
|
||||||
|
{/* Filters Sidebar */}
|
||||||
|
<div className="w-full lg:w-72 flex-shrink-0">
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="font-medium">Filters</h3>
|
||||||
|
<Button variant="ghost" size="sm" className="h-8 px-2 text-xs">
|
||||||
|
Reset All
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium mb-1.5 block">Price Range</label>
|
||||||
|
<div className="flex items-center justify-between text-xs text-muted-foreground mb-2">
|
||||||
|
<span>฿5,000,000</span>
|
||||||
|
<span>฿20,000,000</span>
|
||||||
|
</div>
|
||||||
|
<Slider
|
||||||
|
defaultValue={[5000000, 20000000]}
|
||||||
|
max={50000000}
|
||||||
|
min={1000000}
|
||||||
|
step={1000000}
|
||||||
|
className="mb-6"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium mb-1.5 block">Property Type</label>
|
||||||
|
<Select defaultValue="any">
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Any Type" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="any">Any Type</SelectItem>
|
||||||
|
<SelectItem value="house">House</SelectItem>
|
||||||
|
<SelectItem value="condo">Condominium</SelectItem>
|
||||||
|
<SelectItem value="townhouse">Townhouse</SelectItem>
|
||||||
|
<SelectItem value="land">Land</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium mb-1.5 block">Bedrooms</label>
|
||||||
|
<div className="grid grid-cols-5 gap-1">
|
||||||
|
{["Any", "1", "2", "3", "4+"].map((num) => (
|
||||||
|
<Button
|
||||||
|
key={num}
|
||||||
|
variant={num === "Any" ? "default" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
className="text-xs h-8"
|
||||||
|
>
|
||||||
|
{num}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium mb-1.5 block">Bathrooms</label>
|
||||||
|
<div className="grid grid-cols-5 gap-1">
|
||||||
|
{["Any", "1", "2", "3", "4+"].map((num) => (
|
||||||
|
<Button
|
||||||
|
key={num}
|
||||||
|
variant={num === "Any" ? "default" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
className="text-xs h-8"
|
||||||
|
>
|
||||||
|
{num}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium mb-1.5 block">Area (sqm)</label>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<Input placeholder="Min" className="h-9" />
|
||||||
|
<Input placeholder="Max" className="h-9" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium mb-1.5 block">Location</label>
|
||||||
|
<Select defaultValue="bangkok">
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select Location" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="bangkok">Bangkok</SelectItem>
|
||||||
|
<SelectItem value="phuket">Phuket</SelectItem>
|
||||||
|
<SelectItem value="chiangmai">Chiang Mai</SelectItem>
|
||||||
|
<SelectItem value="pattaya">Pattaya</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-2">
|
||||||
|
<Button className="w-full">Apply Filters</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<div className="flex-1">
|
||||||
|
{/* Search and Sort Bar */}
|
||||||
|
<div className="flex flex-col sm:flex-row gap-3 mb-4">
|
||||||
|
<div className="relative flex-1">
|
||||||
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Input placeholder="Search properties..." className="pl-9" />
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Select defaultValue="recommended">
|
||||||
|
<SelectTrigger className="w-[180px]">
|
||||||
|
<SelectValue placeholder="Sort by" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="recommended">Recommended</SelectItem>
|
||||||
|
<SelectItem value="price-asc">Price: Low to High</SelectItem>
|
||||||
|
<SelectItem value="price-desc">Price: High to Low</SelectItem>
|
||||||
|
<SelectItem value="newest">Newest First</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Link href="/maps">
|
||||||
|
<Button variant="outline" className="gap-2">
|
||||||
|
<MapPin className="h-4 w-4" />
|
||||||
|
<span>Map View</span>
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* View Tabs */}
|
||||||
|
<Tabs defaultValue="grid" className="mb-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
Showing <span className="font-medium text-foreground">156</span> properties
|
||||||
|
</div>
|
||||||
|
<TabsList>
|
||||||
|
<TabsTrigger value="grid" className="text-xs px-3">
|
||||||
|
Grid
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="list" className="text-xs px-3">
|
||||||
|
List
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TabsContent value="grid" className="mt-4">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
{/* Property Cards */}
|
||||||
|
<PropertyCard
|
||||||
|
id="prop1"
|
||||||
|
title="Modern Condominium"
|
||||||
|
price={15000000}
|
||||||
|
location="Sukhumvit, Bangkok"
|
||||||
|
bedrooms={3}
|
||||||
|
bathrooms={2}
|
||||||
|
area={150}
|
||||||
|
type="Condominium"
|
||||||
|
image="/placeholder.svg?height=200&width=300"
|
||||||
|
premium={true}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PropertyCard
|
||||||
|
id="prop2"
|
||||||
|
title="Luxury Villa with Pool"
|
||||||
|
price={25000000}
|
||||||
|
location="Phuket Beach, Phuket"
|
||||||
|
bedrooms={4}
|
||||||
|
bathrooms={3}
|
||||||
|
area={320}
|
||||||
|
type="House"
|
||||||
|
image="/placeholder.svg?height=200&width=300"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PropertyCard
|
||||||
|
id="prop3"
|
||||||
|
title="City Center Apartment"
|
||||||
|
price={8500000}
|
||||||
|
location="Silom, Bangkok"
|
||||||
|
bedrooms={2}
|
||||||
|
bathrooms={1}
|
||||||
|
area={85}
|
||||||
|
type="Condominium"
|
||||||
|
image="/placeholder.svg?height=200&width=300"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PropertyCard
|
||||||
|
id="prop4"
|
||||||
|
title="Riverside Townhouse"
|
||||||
|
price={12000000}
|
||||||
|
location="Chao Phraya, Bangkok"
|
||||||
|
bedrooms={3}
|
||||||
|
bathrooms={3}
|
||||||
|
area={180}
|
||||||
|
type="Townhouse"
|
||||||
|
image="/placeholder.svg?height=200&width=300"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PropertyCard
|
||||||
|
id="prop5"
|
||||||
|
title="Mountain View Villa"
|
||||||
|
price={18000000}
|
||||||
|
location="Doi Suthep, Chiang Mai"
|
||||||
|
bedrooms={3}
|
||||||
|
bathrooms={2}
|
||||||
|
area={250}
|
||||||
|
type="House"
|
||||||
|
image="/placeholder.svg?height=200&width=300"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PropertyCard
|
||||||
|
id="prop6"
|
||||||
|
title="Beachfront Condo"
|
||||||
|
price={9800000}
|
||||||
|
location="Jomtien, Pattaya"
|
||||||
|
bedrooms={1}
|
||||||
|
bathrooms={1}
|
||||||
|
area={65}
|
||||||
|
type="Condominium"
|
||||||
|
image="/placeholder.svg?height=200&width=300"
|
||||||
|
premium={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8 flex justify-center">
|
||||||
|
<Button variant="outline" className="gap-2">
|
||||||
|
Load More <ArrowRight className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="list" className="mt-4">
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* List View Property Cards */}
|
||||||
|
<PropertyCardList
|
||||||
|
id="prop1"
|
||||||
|
title="Modern Condominium"
|
||||||
|
price={15000000}
|
||||||
|
location="Sukhumvit, Bangkok"
|
||||||
|
bedrooms={3}
|
||||||
|
bathrooms={2}
|
||||||
|
area={150}
|
||||||
|
type="Condominium"
|
||||||
|
image="/placeholder.svg?height=120&width=180"
|
||||||
|
premium={true}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PropertyCardList
|
||||||
|
id="prop2"
|
||||||
|
title="Luxury Villa with Pool"
|
||||||
|
price={25000000}
|
||||||
|
location="Phuket Beach, Phuket"
|
||||||
|
bedrooms={4}
|
||||||
|
bathrooms={3}
|
||||||
|
area={320}
|
||||||
|
type="House"
|
||||||
|
image="/placeholder.svg?height=120&width=180"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PropertyCardList
|
||||||
|
id="prop3"
|
||||||
|
title="City Center Apartment"
|
||||||
|
price={8500000}
|
||||||
|
location="Silom, Bangkok"
|
||||||
|
bedrooms={2}
|
||||||
|
bathrooms={1}
|
||||||
|
area={85}
|
||||||
|
type="Condominium"
|
||||||
|
image="/placeholder.svg?height=120&width=180"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PropertyCardList
|
||||||
|
id="prop4"
|
||||||
|
title="Riverside Townhouse"
|
||||||
|
price={12000000}
|
||||||
|
location="Chao Phraya, Bangkok"
|
||||||
|
bedrooms={3}
|
||||||
|
bathrooms={3}
|
||||||
|
area={180}
|
||||||
|
type="Townhouse"
|
||||||
|
image="/placeholder.svg?height=120&width=180"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8 flex justify-center">
|
||||||
|
<Button variant="outline" className="gap-2">
|
||||||
|
Load More <ArrowRight className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PropertyCardProps {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
price: number
|
||||||
|
location: string
|
||||||
|
bedrooms: number
|
||||||
|
bathrooms: number
|
||||||
|
area: number
|
||||||
|
type: string
|
||||||
|
image: string
|
||||||
|
premium?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
function PropertyCard({
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
price,
|
||||||
|
location,
|
||||||
|
bedrooms,
|
||||||
|
bathrooms,
|
||||||
|
area,
|
||||||
|
type,
|
||||||
|
image,
|
||||||
|
premium,
|
||||||
|
}: PropertyCardProps) {
|
||||||
|
return (
|
||||||
|
<Link href={`/properties/${id}`}>
|
||||||
|
<div className={`property-card ${premium ? "property-card-premium" : ""} h-full flex flex-col`}>
|
||||||
|
<div className="relative">
|
||||||
|
<img src={image || "/placeholder.svg"} alt={title} className="w-full h-48 object-cover" />
|
||||||
|
<div className="absolute top-2 left-2 flex gap-1">
|
||||||
|
<Badge className="bg-primary">{type}</Badge>
|
||||||
|
{premium && (
|
||||||
|
<Badge className="bg-amber-500">
|
||||||
|
<Star className="h-3 w-3 mr-1" /> Premium
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 flex-1 flex flex-col">
|
||||||
|
<h3 className="font-medium text-lg mb-1">{title}</h3>
|
||||||
|
<div className="flex items-center text-muted-foreground text-sm mb-2">
|
||||||
|
<MapPin className="h-3.5 w-3.5 mr-1 flex-shrink-0" />
|
||||||
|
<span className="truncate">{location}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3 text-sm mb-3">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<BedDouble className="h-4 w-4 mr-1 text-primary" />
|
||||||
|
<span>{bedrooms}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Bath className="h-4 w-4 mr-1 text-primary" />
|
||||||
|
<span>{bathrooms}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Home className="h-4 w-4 mr-1 text-primary" />
|
||||||
|
<span>{area} m²</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-auto pt-2 border-t flex items-center justify-between">
|
||||||
|
<div className="font-semibold text-lg">฿{price.toLocaleString()}</div>
|
||||||
|
<Button size="sm" className="h-8">
|
||||||
|
View Details
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function PropertyCardList({
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
price,
|
||||||
|
location,
|
||||||
|
bedrooms,
|
||||||
|
bathrooms,
|
||||||
|
area,
|
||||||
|
type,
|
||||||
|
image,
|
||||||
|
premium,
|
||||||
|
}: PropertyCardProps) {
|
||||||
|
return (
|
||||||
|
<Link href={`/properties/${id}`}>
|
||||||
|
<div className={`property-card ${premium ? "property-card-premium" : ""}`}>
|
||||||
|
<div className="flex flex-col sm:flex-row">
|
||||||
|
<div className="relative sm:w-48 flex-shrink-0">
|
||||||
|
<img src={image || "/placeholder.svg"} alt={title} className="w-full h-48 sm:h-full object-cover" />
|
||||||
|
<div className="absolute top-2 left-2 flex gap-1">
|
||||||
|
<Badge className="bg-primary">{type}</Badge>
|
||||||
|
{premium && (
|
||||||
|
<Badge className="bg-amber-500">
|
||||||
|
<Star className="h-3 w-3 mr-1" /> Premium
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 flex-1 flex flex-col">
|
||||||
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 mb-2">
|
||||||
|
<h3 className="font-medium text-lg">{title}</h3>
|
||||||
|
<div className="font-semibold text-lg">฿{price.toLocaleString()}</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center text-muted-foreground text-sm mb-3">
|
||||||
|
<MapPin className="h-3.5 w-3.5 mr-1 flex-shrink-0" />
|
||||||
|
<span>{location}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-4 text-sm mb-4">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<BedDouble className="h-4 w-4 mr-1 text-primary" />
|
||||||
|
<span>{bedrooms} Beds</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Bath className="h-4 w-4 mr-1 text-primary" />
|
||||||
|
<span>{bathrooms} Baths</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Home className="h-4 w-4 mr-1 text-primary" />
|
||||||
|
<span>{area} m²</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-auto flex justify-end">
|
||||||
|
<Button size="sm">View Details</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@ -1,101 +1,9 @@
|
|||||||
@import 'tailwindcss' prefix(tw);
|
@import "tailwindcss";
|
||||||
|
|
||||||
@plugin "tailwindcss-animate";
|
|
||||||
|
|
||||||
@plugin 'tailwindcss-animate';
|
@plugin 'tailwindcss-animate';
|
||||||
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
|
||||||
|
|
||||||
@theme {
|
|
||||||
--font-sans:
|
|
||||||
var(--font-sans), ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji',
|
|
||||||
'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
|
||||||
|
|
||||||
--color-border: hsl(var(--border));
|
|
||||||
--color-input: hsl(var(--input));
|
|
||||||
--color-ring: hsl(var(--ring));
|
|
||||||
--color-background: hsl(var(--background));
|
|
||||||
--color-foreground: hsl(var(--foreground));
|
|
||||||
|
|
||||||
--color-primary: hsl(var(--primary));
|
|
||||||
--color-primary-foreground: hsl(var(--primary-foreground));
|
|
||||||
|
|
||||||
--color-secondary: hsl(var(--secondary));
|
|
||||||
--color-secondary-foreground: hsl(var(--secondary-foreground));
|
|
||||||
|
|
||||||
--color-destructive: hsl(var(--destructive));
|
|
||||||
--color-destructive-foreground: hsl(var(--destructive-foreground));
|
|
||||||
|
|
||||||
--color-muted: hsl(var(--muted));
|
|
||||||
--color-muted-foreground: hsl(var(--muted-foreground));
|
|
||||||
|
|
||||||
--color-accent: hsl(var(--accent));
|
|
||||||
--color-accent-foreground: hsl(var(--accent-foreground));
|
|
||||||
|
|
||||||
--color-popover: hsl(var(--popover));
|
|
||||||
--color-popover-foreground: hsl(var(--popover-foreground));
|
|
||||||
|
|
||||||
--color-card: hsl(var(--card));
|
|
||||||
--color-card-foreground: hsl(var(--card-foreground));
|
|
||||||
|
|
||||||
--color-sidebar: hsl(var(--sidebar-background));
|
|
||||||
--color-sidebar-foreground: hsl(var(--sidebar-foreground));
|
|
||||||
--color-sidebar-border: hsl(var(--sidebar-border));
|
|
||||||
--color-sidebar-ring: hsl(var(--sidebar-ring));
|
|
||||||
|
|
||||||
--color-sidebar-accent: hsl(var(--sidebar-accent));
|
|
||||||
--color-sidebar-accent-foreground: hsl(var(--sidebar-accent-foreground));
|
|
||||||
|
|
||||||
--radius-lg: var(--radius);
|
|
||||||
--radius-md: calc(var(--radius) - 2px);
|
|
||||||
--radius-sm: calc(var(--radius) - 4px);
|
|
||||||
|
|
||||||
--animate-accordion-down: accordion-down 0.2s ease-out;
|
|
||||||
--animate-accordion-up: accordion-up 0.2s ease-out;
|
|
||||||
--animate-caret-blink: caret-blink 1s ease-in-out infinite;
|
|
||||||
|
|
||||||
@keyframes accordion-down {
|
|
||||||
from {
|
|
||||||
height: 0;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
height: var(--radix-accordion-content-height);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@keyframes accordion-up {
|
|
||||||
from {
|
|
||||||
height: var(--radix-accordion-content-height);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
height: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@keyframes caret-blink {
|
|
||||||
0%,
|
|
||||||
50%,
|
|
||||||
100% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
25%,
|
|
||||||
75% {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@utility container {
|
|
||||||
margin-inline: auto;
|
|
||||||
padding-inline: 2rem;
|
|
||||||
@media (width >= --theme(--breakpoint-sm)) {
|
|
||||||
max-width: none;
|
|
||||||
}
|
|
||||||
@media (width >= 1400px) {
|
|
||||||
max-width: 1400px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
--color-background: hsl(var(--background));
|
--color-background: hsl(var(--background));
|
||||||
--color-foreground: hsl(var(--foreground));
|
--color-foreground: hsl(var(--foreground));
|
||||||
@ -165,24 +73,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
The default border color has changed to `currentcolor` in Tailwind CSS v4,
|
|
||||||
so we've added these compatibility styles to make sure everything still
|
|
||||||
looks the same as it did with Tailwind CSS v3.
|
|
||||||
|
|
||||||
If we ever want to remove these styles, we need to add an explicit border
|
|
||||||
color utility to any element that depends on these defaults.
|
|
||||||
*/
|
|
||||||
@layer base {
|
|
||||||
*,
|
|
||||||
::after,
|
|
||||||
::before,
|
|
||||||
::backdrop,
|
|
||||||
::file-selector-button {
|
|
||||||
border-color: var(--color-gray-200, currentcolor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
The default border color has changed to `currentColor` in Tailwind CSS v4,
|
The default border color has changed to `currentColor` in Tailwind CSS v4,
|
||||||
so we've added these compatibility styles to make sure everything still
|
so we've added these compatibility styles to make sure everything still
|
||||||
|
|||||||
@ -1,22 +1,19 @@
|
|||||||
/*
|
import type React from "react";
|
||||||
========================================
|
|
||||||
File: frontend/app/layout.tsx
|
|
||||||
========================================
|
|
||||||
*/
|
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Inter } from "next/font/google"; // Example using Inter font
|
import { Poppins } from "next/font/google";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import { ThemeProvider } from "@/components/common/ThemeProvider"; // Correct path
|
import { ThemeProvider } from "@/components/theme-provider";
|
||||||
import { ThemeController } from "@/components/common/ThemeController"; // Correct path
|
import Sidebar from "@/components/sidebar";
|
||||||
import { Toaster as SonnerToaster } from "@/components/ui/sonner"; // Sonner for notifications
|
|
||||||
import { Toaster as RadixToaster } from "@/components/ui/toaster"; // Shadcn Toaster (if using useToast hook)
|
|
||||||
|
|
||||||
// Setup font
|
const poppins = Poppins({
|
||||||
const inter = Inter({ subsets: ["latin"], variable: "--font-sans" }); // Define CSS variable
|
subsets: ["latin"],
|
||||||
|
weight: ["300", "400", "500", "600", "700"],
|
||||||
|
variable: "--font-poppins",
|
||||||
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Borbann - Data Platform", // More specific title
|
title: "BorBann - Property Analytics",
|
||||||
description: "Data integration, analysis, and visualization platform.",
|
description: "Automated data integration pipeline and property analytics for real estate",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
@ -26,19 +23,12 @@ export default function RootLayout({
|
|||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en" suppressHydrationWarning>
|
<html lang="en" suppressHydrationWarning>
|
||||||
{" "}
|
<body className={poppins.className}>
|
||||||
{/* suppressHydrationWarning needed for next-themes */}
|
|
||||||
<body className={`${inter.variable} font-sans`}>
|
|
||||||
{" "}
|
|
||||||
{/* Apply font class */}
|
|
||||||
{/* ThemeProvider should wrap everything for theme context */}
|
|
||||||
<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
|
<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
|
||||||
{/* ThemeController can wrap specific parts or the whole app */}
|
<div className="flex h-screen">
|
||||||
{/* If placed here, it controls the base theme and color scheme */}
|
<Sidebar />
|
||||||
<ThemeController defaultColorScheme="Blue">{children}</ThemeController>
|
<div className="flex-1 overflow-auto">{children}</div>
|
||||||
{/* Include Toaster components for notifications */}
|
</div>
|
||||||
<SonnerToaster />
|
|
||||||
<RadixToaster /> {/* Include if using the useToast hook */}
|
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
40
frontend/app/loading.tsx
Normal file
40
frontend/app/loading.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
/* === src/app/loading.tsx === */
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
|
||||||
|
// A more detailed full-page loading skeleton mimicking the layout
|
||||||
|
export default function Loading() {
|
||||||
|
return (
|
||||||
|
<div className="flex-1 flex flex-col p-6 space-y-6">
|
||||||
|
{/* Page Header Skeleton */}
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Skeleton className="h-4 w-48" />
|
||||||
|
<Skeleton className="h-8 w-64" />
|
||||||
|
<Skeleton className="h-4 w-80" />
|
||||||
|
</div>
|
||||||
|
<Skeleton className="h-9 w-9 rounded-full" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content Area Skeleton - Adjust based on typical page content */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
{/* Left Column / Filters Skeleton (Example) */}
|
||||||
|
<div className="md:col-span-1 space-y-4">
|
||||||
|
<Skeleton className="h-64 w-full rounded-lg" />
|
||||||
|
<Skeleton className="h-48 w-full rounded-lg" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Column / Main Content Skeleton (Example) */}
|
||||||
|
<div className="md:col-span-2 space-y-4">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<Skeleton className="h-6 w-1/3" />
|
||||||
|
<Skeleton className="h-9 w-24" />
|
||||||
|
</div>
|
||||||
|
<Skeleton className="h-96 w-full rounded-lg" />
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<Skeleton className="h-10 w-32" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,36 +1,204 @@
|
|||||||
/*
|
import { Button } from "@/components/ui/button";
|
||||||
========================================
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
File: frontend/app/page.tsx
|
import { Badge } from "@/components/ui/badge";
|
||||||
========================================
|
import { ArrowRight, BarChart2, Building, Database, LineChart, MapPin, Zap } from "lucide-react";
|
||||||
*/
|
import Link from "next/link";
|
||||||
import Image from "next/image";
|
|
||||||
import Link from "next/link"; // Import Link
|
|
||||||
import { Button } from "@/components/ui/button"; // Import common UI
|
|
||||||
|
|
||||||
export default function Home() {
|
export default function LandingPage() {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col min-h-screen items-center justify-center p-8 sm:p-20 text-center">
|
<div className="min-h-screen">
|
||||||
{/* Replace with your actual logo/branding */}
|
{/* Hero Section */}
|
||||||
<h1 className="text-4xl font-bold mb-4">Welcome to Borbann</h1>
|
<section className="bg-gradient-to-b from-background to-muted py-20">
|
||||||
<p className="text-lg text-muted-foreground mb-8">Your data integration and analysis platform.</p>
|
<div className="container mx-auto px-6 text-center">
|
||||||
|
<Badge className="mb-4 bg-primary/10 text-primary hover:bg-primary/20 border-primary/20">
|
||||||
<div className="flex flex-col sm:flex-row gap-4 items-center">
|
Property Analytics Platform
|
||||||
<Link href="/map">
|
</Badge>
|
||||||
<Button size="lg">Go to Map</Button>
|
<h1 className="text-4xl md:text-5xl font-bold mb-6">Data-Driven Property Intelligence</h1>
|
||||||
|
<p className="text-xl text-muted-foreground max-w-3xl mx-auto mb-10">
|
||||||
|
Leverage AI and machine learning to analyze property data, predict prices, and gain valuable insights for
|
||||||
|
better decision making.
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||||
|
<Button size="lg" asChild>
|
||||||
|
<Link href="/maps">
|
||||||
|
Explore Map <ArrowRight className="ml-2 h-4 w-4" />
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="/documentation">
|
|
||||||
{" "}
|
|
||||||
{/* Example link */}
|
|
||||||
<Button size="lg" variant="outline">
|
|
||||||
Read Docs
|
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
<Button size="lg" variant="outline" asChild>
|
||||||
|
<Link href="/data-pipeline">Manage Data Pipelines</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Features Section */}
|
||||||
|
<section className="py-20">
|
||||||
|
<div className="container mx-auto px-6">
|
||||||
|
<div className="text-center mb-16">
|
||||||
|
<h2 className="text-3xl font-bold mb-4">Powerful Analytics Features</h2>
|
||||||
|
<p className="text-muted-foreground max-w-2xl mx-auto">
|
||||||
|
Our platform provides comprehensive tools to analyze property data from multiple angles
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Optional: Add more introductory content or links */}
|
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||||
<footer className="absolute bottom-8 text-sm text-muted-foreground">
|
<Card>
|
||||||
© {new Date().getFullYear()} Borbann Project.
|
<CardHeader>
|
||||||
</footer>
|
<div className="h-12 w-12 rounded-lg bg-primary/10 flex items-center justify-center mb-4">
|
||||||
|
<MapPin className="h-6 w-6 text-primary" />
|
||||||
|
</div>
|
||||||
|
<CardTitle>Geospatial Visualization</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Interactive maps with property data overlays and environmental factors
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Visualize properties on an interactive map with customizable overlays for flood risk, air quality, and
|
||||||
|
more. Analyze properties within a specific radius.
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter>
|
||||||
|
<Button variant="ghost" className="gap-1" asChild>
|
||||||
|
<Link href="/maps">
|
||||||
|
Explore Maps <ArrowRight className="h-4 w-4" />
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="h-12 w-12 rounded-lg bg-primary/10 flex items-center justify-center mb-4">
|
||||||
|
<BarChart2 className="h-6 w-6 text-primary" />
|
||||||
|
</div>
|
||||||
|
<CardTitle>Price Prediction</CardTitle>
|
||||||
|
<CardDescription>AI-powered price prediction with explainable features</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Understand how different factors affect property prices with our explainable AI models. Adjust
|
||||||
|
parameters to see how they impact predictions.
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter>
|
||||||
|
<Button variant="ghost" className="gap-1" asChild>
|
||||||
|
<Link href="/price-prediction">
|
||||||
|
View Predictions <ArrowRight className="h-4 w-4" />
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="h-12 w-12 rounded-lg bg-primary/10 flex items-center justify-center mb-4">
|
||||||
|
<Database className="h-6 w-6 text-primary" />
|
||||||
|
</div>
|
||||||
|
<CardTitle>Data Pipelines</CardTitle>
|
||||||
|
<CardDescription>Automated data collection and processing</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Set up automated data collection from multiple sources. Our AI-powered pipelines clean, normalize, and
|
||||||
|
prepare data for analysis.
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter>
|
||||||
|
<Button variant="ghost" className="gap-1" asChild>
|
||||||
|
<Link href="/data-pipeline">
|
||||||
|
Manage Pipelines <ArrowRight className="h-4 w-4" />
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="h-12 w-12 rounded-lg bg-primary/10 flex items-center justify-center mb-4">
|
||||||
|
<Building className="h-6 w-6 text-primary" />
|
||||||
|
</div>
|
||||||
|
<CardTitle>Property Analytics</CardTitle>
|
||||||
|
<CardDescription>Comprehensive property data analysis</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Analyze property details, historical price trends, and environmental factors. Generate reports and
|
||||||
|
export data for further analysis.
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter>
|
||||||
|
<Button variant="ghost" className="gap-1" asChild>
|
||||||
|
<Link href="/properties">
|
||||||
|
Browse Properties <ArrowRight className="h-4 w-4" />
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="h-12 w-12 rounded-lg bg-primary/10 flex items-center justify-center mb-4">
|
||||||
|
<LineChart className="h-6 w-6 text-primary" />
|
||||||
|
</div>
|
||||||
|
<CardTitle>Market Trends</CardTitle>
|
||||||
|
<CardDescription>Real-time market analysis and trends</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Track property market trends over time. Identify emerging patterns and make data-driven investment
|
||||||
|
decisions.
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter>
|
||||||
|
<Button variant="ghost" className="gap-1" asChild>
|
||||||
|
<Link href="/maps">
|
||||||
|
View Trends <ArrowRight className="h-4 w-4" />
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="h-12 w-12 rounded-lg bg-primary/10 flex items-center justify-center mb-4">
|
||||||
|
<Zap className="h-6 w-6 text-primary" />
|
||||||
|
</div>
|
||||||
|
<CardTitle>Custom ML Models</CardTitle>
|
||||||
|
<CardDescription>Train and deploy custom machine learning models</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Create and train custom machine learning models using your own data. Deploy models for specific
|
||||||
|
analysis needs.
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter>
|
||||||
|
<Button variant="ghost" className="gap-1" asChild>
|
||||||
|
<Link href="/models">
|
||||||
|
Manage Models <ArrowRight className="h-4 w-4" />
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* CTA Section */}
|
||||||
|
<section className="py-20 bg-muted">
|
||||||
|
<div className="container mx-auto px-6 text-center">
|
||||||
|
<h2 className="text-3xl font-bold mb-4">Ready to Get Started?</h2>
|
||||||
|
<p className="text-muted-foreground max-w-2xl mx-auto mb-8">
|
||||||
|
Explore our platform and discover how data-driven property analytics can transform your decision making.
|
||||||
|
</p>
|
||||||
|
<Button size="lg" asChild>
|
||||||
|
<Link href="/maps">
|
||||||
|
Start Exploring <ArrowRight className="ml-2 h-4 w-4" />
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,24 +1,26 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://ui.shadcn.com/schema.json",
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
"style": "new-york",
|
"style": "default",
|
||||||
"rsc": true,
|
"rsc": true,
|
||||||
"tsx": true,
|
"tsx": true,
|
||||||
"tailwind": {
|
"tailwind": {
|
||||||
"config": "tailwind.config.ts",
|
"config": "tailwind.config.ts",
|
||||||
"css": "app/globals.css",
|
"css": "src/styles/globals.css",
|
||||||
"baseColor": "neutral",
|
"baseColor": "neutral",
|
||||||
"cssVariables": true,
|
"cssVariables": true,
|
||||||
"prefix": ""
|
"prefix": ""
|
||||||
},
|
},
|
||||||
"aliases": {
|
"aliases": {
|
||||||
|
"@": "@/src",
|
||||||
"components": "@/components",
|
"components": "@/components",
|
||||||
"utils": "@/lib/utils",
|
"utils": "@/lib/utils",
|
||||||
"ui": "@/components/ui",
|
"ui": "@/components/ui",
|
||||||
"lib": "@/lib",
|
"lib": "@/lib",
|
||||||
"hooks": "@/hooks",
|
"hooks": "@/hooks",
|
||||||
"features": "@/features",
|
"features": "@/features",
|
||||||
"types": "@/types",
|
"services": "@/services",
|
||||||
"services": "@/services"
|
"store": "@/store",
|
||||||
|
"types": "@/types"
|
||||||
},
|
},
|
||||||
"iconLibrary": "lucide"
|
"iconLibrary": "lucide"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,29 +0,0 @@
|
|||||||
/*
|
|
||||||
========================================
|
|
||||||
File: frontend/components/common/PageLayout.tsx (NEW - Example)
|
|
||||||
========================================
|
|
||||||
*/
|
|
||||||
import React, { ReactNode } from "react";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
// Example: Assuming you might have a common Header/Footer/Sidebar structure
|
|
||||||
// import { AppHeader } from './AppHeader';
|
|
||||||
// import { AppFooter } from './AppFooter';
|
|
||||||
|
|
||||||
interface PageLayoutProps {
|
|
||||||
children: ReactNode;
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DUMMY: A basic layout component for pages.
|
|
||||||
* Might include common headers, footers, or side navigation structures.
|
|
||||||
*/
|
|
||||||
export function PageLayout({ children, className }: PageLayoutProps) {
|
|
||||||
return (
|
|
||||||
<div className={cn("flex flex-col min-h-screen", className)}>
|
|
||||||
{/* <AppHeader /> */} {/* Example: Shared Header */}
|
|
||||||
<main className="flex-1">{children}</main>
|
|
||||||
{/* <AppFooter /> */} {/* Example: Shared Footer */}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,144 +0,0 @@
|
|||||||
/*
|
|
||||||
========================================
|
|
||||||
File: frontend/components/common/ThemeController.tsx
|
|
||||||
========================================
|
|
||||||
*/
|
|
||||||
"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 (these affect CSS variables)
|
|
||||||
const colorSchemes = [
|
|
||||||
{ name: "Blue", primary: "221.2 83.2% 53.3%" }, // Default blue
|
|
||||||
{ 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);
|
|
||||||
// State for overlay boundaries removed, as overlay context now manages positioning/constraints.
|
|
||||||
// const [overlayBoundaries, setOverlayBoundaries] = useState({ width: 0, height: 0 });
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
// Update overlay boundaries - This logic might be better placed within the OverlayProvider
|
|
||||||
// or removed if not strictly necessary for the controller's function.
|
|
||||||
// Kept here for now as per original code structure, but consider moving it.
|
|
||||||
// useEffect(() => {
|
|
||||||
// const updateBoundaries = () => {
|
|
||||||
// if (containerRef.current) {
|
|
||||||
// const width = containerRef.current.clientWidth;
|
|
||||||
// const height = containerRef.current.clientHeight;
|
|
||||||
// document.documentElement.style.setProperty("--max-overlay-width", `${width - 32}px`);
|
|
||||||
// document.documentElement.style.setProperty("--max-overlay-height", `${height - 32}px`);
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
// updateBoundaries();
|
|
||||||
// window.addEventListener("resize", updateBoundaries);
|
|
||||||
// return () => window.removeEventListener("resize", updateBoundaries);
|
|
||||||
// }, []);
|
|
||||||
|
|
||||||
// Apply color scheme by setting the '--primary' CSS variable
|
|
||||||
useEffect(() => {
|
|
||||||
const scheme = colorSchemes.find((s) => s.name === colorScheme);
|
|
||||||
if (scheme) {
|
|
||||||
document.documentElement.style.setProperty("--primary", scheme.primary);
|
|
||||||
// You might need to set --ring as well if it depends on primary
|
|
||||||
// document.documentElement.style.setProperty("--ring", scheme.primary);
|
|
||||||
}
|
|
||||||
}, [colorScheme]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={containerRef} className="relative h-full w-full overflow-hidden" data-theme-controller="true">
|
|
||||||
{children}
|
|
||||||
|
|
||||||
{/* Theme Controller UI */}
|
|
||||||
<div className="fixed bottom-4 left-4 z-999 flex items-center gap-2">
|
|
||||||
{" "}
|
|
||||||
{/* Ensure high z-index */}
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="icon"
|
|
||||||
className="h-10 w-10 rounded-full bg-background/90 backdrop-blur-xs shadow-md">
|
|
||||||
<Palette className="h-5 w-5" />
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="start" className="w-56">
|
|
||||||
<DropdownMenuLabel>Theme Options</DropdownMenuLabel>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
|
|
||||||
<DropdownMenuItem onClick={() => setTheme("light")} className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Sun className="h-4 w-4" />
|
|
||||||
<span>Light</span>
|
|
||||||
</div>
|
|
||||||
{theme === "light" && <Check className="h-4 w-4" />}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
|
|
||||||
<DropdownMenuItem onClick={() => setTheme("dark")} className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Moon className="h-4 w-4" />
|
|
||||||
<span>Dark</span>
|
|
||||||
</div>
|
|
||||||
{theme === "dark" && <Check className="h-4 w-4" />}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
|
|
||||||
<DropdownMenuItem onClick={() => setTheme("system")} className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Laptop className="h-4 w-4" />
|
|
||||||
<span>System</span>
|
|
||||||
</div>
|
|
||||||
{theme === "system" && <Check className="h-4 w-4" />}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuLabel>Color Scheme</DropdownMenuLabel>
|
|
||||||
|
|
||||||
{colorSchemes.map((scheme) => (
|
|
||||||
<DropdownMenuItem
|
|
||||||
key={scheme.name}
|
|
||||||
onClick={() => setColorScheme(scheme.name)}
|
|
||||||
className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="h-4 w-4 rounded-full" style={{ backgroundColor: `hsl(${scheme.primary})` }} />
|
|
||||||
<span>{scheme.name}</span>
|
|
||||||
</div>
|
|
||||||
{colorScheme === scheme.name && <Check className="h-4 w-4" />}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
))}
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="right">
|
|
||||||
<p>Theme Settings</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
/*
|
|
||||||
========================================
|
|
||||||
File: frontend/components/common/ThemeProvider.tsx
|
|
||||||
========================================
|
|
||||||
*/
|
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
|
||||||
import type { ThemeProviderProps } from "next-themes";
|
|
||||||
|
|
||||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
|
||||||
return (
|
|
||||||
<NextThemesProvider
|
|
||||||
attribute="class"
|
|
||||||
defaultTheme="system"
|
|
||||||
enableSystem
|
|
||||||
disableTransitionOnChange
|
|
||||||
{...props} // Pass through any other props
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</NextThemesProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,57 +0,0 @@
|
|||||||
/*
|
|
||||||
========================================
|
|
||||||
File: frontend/components/common/ThemeToggle.tsx
|
|
||||||
========================================
|
|
||||||
*/
|
|
||||||
"use client";
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
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 (
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button variant="ghost" size="icon">
|
|
||||||
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
|
||||||
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
|
||||||
<span className="sr-only">Toggle theme</span>
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end">
|
|
||||||
<DropdownMenuItem onClick={() => setTheme("light")}>
|
|
||||||
<Sun className="mr-2 h-4 w-4" />
|
|
||||||
<span>Light</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={() => setTheme("dark")}>
|
|
||||||
<Moon className="mr-2 h-4 w-4" />
|
|
||||||
<span>Dark</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={() => setTheme("system")}>
|
|
||||||
<Laptop className="mr-2 h-4 w-4" />
|
|
||||||
<span>System</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>Change theme</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
28
frontend/components/mode-toggle.tsx
Normal file
28
frontend/components/mode-toggle.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
"use client"
|
||||||
|
import { Moon, Sun } from "lucide-react"
|
||||||
|
import { useTheme } from "next-themes"
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
||||||
|
|
||||||
|
export function ModeToggle() {
|
||||||
|
const { setTheme } = useTheme()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="outline" size="icon" className="h-9 w-9">
|
||||||
|
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||||
|
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||||
|
<span className="sr-only">Toggle theme</span>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem onClick={() => setTheme("light")}>Light</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => setTheme("dark")}>Dark</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => setTheme("system")}>System</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
51
frontend/components/page-header.tsx
Normal file
51
frontend/components/page-header.tsx
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { ChevronRight } from "lucide-react"
|
||||||
|
import Link from "next/link"
|
||||||
|
import { ModeToggle } from "./mode-toggle"
|
||||||
|
|
||||||
|
interface BreadcrumbItem {
|
||||||
|
title: string
|
||||||
|
href: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PageHeaderProps {
|
||||||
|
title: string
|
||||||
|
description?: string
|
||||||
|
breadcrumb?: BreadcrumbItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PageHeader({ title, description, breadcrumb = [] }: PageHeaderProps) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col space-y-2">
|
||||||
|
{breadcrumb.length > 0 && (
|
||||||
|
<nav className="flex items-center text-sm text-muted-foreground">
|
||||||
|
{breadcrumb.map((item, index) => (
|
||||||
|
<div key={item.href} className="flex items-center">
|
||||||
|
{index > 0 && <ChevronRight className="h-4 w-4 mx-1" />}
|
||||||
|
<Link href={item.href} className="hover:text-foreground transition-colors">
|
||||||
|
{item.title}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{title && (
|
||||||
|
<>
|
||||||
|
<ChevronRight className="h-4 w-4 mx-1" />
|
||||||
|
<span className="font-medium text-foreground">{title}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="ml-auto">
|
||||||
|
<ModeToggle />
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold tracking-tight">{title}</h1>
|
||||||
|
{description && <p className="text-muted-foreground">{description}</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
114
frontend/components/sidebar.tsx
Normal file
114
frontend/components/sidebar.tsx
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import type React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { Map, Database, FileText, Users, ChevronDown, ChevronUp, BrainCircuit } from "lucide-react"
|
||||||
|
import Link from "next/link"
|
||||||
|
import { usePathname } from "next/navigation"
|
||||||
|
import { useState } from "react"
|
||||||
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
|
||||||
|
interface SidebarItemProps {
|
||||||
|
icon: React.ReactNode
|
||||||
|
label: string
|
||||||
|
href: string
|
||||||
|
active?: boolean
|
||||||
|
badge?: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
const SidebarItem = ({ icon, label, href, active, badge }: SidebarItemProps) => {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
href={href}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-3 px-3 py-2 rounded-md transition-colors",
|
||||||
|
active
|
||||||
|
? "bg-primary/10 text-primary border-l-2 border-primary"
|
||||||
|
: "text-foreground/80 hover:bg-muted hover:text-foreground hover:border-l-2 hover:border-primary/50",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className={cn("text-primary", active ? "text-primary" : "text-muted-foreground")}>{icon}</div>
|
||||||
|
<span className="flex-1">{label}</span>
|
||||||
|
{badge}
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Sidebar() {
|
||||||
|
const pathname = usePathname()
|
||||||
|
const [expanded, setExpanded] = useState(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-64 border-r bg-background flex flex-col h-full">
|
||||||
|
<div className="p-4 border-b flex items-center gap-2">
|
||||||
|
<div className="h-8 w-8 rounded-full bg-primary flex items-center justify-center text-primary-foreground font-bold">
|
||||||
|
B
|
||||||
|
</div>
|
||||||
|
<span className="font-semibold text-xl">BorBann</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 py-4 px-3 space-y-1 overflow-y-auto">
|
||||||
|
<SidebarItem icon={<Map size={20} />} label="Maps" href="/maps" active={pathname.startsWith("/maps")} />
|
||||||
|
|
||||||
|
<SidebarItem
|
||||||
|
icon={<Database size={20} />}
|
||||||
|
label="Data Pipeline"
|
||||||
|
href="/data-pipeline"
|
||||||
|
active={pathname.startsWith("/data-pipeline")}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SidebarItem
|
||||||
|
icon={<BrainCircuit size={20} />}
|
||||||
|
label="Models"
|
||||||
|
href="/models"
|
||||||
|
active={pathname.startsWith("/models")}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SidebarItem
|
||||||
|
icon={<FileText size={20} />}
|
||||||
|
label="Documentation"
|
||||||
|
href="/documentation"
|
||||||
|
active={pathname.startsWith("/documentation")}
|
||||||
|
badge={<span className="text-xs bg-primary text-primary-foreground px-1.5 py-0.5 rounded-md">NEW</span>}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-auto border-t p-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Avatar>
|
||||||
|
<AvatarImage src="/placeholder.svg?height=40&width=40" />
|
||||||
|
<AvatarFallback>GG</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<div className="flex-1 overflow-hidden">
|
||||||
|
<p className="font-medium truncate">GG_WPX</p>
|
||||||
|
<p className="text-xs text-muted-foreground truncate">garfield.wpx@gmail.com</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="w-full mt-4 justify-start gap-2 border-primary/20 hover:border-primary"
|
||||||
|
onClick={() => setExpanded(!expanded)}
|
||||||
|
>
|
||||||
|
<Users size={18} />
|
||||||
|
<span className="flex-1 text-left">Users</span>
|
||||||
|
{expanded ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{expanded && (
|
||||||
|
<div className="mt-2 pl-2 space-y-1 text-sm">
|
||||||
|
<Link href="/users/manage" className="block py-1 px-2 rounded hover:bg-muted">
|
||||||
|
Manage Users
|
||||||
|
</Link>
|
||||||
|
<Link href="/users/roles" className="block py-1 px-2 rounded hover:bg-muted">
|
||||||
|
Roles & Permissions
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
11
frontend/components/theme-provider.tsx
Normal file
11
frontend/components/theme-provider.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import * as React from 'react'
|
||||||
|
import {
|
||||||
|
ThemeProvider as NextThemesProvider,
|
||||||
|
type ThemeProviderProps,
|
||||||
|
} from 'next-themes'
|
||||||
|
|
||||||
|
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||||
|
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
|
||||||
|
}
|
||||||
@ -1,65 +0,0 @@
|
|||||||
/*
|
|
||||||
========================================
|
|
||||||
File: frontend/features/map/api/mapApi.ts
|
|
||||||
========================================
|
|
||||||
*/
|
|
||||||
import apiClient from "@/services/apiClient";
|
|
||||||
import type { APIResponse, PointOfInterest } from "@/types/api"; // Shared types
|
|
||||||
import type { MapBounds } from "../types"; // Feature-specific types
|
|
||||||
|
|
||||||
interface FetchPOIsParams {
|
|
||||||
bounds: MapBounds;
|
|
||||||
filters?: Record<string, any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DUMMY: Fetches Points of Interest based on map bounds and filters.
|
|
||||||
*/
|
|
||||||
export async function fetchPointsOfInterest(
|
|
||||||
params: FetchPOIsParams
|
|
||||||
): Promise<APIResponse<PointOfInterest[]>> {
|
|
||||||
console.log("DUMMY API: Fetching POIs with params:", params);
|
|
||||||
|
|
||||||
// Simulate building query parameters
|
|
||||||
const queryParams = new URLSearchParams({
|
|
||||||
north: params.bounds.north.toString(),
|
|
||||||
south: params.bounds.south.toString(),
|
|
||||||
east: params.bounds.east.toString(),
|
|
||||||
west: params.bounds.west.toString(),
|
|
||||||
});
|
|
||||||
if (params.filters) {
|
|
||||||
Object.entries(params.filters).forEach(([key, value]) => {
|
|
||||||
if (value !== undefined && value !== null) {
|
|
||||||
queryParams.set(key, String(value));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use the dummy apiClient
|
|
||||||
const response = await apiClient.get<PointOfInterest[]>(`/map/pois?${queryParams.toString()}`);
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
// Simulate adding more data if needed for testing
|
|
||||||
const dummyData: PointOfInterest[] = [
|
|
||||||
{ id: "poi1", lat: params.bounds.north - 0.01, lng: params.bounds.west + 0.01, name: "Dummy Cafe", type: "cafe" },
|
|
||||||
{ id: "poi2", lat: params.bounds.south + 0.01, lng: params.bounds.east - 0.01, name: "Dummy Park", type: "park" },
|
|
||||||
...(response.data || []) // Include data if apiClient simulation provides it
|
|
||||||
];
|
|
||||||
return { success: true, data: dummyData };
|
|
||||||
} else {
|
|
||||||
return response; // Forward the error response
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add other map-related API functions here
|
|
||||||
export async function fetchMapAnalytics(bounds: MapBounds): Promise<APIResponse<any>> {
|
|
||||||
console.log("DUMMY API: Fetching Map Analytics with params:", bounds);
|
|
||||||
// Simulate building query parameters
|
|
||||||
const queryParams = new URLSearchParams({
|
|
||||||
north: bounds.north.toString(),
|
|
||||||
south: bounds.south.toString(),
|
|
||||||
east: bounds.east.toString(),
|
|
||||||
west: bounds.west.toString(),
|
|
||||||
});
|
|
||||||
return apiClient.get(`/map/analytics?${queryParams.toString()}`);
|
|
||||||
}
|
|
||||||
@ -1,120 +0,0 @@
|
|||||||
/*
|
|
||||||
========================================
|
|
||||||
File: frontend/features/map/components/analytics-overlay.tsx
|
|
||||||
========================================
|
|
||||||
*/
|
|
||||||
"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 local overlay system
|
|
||||||
import { useOverlay } from "./overlay-system/overlay-context";
|
|
||||||
|
|
||||||
export function AnalyticsOverlay() {
|
|
||||||
const { toggleOverlay } = useOverlay();
|
|
||||||
|
|
||||||
const handleChatClick = () => {
|
|
||||||
toggleOverlay("chat");
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Overlay
|
|
||||||
id="analytics"
|
|
||||||
title="Analytics"
|
|
||||||
icon={<Sparkles className="h-5 w-5" />}
|
|
||||||
initialPosition="top-right"
|
|
||||||
initialIsOpen={true}
|
|
||||||
width="350px">
|
|
||||||
<div className="h-[calc(min(70vh,600px))] overflow-auto">
|
|
||||||
<div className="p-4">
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<p className="text-xs text-muted-foreground">Information in radius will be analyzed</p>
|
|
||||||
|
|
||||||
{/* Area Price History Card */}
|
|
||||||
<Card className="bg-card/50 border border-border/50 shadow-xs">
|
|
||||||
<CardHeader className="pb-2">
|
|
||||||
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
|
||||||
<LineChart className="h-4 w-4 text-primary" />
|
|
||||||
Area Price History
|
|
||||||
</CardTitle>
|
|
||||||
<div className="flex items-baseline justify-between">
|
|
||||||
<div className="text-2xl font-bold">10,000,000 Baht</div>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-muted-foreground">Overall Price History of this area</p>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="pt-0">
|
|
||||||
<AreaChart
|
|
||||||
data={[8500000, 9000000, 8800000, 9200000, 9500000, 9800000, 10000000]}
|
|
||||||
color="rgba(59, 130, 246, 0.5)"
|
|
||||||
/>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Price Prediction Card */}
|
|
||||||
<Card className="bg-card/50 border border-border/50 shadow-xs">
|
|
||||||
<CardHeader className="pb-2">
|
|
||||||
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
|
||||||
<LineChart className="h-4 w-4 text-primary" />
|
|
||||||
Price Prediction
|
|
||||||
</CardTitle>
|
|
||||||
<div className="flex items-baseline justify-between">
|
|
||||||
<div className="text-2xl font-bold">15,000,000 Baht</div>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-muted-foreground">The estimated price based on various factors.</p>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="pt-0">
|
|
||||||
<AreaChart
|
|
||||||
data={[10000000, 11000000, 12000000, 13000000, 14000000, 14500000, 15000000]}
|
|
||||||
color="rgba(16, 185, 129, 0.5)"
|
|
||||||
/>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Environmental Factors Cards */}
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<Card className="bg-card/50 border border-border/50 shadow-xs">
|
|
||||||
<CardHeader className="p-4">
|
|
||||||
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
|
||||||
<Droplets className="h-4 w-4 text-blue-500" />
|
|
||||||
Flood Factor
|
|
||||||
</CardTitle>
|
|
||||||
<div className="flex items-center gap-2 mt-1">
|
|
||||||
<div className="h-3 w-3 rounded-full bg-yellow-500"></div>
|
|
||||||
<span className="text-sm">Moderate</span>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="bg-card/50 border border-border/50 shadow-xs">
|
|
||||||
<CardHeader className="p-4">
|
|
||||||
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
|
||||||
<Wind className="h-4 w-4 text-purple-500" />
|
|
||||||
Air Factor
|
|
||||||
</CardTitle>
|
|
||||||
<div className="flex items-center gap-2 mt-1">
|
|
||||||
<div className="h-3 w-3 rounded-full bg-red-500"></div>
|
|
||||||
<span className="text-sm">Bad</span>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Chat With AI Card */}
|
|
||||||
<Card
|
|
||||||
className="bg-card/50 border border-border/50 shadow-xs cursor-pointer hover:bg-muted/50 transition-colors"
|
|
||||||
onClick={handleChatClick}>
|
|
||||||
<CardHeader className="p-4">
|
|
||||||
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
|
||||||
<Bot className="h-4 w-4 text-teal-500" />
|
|
||||||
Chat With AI
|
|
||||||
</CardTitle>
|
|
||||||
<p className="text-xs text-muted-foreground">Want to ask specific question?</p>
|
|
||||||
</CardHeader>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Overlay>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
/*
|
|
||||||
========================================
|
|
||||||
File: frontend/features/map/components/analytics-panel.tsx
|
|
||||||
========================================
|
|
||||||
*/
|
|
||||||
// This component seems redundant with analytics-overlay.tsx in the new structure.
|
|
||||||
// If it has unique logic or display variation, keep it and refactor its imports.
|
|
||||||
// Otherwise, it can likely be removed, and its functionality merged into analytics-overlay.tsx.
|
|
||||||
// For this rewrite, assuming it's removed or its distinct logic is integrated elsewhere.
|
|
||||||
// If needed, its structure would be similar to analytics-overlay.tsx but maybe without the <Overlay> wrapper.
|
|
||||||
@ -1,73 +0,0 @@
|
|||||||
/*
|
|
||||||
========================================
|
|
||||||
File: frontend/features/map/components/area-chart.tsx
|
|
||||||
========================================
|
|
||||||
*/
|
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { LineChart, Line, ResponsiveContainer, XAxis, YAxis, Tooltip } from "@/components/ui/chart"; // Using shared ui chart components
|
|
||||||
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 (e.g., months or simple indices)
|
|
||||||
const labels = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul"]; // Example labels
|
|
||||||
|
|
||||||
// Format the data for the chart
|
|
||||||
const chartData = data.map((value, index) => ({
|
|
||||||
name: labels[index % labels.length] || `Point ${index + 1}`, // Use labels or fallback
|
|
||||||
value: value,
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Format the price for display in tooltip
|
|
||||||
const formatPrice = (value: number) => {
|
|
||||||
return new Intl.NumberFormat("th-TH", {
|
|
||||||
style: "currency",
|
|
||||||
currency: "THB",
|
|
||||||
minimumFractionDigits: 0,
|
|
||||||
maximumFractionDigits: 0,
|
|
||||||
}).format(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="h-[80px] w-full">
|
|
||||||
{" "}
|
|
||||||
{/* Adjust height as needed */}
|
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
|
||||||
<LineChart data={chartData}>
|
|
||||||
<XAxis dataKey="name" hide />
|
|
||||||
<YAxis hide domain={[(dataMin: number) => dataMin * 0.95, (dataMax: number) => dataMax * 1.05]} />
|
|
||||||
<Tooltip
|
|
||||||
formatter={(value: number) => [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",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Line
|
|
||||||
type="monotone"
|
|
||||||
dataKey="value"
|
|
||||||
stroke={color.replace("rgba", "rgb").replace(/,[^,]*\)/, ")")} // Ensure valid RGB for stroke
|
|
||||||
strokeWidth={2}
|
|
||||||
dot={{ r: 3, strokeWidth: 1 }}
|
|
||||||
activeDot={{ r: 5, strokeWidth: 0 }}
|
|
||||||
// Area charts typically use <Area>, but keeping <Line> based on original code
|
|
||||||
// If area fill is desired:
|
|
||||||
// fill={color}
|
|
||||||
// fillOpacity={0.5}
|
|
||||||
/>
|
|
||||||
</LineChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
/*
|
|
||||||
========================================
|
|
||||||
File: frontend/features/map/components/chat-bot.tsx
|
|
||||||
========================================
|
|
||||||
*/
|
|
||||||
// This component seems redundant with chat-overlay.tsx.
|
|
||||||
// Assuming it's removed or its logic is integrated into chat-overlay.tsx.
|
|
||||||
// If needed, its structure would be similar to chat-overlay.tsx but maybe without the <Overlay> wrapper.
|
|
||||||
@ -1,164 +0,0 @@
|
|||||||
/*
|
|
||||||
========================================
|
|
||||||
File: frontend/features/map/components/filters-overlay.tsx
|
|
||||||
========================================
|
|
||||||
*/
|
|
||||||
"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 { ScrollArea } from "@/components/ui/scroll-area";
|
|
||||||
import { Overlay } from "./overlay-system/overlay"; // Import local overlay system
|
|
||||||
|
|
||||||
export function FiltersOverlay() {
|
|
||||||
const [area, setArea] = useState("< 30 km");
|
|
||||||
const [timePeriod, setTimePeriod] = useState("All Time");
|
|
||||||
const [propertyType, setPropertyType] = useState("House");
|
|
||||||
const [priceRange, setPriceRange] = useState([5_000_000, 20_000_000]);
|
|
||||||
const [activeTab, setActiveTab] = useState("basic");
|
|
||||||
|
|
||||||
const handleApplyFilters = () => {
|
|
||||||
console.log("DUMMY: Applying filters:", {
|
|
||||||
area,
|
|
||||||
timePeriod,
|
|
||||||
propertyType,
|
|
||||||
priceRange, // Include advanced filters state here
|
|
||||||
});
|
|
||||||
// In real app: trigger data refetch with these filters
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Overlay
|
|
||||||
id="filters"
|
|
||||||
title="Property Filters"
|
|
||||||
icon={<Filter className="h-5 w-5" />}
|
|
||||||
initialPosition="bottom-left"
|
|
||||||
initialIsOpen={true}
|
|
||||||
width="350px">
|
|
||||||
<ScrollArea className="h-[calc(min(70vh,500px))]">
|
|
||||||
{" "}
|
|
||||||
{/* Scrollable content */}
|
|
||||||
<div className="p-4">
|
|
||||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
|
||||||
<TabsList className="grid w-full grid-cols-2 mb-4">
|
|
||||||
<TabsTrigger value="basic">Basic</TabsTrigger>
|
|
||||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
|
|
||||||
<TabsContent value="basic" className="space-y-4">
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Label htmlFor="area-radius" className="text-xs font-medium">
|
|
||||||
Area Radius
|
|
||||||
</Label>
|
|
||||||
<Select value={area} onValueChange={setArea}>
|
|
||||||
<SelectTrigger id="area-radius">
|
|
||||||
<SelectValue placeholder="Select area" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="< 10 km">{"< 10 km"}</SelectItem>
|
|
||||||
<SelectItem value="< 20 km">{"< 20 km"}</SelectItem>
|
|
||||||
<SelectItem value="< 30 km">{"< 30 km"}</SelectItem>
|
|
||||||
<SelectItem value="< 50 km">{"< 50 km"}</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Label htmlFor="time-period" className="text-xs font-medium">
|
|
||||||
Time Period
|
|
||||||
</Label>
|
|
||||||
<Select value={timePeriod} onValueChange={setTimePeriod}>
|
|
||||||
<SelectTrigger id="time-period">
|
|
||||||
<SelectValue placeholder="Select time period" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="Last Month">Last Month</SelectItem>
|
|
||||||
<SelectItem value="Last 3 Months">Last 3 Months</SelectItem>
|
|
||||||
<SelectItem value="Last Year">Last Year</SelectItem>
|
|
||||||
<SelectItem value="All Time">All Time</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Label htmlFor="property-type" className="text-xs font-medium">
|
|
||||||
Property Type
|
|
||||||
</Label>
|
|
||||||
<Select value={propertyType} onValueChange={setPropertyType}>
|
|
||||||
<SelectTrigger id="property-type">
|
|
||||||
<SelectValue placeholder="Select property type" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="House">House</SelectItem>
|
|
||||||
<SelectItem value="Condo">Condo</SelectItem>
|
|
||||||
<SelectItem value="Townhouse">Townhouse</SelectItem>
|
|
||||||
<SelectItem value="Land">Land</SelectItem>
|
|
||||||
<SelectItem value="Commercial">Commercial</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="advanced" className="space-y-4">
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div>
|
|
||||||
<div className="flex justify-between mb-1">
|
|
||||||
<Label htmlFor="price-range" className="text-xs font-medium">
|
|
||||||
Price Range
|
|
||||||
</Label>
|
|
||||||
<span className="text-xs text-muted-foreground">
|
|
||||||
{new Intl.NumberFormat("th-TH", { notation: "compact" }).format(priceRange[0])} -{" "}
|
|
||||||
{new Intl.NumberFormat("th-TH", { notation: "compact" }).format(priceRange[1])} ฿
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<Slider
|
|
||||||
id="price-range"
|
|
||||||
value={priceRange}
|
|
||||||
min={1_000_000}
|
|
||||||
max={50_000_000}
|
|
||||||
step={100_000} // Finer step
|
|
||||||
onValueChange={setPriceRange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label className="text-xs font-medium">Environmental Factors</Label>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<Label htmlFor="low-flood" className="text-xs">
|
|
||||||
Low Flood Risk
|
|
||||||
</Label>
|
|
||||||
<Switch id="low-flood" />
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<Label htmlFor="good-air" className="text-xs">
|
|
||||||
Good Air Quality
|
|
||||||
</Label>
|
|
||||||
<Switch id="good-air" />
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<Label htmlFor="low-noise" className="text-xs">
|
|
||||||
Low Noise Pollution
|
|
||||||
</Label>
|
|
||||||
<Switch id="low-noise" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
<Button className="mt-4 w-full" size="sm" onClick={handleApplyFilters}>
|
|
||||||
Apply Filters
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</ScrollArea>
|
|
||||||
</Overlay>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,75 +0,0 @@
|
|||||||
/*
|
|
||||||
========================================
|
|
||||||
File: frontend/features/map/components/map-container.tsx
|
|
||||||
========================================
|
|
||||||
*/
|
|
||||||
"use client";
|
|
||||||
|
|
||||||
import React, { useEffect, useRef } from "react";
|
|
||||||
import type { MapLocation } from "../types"; // Feature-specific type
|
|
||||||
|
|
||||||
interface MapContainerProps {
|
|
||||||
selectedLocation: MapLocation;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function MapContainer({ selectedLocation }: MapContainerProps) {
|
|
||||||
const mapRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const mapElement = mapRef.current;
|
|
||||||
console.log("DUMMY MAP: Initializing/updating for:", selectedLocation);
|
|
||||||
|
|
||||||
if (mapElement) {
|
|
||||||
// Placeholder for actual map library integration (e.g., Leaflet, Mapbox GL JS, Google Maps API)
|
|
||||||
mapElement.innerHTML = `
|
|
||||||
<div style="
|
|
||||||
width: 100%; height: 100%;
|
|
||||||
background-image: url('/placeholder.svg?height=800&width=1200'); /* Using placeholder */
|
|
||||||
background-size: cover;
|
|
||||||
background-position: center;
|
|
||||||
display: flex; align-items: center; justify-content: center;
|
|
||||||
font-family: sans-serif; color: #555;
|
|
||||||
border: 1px dashed #aaa;
|
|
||||||
position: relative; /* Needed for marker positioning */
|
|
||||||
">
|
|
||||||
Map Placeholder: Centered on ${selectedLocation.name || "location"} (${selectedLocation.lat.toFixed(
|
|
||||||
4
|
|
||||||
)}, ${selectedLocation.lng.toFixed(4)})
|
|
||||||
<div style="
|
|
||||||
position: absolute;
|
|
||||||
left: 50%; top: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
width: 24px; height: 24px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: red;
|
|
||||||
border: 4px solid rgba(255, 100, 100, 0.5);
|
|
||||||
animation: pulse 1.5s infinite;
|
|
||||||
"></div>
|
|
||||||
</div>
|
|
||||||
<style>
|
|
||||||
@keyframes pulse {
|
|
||||||
0% { box-shadow: 0 0 0 0 rgba(255, 0, 0, 0.7); }
|
|
||||||
70% { box-shadow: 0 0 0 10px rgba(255, 0, 0, 0); }
|
|
||||||
100% { box-shadow: 0 0 0 0 rgba(255, 0, 0, 0); }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
`;
|
|
||||||
// In a real app, you'd initialize the map library here, set view, add layers/markers.
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cleanup function
|
|
||||||
return () => {
|
|
||||||
console.log("DUMMY MAP: Cleaning up map instance");
|
|
||||||
if (mapElement) {
|
|
||||||
mapElement.innerHTML = ""; // Clear placeholder
|
|
||||||
// In a real app, you'd properly destroy the map instance here.
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [selectedLocation]); // Re-run effect if location changes
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={mapRef} className="absolute inset-0 h-full w-full bg-muted/20 dark:bg-muted/10">
|
|
||||||
{/* The map library will render into this div */}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
/*
|
|
||||||
========================================
|
|
||||||
File: frontend/features/map/components/map-header.tsx
|
|
||||||
========================================
|
|
||||||
*/
|
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { ChevronRight } from "lucide-react";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { ThemeToggle } from "@/components/common/ThemeToggle"; // Import from common
|
|
||||||
|
|
||||||
export function MapHeader() {
|
|
||||||
// Add any map-specific header logic here if needed
|
|
||||||
return (
|
|
||||||
<header className="flex h-14 items-center justify-between border-b px-4 bg-background shrink-0">
|
|
||||||
{/* Breadcrumbs or Title */}
|
|
||||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
||||||
<Link href="/tools" className="hover:text-foreground">
|
|
||||||
{" "}
|
|
||||||
{/* Example link */}
|
|
||||||
Tools
|
|
||||||
</Link>
|
|
||||||
<ChevronRight className="h-4 w-4" />
|
|
||||||
<span className="font-medium text-foreground">Map</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Header Actions */}
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<ThemeToggle />
|
|
||||||
<Button variant="outline" size="sm" className="ml-2">
|
|
||||||
Dummy Action 1
|
|
||||||
</Button>
|
|
||||||
<Button variant="ghost" size="sm">
|
|
||||||
Dummy Action 2
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,144 +0,0 @@
|
|||||||
/*
|
|
||||||
========================================
|
|
||||||
File: frontend/features/map/components/map-sidebar.tsx
|
|
||||||
========================================
|
|
||||||
*/
|
|
||||||
"use client";
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { usePathname } from "next/navigation";
|
|
||||||
import {
|
|
||||||
Home,
|
|
||||||
Map, // Changed from Clock
|
|
||||||
BarChart3, // Changed from Map
|
|
||||||
Layers, // Changed from FileText
|
|
||||||
Settings,
|
|
||||||
SlidersHorizontal, // Changed from PenTool
|
|
||||||
MessageCircle, // Changed from BarChart3
|
|
||||||
Info, // Changed from Plane
|
|
||||||
LineChart,
|
|
||||||
DollarSign,
|
|
||||||
MoreHorizontal,
|
|
||||||
Gift, // Added Gift icon component below
|
|
||||||
} from "lucide-react";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import {
|
|
||||||
Sidebar,
|
|
||||||
SidebarContent,
|
|
||||||
SidebarFooter,
|
|
||||||
SidebarHeader,
|
|
||||||
SidebarMenu,
|
|
||||||
SidebarMenuItem,
|
|
||||||
SidebarMenuButton,
|
|
||||||
} from "@/components/ui/sidebar"; // Assuming sidebar is a shared UI component structure
|
|
||||||
|
|
||||||
export function MapSidebar() {
|
|
||||||
const pathname = usePathname();
|
|
||||||
|
|
||||||
// Define navigation items relevant to the map context or general app navigation shown here
|
|
||||||
const mainNavItems = [
|
|
||||||
{ name: "Map View", icon: Map, href: "/map" },
|
|
||||||
{ name: "Analytics", icon: BarChart3, href: "/map/analytics" }, // Example sub-route
|
|
||||||
{ name: "Filters", icon: SlidersHorizontal, href: "/map/filters" }, // Example sub-route
|
|
||||||
{ name: "Data Layers", icon: Layers, href: "/map/layers" }, // Example sub-route
|
|
||||||
{ name: "Chat", icon: MessageCircle, href: "/map/chat" }, // Example sub-route
|
|
||||||
{ name: "Model Info", icon: Info, href: "/model-explanation" }, // Link to other feature
|
|
||||||
{ name: "Settings", icon: Settings, href: "/settings" }, // Example general setting
|
|
||||||
{ name: "More", icon: MoreHorizontal, href: "/more" }, // Example general setting
|
|
||||||
];
|
|
||||||
|
|
||||||
// Example project-specific items (if sidebar is shared)
|
|
||||||
const projectNavItems = [
|
|
||||||
{ name: "Market Trends", icon: LineChart, href: "/projects/trends" },
|
|
||||||
{ name: "Investment", icon: DollarSign, href: "/projects/investment" },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
// Using the shared Sidebar component structure
|
|
||||||
<Sidebar side="left" variant="sidebar" collapsible="icon">
|
|
||||||
<SidebarHeader>
|
|
||||||
<Link href="/" className="flex items-center gap-2 font-semibold px-2">
|
|
||||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-primary text-primary-foreground">
|
|
||||||
B
|
|
||||||
</div>
|
|
||||||
{/* Hide text when collapsed */}
|
|
||||||
<span className="text-xl font-bold group-data-collapsed:hidden">BorBann</span>
|
|
||||||
</Link>
|
|
||||||
</SidebarHeader>
|
|
||||||
|
|
||||||
<SidebarContent className="p-2">
|
|
||||||
<SidebarMenu>
|
|
||||||
{mainNavItems.map((item) => (
|
|
||||||
<SidebarMenuItem key={item.name}>
|
|
||||||
<SidebarMenuButton
|
|
||||||
asChild // Use asChild if the button itself is a Link or wraps one
|
|
||||||
variant="default"
|
|
||||||
size="default"
|
|
||||||
isActive={pathname === item.href}
|
|
||||||
tooltip={item.name} // Tooltip shown when collapsed
|
|
||||||
>
|
|
||||||
<Link href={item.href}>
|
|
||||||
<item.icon />
|
|
||||||
{/* Hide text when collapsed */}
|
|
||||||
<span className="group-data-collapsed:hidden">{item.name}</span>
|
|
||||||
</Link>
|
|
||||||
</SidebarMenuButton>
|
|
||||||
</SidebarMenuItem>
|
|
||||||
))}
|
|
||||||
</SidebarMenu>
|
|
||||||
|
|
||||||
{/* Optional Project Section */}
|
|
||||||
{/* <SidebarSeparator />
|
|
||||||
<SidebarGroup>
|
|
||||||
<SidebarGroupLabel>Projects</SidebarGroupLabel>
|
|
||||||
<SidebarGroupContent>
|
|
||||||
<SidebarMenu>
|
|
||||||
{projectNavItems.map((item) => (
|
|
||||||
<SidebarMenuItem key={item.name}>
|
|
||||||
<SidebarMenuButton >...</SidebarMenuButton>
|
|
||||||
</SidebarMenuItem>
|
|
||||||
))}
|
|
||||||
</SidebarMenu>
|
|
||||||
</SidebarGroupContent>
|
|
||||||
</SidebarGroup> */}
|
|
||||||
</SidebarContent>
|
|
||||||
|
|
||||||
<SidebarFooter>
|
|
||||||
{/* Footer content like user profile, settings shortcut etc. */}
|
|
||||||
<div className="flex items-center gap-3 p-2">
|
|
||||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-muted text-muted-foreground text-xs">
|
|
||||||
GG
|
|
||||||
</div>
|
|
||||||
<div className="group-data-collapsed:hidden">
|
|
||||||
<div className="font-medium text-sm">GG_WPX</div>
|
|
||||||
<div className="text-xs text-muted-foreground">gg@example.com</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</SidebarFooter>
|
|
||||||
</Sidebar>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Example Gift Icon (if not using lucide-react)
|
|
||||||
function GiftIcon(props: React.SVGProps<SVGSVGElement>) {
|
|
||||||
return (
|
|
||||||
<svg
|
|
||||||
{...props}
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round">
|
|
||||||
<polyline points="20 12 20 22 4 22 4 12"></polyline>
|
|
||||||
<rect x="2" y="7" width="20" height="5"></rect>
|
|
||||||
<line x1="12" y1="22" x2="12" y2="7"></line>
|
|
||||||
<path d="M12 7H7.5a2.5 2.5 0 0 1 0-5C11 2 12 7 12 7z"></path>
|
|
||||||
<path d="M12 7h4.5a2.5 2.5 0 0 0 0-5C13 2 12 7 12 7z"></path>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,230 +0,0 @@
|
|||||||
/*
|
|
||||||
========================================
|
|
||||||
File: frontend/features/map/components/overlay-system/overlay-context.tsx
|
|
||||||
========================================
|
|
||||||
*/
|
|
||||||
"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<OverlayId, OverlayState>;
|
|
||||||
registerOverlay: (id: OverlayId, initialState: Partial<Omit<OverlayState, "id" | "zIndex">>) => 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<OverlayContextType | undefined>(undefined);
|
|
||||||
|
|
||||||
// Default values for overlay state
|
|
||||||
const defaultOverlayState: Omit<OverlayState, "id" | "title" | "icon"> = {
|
|
||||||
isOpen: false,
|
|
||||||
isMinimized: false,
|
|
||||||
position: "bottom-right", // Default position
|
|
||||||
zIndex: 10, // Starting z-index
|
|
||||||
};
|
|
||||||
|
|
||||||
export function OverlayProvider({ children }: { children: ReactNode }) {
|
|
||||||
const [overlays, setOverlays] = useState<Record<OverlayId, OverlayState>>({});
|
|
||||||
const maxZIndexRef = useRef(10); // Start z-index from 10
|
|
||||||
|
|
||||||
// Get the next z-index value
|
|
||||||
const getNextZIndex = useCallback(() => {
|
|
||||||
maxZIndexRef.current += 1;
|
|
||||||
return maxZIndexRef.current;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Register a new overlay
|
|
||||||
const registerOverlay = useCallback(
|
|
||||||
(id: OverlayId, initialState: Partial<Omit<OverlayState, "id" | "zIndex">>) => {
|
|
||||||
setOverlays((prev) => {
|
|
||||||
if (prev[id]) {
|
|
||||||
console.warn(`Overlay with id "${id}" already registered.`);
|
|
||||||
return prev;
|
|
||||||
}
|
|
||||||
const newZIndex = initialState.isOpen ? getNextZIndex() : defaultOverlayState.zIndex;
|
|
||||||
return {
|
|
||||||
...prev,
|
|
||||||
[id]: {
|
|
||||||
...defaultOverlayState,
|
|
||||||
id,
|
|
||||||
title: id, // Default title to id
|
|
||||||
...initialState,
|
|
||||||
zIndex: newZIndex, // Set initial z-index
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[getNextZIndex]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Unregister an overlay
|
|
||||||
const unregisterOverlay = useCallback((id: OverlayId) => {
|
|
||||||
setOverlays((prev) => {
|
|
||||||
const { [id]: _, ...rest } = prev; // Use destructuring to remove the key
|
|
||||||
return rest;
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Open an overlay
|
|
||||||
const openOverlay = useCallback(
|
|
||||||
(id: OverlayId) => {
|
|
||||||
setOverlays((prev) => {
|
|
||||||
if (!prev[id] || prev[id].isOpen) return prev;
|
|
||||||
return {
|
|
||||||
...prev,
|
|
||||||
[id]: {
|
|
||||||
...prev[id],
|
|
||||||
isOpen: true,
|
|
||||||
isMinimized: false, // Ensure not minimized when opened
|
|
||||||
zIndex: getNextZIndex(), // Bring to front
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[getNextZIndex]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Close an overlay
|
|
||||||
const closeOverlay = useCallback((id: OverlayId) => {
|
|
||||||
setOverlays((prev) => {
|
|
||||||
if (!prev[id] || !prev[id].isOpen) return prev;
|
|
||||||
return {
|
|
||||||
...prev,
|
|
||||||
[id]: { ...prev[id], isOpen: false },
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Toggle an overlay's open/closed state
|
|
||||||
const toggleOverlay = useCallback(
|
|
||||||
(id: OverlayId) => {
|
|
||||||
setOverlays((prev) => {
|
|
||||||
if (!prev[id]) return prev; // Don't toggle non-existent overlays
|
|
||||||
|
|
||||||
const willBeOpen = !prev[id].isOpen;
|
|
||||||
const newZIndex = willBeOpen ? getNextZIndex() : prev[id].zIndex; // Bring to front only if opening
|
|
||||||
|
|
||||||
return {
|
|
||||||
...prev,
|
|
||||||
[id]: {
|
|
||||||
...prev[id],
|
|
||||||
isOpen: willBeOpen,
|
|
||||||
isMinimized: willBeOpen ? false : prev[id].isMinimized, // Maximize when toggling open
|
|
||||||
zIndex: newZIndex,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[getNextZIndex]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Minimize an overlay
|
|
||||||
const minimizeOverlay = useCallback((id: OverlayId) => {
|
|
||||||
setOverlays((prev) => {
|
|
||||||
if (!prev[id] || !prev[id].isOpen || prev[id].isMinimized) return prev; // Only minimize open, non-minimized overlays
|
|
||||||
return {
|
|
||||||
...prev,
|
|
||||||
[id]: {
|
|
||||||
...prev[id],
|
|
||||||
isMinimized: true,
|
|
||||||
// Optionally send to back when minimized: zIndex: defaultOverlayState.zIndex
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Maximize an overlay
|
|
||||||
const maximizeOverlay = useCallback(
|
|
||||||
(id: OverlayId) => {
|
|
||||||
setOverlays((prev) => {
|
|
||||||
if (!prev[id] || !prev[id].isOpen || !prev[id].isMinimized) return prev; // Only maximize minimized overlays
|
|
||||||
return {
|
|
||||||
...prev,
|
|
||||||
[id]: {
|
|
||||||
...prev[id],
|
|
||||||
isMinimized: false,
|
|
||||||
zIndex: getNextZIndex(), // Bring to front when maximized
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[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] || !prev[id].isOpen) return prev; // Only bring open overlays to front
|
|
||||||
// Avoid getting new zIndex if already on top
|
|
||||||
if (prev[id].zIndex === maxZIndexRef.current) return prev;
|
|
||||||
return {
|
|
||||||
...prev,
|
|
||||||
[id]: { ...prev[id], zIndex: getNextZIndex() },
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[getNextZIndex]
|
|
||||||
);
|
|
||||||
|
|
||||||
const value = {
|
|
||||||
overlays,
|
|
||||||
registerOverlay,
|
|
||||||
unregisterOverlay,
|
|
||||||
openOverlay,
|
|
||||||
closeOverlay,
|
|
||||||
toggleOverlay,
|
|
||||||
minimizeOverlay,
|
|
||||||
maximizeOverlay,
|
|
||||||
setPosition,
|
|
||||||
bringToFront,
|
|
||||||
getNextZIndex,
|
|
||||||
};
|
|
||||||
|
|
||||||
return <OverlayContext.Provider value={value}>{children}</OverlayContext.Provider>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hook to use the overlay context
|
|
||||||
export function useOverlay() {
|
|
||||||
const context = useContext(OverlayContext);
|
|
||||||
if (context === undefined) {
|
|
||||||
throw new Error("useOverlay must be used within an OverlayProvider");
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
@ -1,67 +0,0 @@
|
|||||||
/*
|
|
||||||
========================================
|
|
||||||
File: frontend/features/map/components/overlay-system/overlay-dock.tsx
|
|
||||||
========================================
|
|
||||||
*/
|
|
||||||
"use client";
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
|
||||||
import { useOverlay } from "./overlay-context";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
interface OverlayDockProps {
|
|
||||||
position?: "bottom" | "right" | "left" | "top"; // Added more positions
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function OverlayDock({ position = "bottom", className }: OverlayDockProps) {
|
|
||||||
const { overlays, toggleOverlay } = useOverlay();
|
|
||||||
|
|
||||||
// Filter overlays that have icons defined (and potentially are registered)
|
|
||||||
const dockableOverlays = Object.values(overlays).filter((overlay) => overlay.icon);
|
|
||||||
|
|
||||||
// No need to render if there are no dockable overlays
|
|
||||||
if (dockableOverlays.length === 0) return null;
|
|
||||||
|
|
||||||
// Define CSS classes for different positions
|
|
||||||
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",
|
|
||||||
left: "fixed left-4 top-1/2 -translate-y-1/2 flex flex-col gap-2 z-50",
|
|
||||||
top: "fixed top-4 left-1/2 -translate-x-1/2 flex flex-row gap-2 z-50",
|
|
||||||
};
|
|
||||||
|
|
||||||
const tooltipSide = {
|
|
||||||
bottom: "top",
|
|
||||||
top: "bottom",
|
|
||||||
left: "right",
|
|
||||||
right: "left",
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TooltipProvider>
|
|
||||||
<div className={cn(positionClasses[position], className)}>
|
|
||||||
{dockableOverlays.map((overlay) => (
|
|
||||||
<div key={overlay.id}>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant={overlay.isOpen && !overlay.isMinimized ? "default" : "outline"} // Highlight if open and not minimized
|
|
||||||
size="icon"
|
|
||||||
className="h-10 w-10 rounded-full bg-background/90 backdrop-blur-xs shadow-md"
|
|
||||||
onClick={() => toggleOverlay(overlay.id)}>
|
|
||||||
{overlay.icon}
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side={tooltipSide[position]}>
|
|
||||||
{overlay.isOpen ? `Hide ${overlay.title}` : `Show ${overlay.title}`}
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</TooltipProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,219 +0,0 @@
|
|||||||
/*
|
|
||||||
========================================
|
|
||||||
File: frontend/features/map/components/overlay-system/overlay.tsx
|
|
||||||
========================================
|
|
||||||
*/
|
|
||||||
"use client";
|
|
||||||
|
|
||||||
import React, { useEffect, useState, useRef, useCallback } 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; // Can be 'auto' or specific value like '400px'
|
|
||||||
maxHeight?: string; // e.g., '80vh'
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Overlay({
|
|
||||||
id,
|
|
||||||
title,
|
|
||||||
icon,
|
|
||||||
initialPosition = "bottom-right",
|
|
||||||
initialIsOpen = false,
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
onClose,
|
|
||||||
showMinimize = true,
|
|
||||||
width = "350px",
|
|
||||||
height = "auto",
|
|
||||||
maxHeight = "80vh", // Default max height
|
|
||||||
}: OverlayProps) {
|
|
||||||
const {
|
|
||||||
overlays,
|
|
||||||
registerOverlay,
|
|
||||||
unregisterOverlay,
|
|
||||||
closeOverlay,
|
|
||||||
minimizeOverlay,
|
|
||||||
maximizeOverlay,
|
|
||||||
bringToFront,
|
|
||||||
// Add setPosition if dragging is implemented
|
|
||||||
} = useOverlay();
|
|
||||||
|
|
||||||
const overlayRef = useRef<HTMLDivElement>(null);
|
|
||||||
// State for dragging logic (Optional, basic example commented out)
|
|
||||||
// const [isDragging, setIsDragging] = useState(false);
|
|
||||||
// const [offset, setOffset] = useState({ x: 0, y: 0 });
|
|
||||||
|
|
||||||
// Register overlay on mount
|
|
||||||
useEffect(() => {
|
|
||||||
registerOverlay(id, {
|
|
||||||
title,
|
|
||||||
icon,
|
|
||||||
position: initialPosition,
|
|
||||||
isOpen: initialIsOpen,
|
|
||||||
// Add initial zIndex if needed, otherwise context handles it
|
|
||||||
});
|
|
||||||
|
|
||||||
// Unregister on unmount
|
|
||||||
return () => unregisterOverlay(id);
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [id, registerOverlay, unregisterOverlay]); // Only run once on mount/unmount
|
|
||||||
|
|
||||||
// Get the current state of this overlay
|
|
||||||
const overlay = overlays[id];
|
|
||||||
|
|
||||||
// --- Optional Dragging Logic ---
|
|
||||||
// const handleMouseDown = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
|
|
||||||
// if (!overlayRef.current) return;
|
|
||||||
// bringToFront(id);
|
|
||||||
// setIsDragging(true);
|
|
||||||
// const rect = overlayRef.current.getBoundingClientRect();
|
|
||||||
// setOffset({
|
|
||||||
// x: e.clientX - rect.left,
|
|
||||||
// y: e.clientY - rect.top,
|
|
||||||
// });
|
|
||||||
// // Prevent text selection during drag
|
|
||||||
// e.preventDefault();
|
|
||||||
// }, [bringToFront, id]);
|
|
||||||
|
|
||||||
// const handleMouseMove = useCallback((e: MouseEvent) => {
|
|
||||||
// if (!isDragging || !overlayRef.current) return;
|
|
||||||
// overlayRef.current.style.left = `${e.clientX - offset.x}px`;
|
|
||||||
// overlayRef.current.style.top = `${e.clientY - offset.y}px`;
|
|
||||||
// // Remove fixed positioning classes if dragging manually
|
|
||||||
// overlayRef.current.classList.remove(...Object.values(positionClasses));
|
|
||||||
// }, [isDragging, offset]);
|
|
||||||
|
|
||||||
// const handleMouseUp = useCallback(() => {
|
|
||||||
// if (isDragging) {
|
|
||||||
// setIsDragging(false);
|
|
||||||
// // Optional: Snap to edge or update position state in context
|
|
||||||
// }
|
|
||||||
// }, [isDragging]);
|
|
||||||
|
|
||||||
// useEffect(() => {
|
|
||||||
// if (isDragging) {
|
|
||||||
// window.addEventListener("mousemove", handleMouseMove);
|
|
||||||
// window.addEventListener("mouseup", handleMouseUp);
|
|
||||||
// } else {
|
|
||||||
// window.removeEventListener("mousemove", handleMouseMove);
|
|
||||||
// window.removeEventListener("mouseup", handleMouseUp);
|
|
||||||
// }
|
|
||||||
// return () => {
|
|
||||||
// window.removeEventListener("mousemove", handleMouseMove);
|
|
||||||
// window.removeEventListener("mouseup", handleMouseUp);
|
|
||||||
// };
|
|
||||||
// }, [isDragging, handleMouseMove, handleMouseUp]);
|
|
||||||
// --- End Optional Dragging Logic ---
|
|
||||||
|
|
||||||
// If the overlay isn't registered yet or isn't open, don't render anything
|
|
||||||
if (!overlay || !overlay.isOpen) return null;
|
|
||||||
|
|
||||||
const handleCloseClick = (e: React.MouseEvent) => {
|
|
||||||
e.stopPropagation(); // Prevent triggering bringToFront if clicking close
|
|
||||||
closeOverlay(id);
|
|
||||||
if (onClose) onClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMinimizeClick = (e: React.MouseEvent) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
minimizeOverlay(id);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMaximizeClick = (e: React.MouseEvent) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
maximizeOverlay(id);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleHeaderMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
||||||
bringToFront(id);
|
|
||||||
// handleMouseDown(e); // Uncomment if implementing dragging
|
|
||||||
};
|
|
||||||
|
|
||||||
// Define position classes based on the current state
|
|
||||||
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",
|
|
||||||
};
|
|
||||||
|
|
||||||
// Render minimized state in the dock (handled by OverlayDock now)
|
|
||||||
if (overlay.isMinimized) {
|
|
||||||
// Minimized state is now handled by the OverlayDock component based on context state
|
|
||||||
// This component only renders the full overlay or nothing.
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render full overlay
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={overlayRef}
|
|
||||||
className={cn(
|
|
||||||
"fixed z-10", // z-index is managed by inline style
|
|
||||||
positionClasses[overlay.position], // Apply position classes
|
|
||||||
// Add transition for position changes if needed: 'transition-all duration-300 ease-out'
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
style={{ zIndex: overlay.zIndex }} // Apply dynamic z-index
|
|
||||||
onClick={() => bringToFront(id)} // Bring to front on any click within the overlay
|
|
||||||
aria-labelledby={`${id}-title`}
|
|
||||||
role="dialog" // Use appropriate role
|
|
||||||
>
|
|
||||||
<Card
|
|
||||||
className={cn(
|
|
||||||
"shadow-lg bg-card/95 backdrop-blur-xs border border-border/50 overflow-hidden flex flex-col", // Added flex flex-col
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
style={{
|
|
||||||
width,
|
|
||||||
height, // Use height directly
|
|
||||||
maxHeight, // Use maxHeight
|
|
||||||
maxWidth: "calc(100vw - 32px)", // Prevent overlay exceeding viewport width
|
|
||||||
}}>
|
|
||||||
{/* Make header draggable */}
|
|
||||||
<CardHeader
|
|
||||||
className="pb-2 flex flex-row items-center justify-between cursor-move shrink-0" // Added shrink-0
|
|
||||||
// onMouseDown={handleHeaderMouseDown} // Uncomment if implementing dragging
|
|
||||||
>
|
|
||||||
<CardTitle id={`${id}-title`} className="text-sm font-medium flex items-center gap-2">
|
|
||||||
{icon && <span className="text-primary">{icon}</span>}
|
|
||||||
{title}
|
|
||||||
{/* <Move className="h-3 w-3 text-muted-foreground ml-1 cursor-move" /> */} {/* Optional move icon */}
|
|
||||||
</CardTitle>
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
{showMinimize && (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="h-6 w-6 p-0"
|
|
||||||
onClick={handleMinimizeClick}
|
|
||||||
title="Minimize">
|
|
||||||
<Minimize2 className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Button variant="ghost" size="icon" className="h-6 w-6 p-0" onClick={handleCloseClick} title="Close">
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
{/* Ensure content area takes remaining space and scrolls if needed */}
|
|
||||||
<CardContent className="p-0 flex-1 min-h-0 overflow-auto">{children}</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
/*
|
|
||||||
========================================
|
|
||||||
File: frontend/features/map/components/property-filters.tsx
|
|
||||||
========================================
|
|
||||||
*/
|
|
||||||
// This component seems redundant with filters-overlay.tsx.
|
|
||||||
// Assuming it's removed or its logic is integrated into filters-overlay.tsx.
|
|
||||||
// If needed, its structure would be similar to filters-overlay.tsx but maybe without the <Overlay> wrapper.
|
|
||||||
@ -1,38 +0,0 @@
|
|||||||
/*
|
|
||||||
========================================
|
|
||||||
File: frontend/features/map/hooks/useMapInteractions.ts (NEW - Dummy)
|
|
||||||
========================================
|
|
||||||
*/
|
|
||||||
import { useState, useCallback } from "react";
|
|
||||||
import type { MapLocation } from "../types";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DUMMY Hook: Manages map interaction state like selected markers, zoom level etc.
|
|
||||||
*/
|
|
||||||
export function useMapInteractions(initialLocation: MapLocation) {
|
|
||||||
const [currentLocation, setCurrentLocation] = useState<MapLocation>(initialLocation);
|
|
||||||
const [selectedMarkerId, setSelectedMarkerId] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const handleMapMove = useCallback((newLocation: MapLocation) => {
|
|
||||||
console.log("DUMMY Hook: Map moved to", newLocation);
|
|
||||||
setCurrentLocation(newLocation);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleMarkerClick = useCallback((markerId: string) => {
|
|
||||||
console.log("DUMMY Hook: Marker clicked", markerId);
|
|
||||||
setSelectedMarkerId(markerId);
|
|
||||||
// Potentially fetch details for this marker via API
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const clearSelection = useCallback(() => {
|
|
||||||
setSelectedMarkerId(null);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return {
|
|
||||||
currentLocation,
|
|
||||||
selectedMarkerId,
|
|
||||||
handleMapMove,
|
|
||||||
handleMarkerClick,
|
|
||||||
clearSelection,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
/*
|
|
||||||
========================================
|
|
||||||
File: frontend/features/map/types/index.ts (NEW - Dummy)
|
|
||||||
========================================
|
|
||||||
*/
|
|
||||||
// Types specific only to the Map feature
|
|
||||||
|
|
||||||
export interface MapBounds {
|
|
||||||
north: number;
|
|
||||||
south: number;
|
|
||||||
east: number;
|
|
||||||
west: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MapLocation {
|
|
||||||
lat: number;
|
|
||||||
lng: number;
|
|
||||||
name?: string;
|
|
||||||
zoom?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Example type for map layer configuration
|
|
||||||
export interface MapLayerConfig {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
url: string; // e.g., Tile server URL template
|
|
||||||
type: "raster" | "vector" | "geojson";
|
|
||||||
visible: boolean;
|
|
||||||
opacity?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Example type for data displayed on the map
|
|
||||||
export interface MapPropertyData {
|
|
||||||
id: string;
|
|
||||||
coordinates: [number, number]; // [lng, lat]
|
|
||||||
price: number;
|
|
||||||
type: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Re-export relevant shared types if needed for convenience
|
|
||||||
// export type { PointOfInterest } from '@/types/api';
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
/*
|
|
||||||
========================================
|
|
||||||
File: frontend/features/map/utils/mapHelpers.ts (NEW - Dummy)
|
|
||||||
========================================
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { MapBounds, MapLocation } from "../types";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DUMMY Utility: Calculates the center of given map bounds.
|
|
||||||
*/
|
|
||||||
export function calculateBoundsCenter(bounds: MapBounds): MapLocation {
|
|
||||||
const centerLat = (bounds.north + bounds.south) / 2;
|
|
||||||
const centerLng = (bounds.east + bounds.west) / 2;
|
|
||||||
console.log("DUMMY Util: Calculating center for bounds:", bounds);
|
|
||||||
return { lat: centerLat, lng: centerLng };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DUMMY Utility: Formats coordinates for display.
|
|
||||||
*/
|
|
||||||
export function formatCoords(location: MapLocation): string {
|
|
||||||
return `${location.lat.toFixed(4)}, ${location.lng.toFixed(4)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add other map-specific utility functions here
|
|
||||||
@ -1,51 +0,0 @@
|
|||||||
/*
|
|
||||||
========================================
|
|
||||||
File: frontend/features/model-explanation/api/explanationApi.ts (NEW - Dummy)
|
|
||||||
========================================
|
|
||||||
*/
|
|
||||||
import apiClient from "@/services/apiClient";
|
|
||||||
import type { APIResponse } from "@/types/api";
|
|
||||||
import type { ModelExplanationData, FeatureImportance } from "../types";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DUMMY: Fetches data needed for the model explanation page.
|
|
||||||
*/
|
|
||||||
export async function fetchModelExplanation(propertyId: string): Promise<APIResponse<ModelExplanationData>> {
|
|
||||||
console.log(`DUMMY API: Fetching model explanation for property ID: ${propertyId}`);
|
|
||||||
// return apiClient.get<ModelExplanationData>(`/properties/${propertyId}/explanation`);
|
|
||||||
|
|
||||||
// Simulate response
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 700));
|
|
||||||
const dummyExplanation: ModelExplanationData = {
|
|
||||||
propertyDetails: {
|
|
||||||
address: `Dummy Property ${propertyId}`,
|
|
||||||
type: "Condo",
|
|
||||||
size: 120,
|
|
||||||
bedrooms: 2,
|
|
||||||
bathrooms: 2,
|
|
||||||
age: 3,
|
|
||||||
floor: 10,
|
|
||||||
amenities: ["Pool", "Gym"],
|
|
||||||
predictedPrice: 12500000,
|
|
||||||
},
|
|
||||||
similarProperties: [
|
|
||||||
{ address: "Comp 1", price: 12000000, size: 115, age: 4 },
|
|
||||||
{ address: "Comp 2", price: 13500000, size: 130, age: 2 },
|
|
||||||
],
|
|
||||||
features: [
|
|
||||||
{ name: "Location", importance: 40, value: "Near BTS", impact: "positive" },
|
|
||||||
{ name: "Size", importance: 30, value: "120 sqm", impact: "positive" },
|
|
||||||
{ name: "Age", importance: 15, value: "3 years", impact: "neutral" },
|
|
||||||
{ name: "Amenities", importance: 10, value: "Pool, Gym", impact: "positive" },
|
|
||||||
{ name: "Floor", importance: 5, value: "10th", impact: "positive" },
|
|
||||||
],
|
|
||||||
environmentalFactors: {
|
|
||||||
floodRisk: "low",
|
|
||||||
airQuality: "moderate",
|
|
||||||
noiseLevel: "low",
|
|
||||||
},
|
|
||||||
confidence: 0.91,
|
|
||||||
priceRange: { lower: 11800000, upper: 13200000 },
|
|
||||||
};
|
|
||||||
return { success: true, data: dummyExplanation };
|
|
||||||
}
|
|
||||||
@ -1,71 +0,0 @@
|
|||||||
/*
|
|
||||||
========================================
|
|
||||||
File: frontend/features/model-explanation/components/feature-importance-chart.tsx
|
|
||||||
========================================
|
|
||||||
*/
|
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { useTheme } from "next-themes";
|
|
||||||
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Cell } from "@/components/ui/chart"; // Using shared ui chart components
|
|
||||||
import type { FeatureImportance } from "../types"; // Feature specific type
|
|
||||||
|
|
||||||
interface FeatureImportanceChartProps {
|
|
||||||
features: FeatureImportance[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function FeatureImportanceChart({ features }: FeatureImportanceChartProps) {
|
|
||||||
const { theme } = useTheme();
|
|
||||||
const isDark = theme === "dark";
|
|
||||||
|
|
||||||
// Sort features by importance for consistent display
|
|
||||||
const sortedFeatures = [...features].sort((a, b) => b.importance - a.importance);
|
|
||||||
|
|
||||||
// Define colors based on impact
|
|
||||||
const getBarColor = (impact: "positive" | "negative" | "neutral") => {
|
|
||||||
if (impact === "positive") return "#10b981"; // Green
|
|
||||||
if (impact === "negative") return "#ef4444"; // Red
|
|
||||||
return "#f59e0b"; // Amber/Yellow for neutral
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
|
||||||
<BarChart data={sortedFeatures} layout="vertical" margin={{ top: 5, right: 30, left: 80, bottom: 5 }}>
|
|
||||||
<CartesianGrid
|
|
||||||
strokeDasharray="3 3"
|
|
||||||
horizontal={true}
|
|
||||||
vertical={false} // Typically vertical grid lines are less useful for horizontal bar charts
|
|
||||||
stroke={isDark ? "#374151" : "#e5e7eb"}
|
|
||||||
/>
|
|
||||||
<XAxis
|
|
||||||
type="number"
|
|
||||||
domain={[0, 100]} // Assuming importance is a percentage
|
|
||||||
tickFormatter={(value) => `${value}%`}
|
|
||||||
stroke={isDark ? "#9ca3af" : "#6b7280"}
|
|
||||||
/>
|
|
||||||
<YAxis
|
|
||||||
dataKey="name"
|
|
||||||
type="category"
|
|
||||||
width={80} // Adjust width based on longest label
|
|
||||||
stroke={isDark ? "#9ca3af" : "#6b7280"}
|
|
||||||
tick={{ fontSize: 12 }}
|
|
||||||
/>
|
|
||||||
<Tooltip
|
|
||||||
formatter={(value: number) => [`${value.toFixed(1)}%`, "Importance"]}
|
|
||||||
labelFormatter={(label: string) => `Feature: ${label}`} // Show feature name in tooltip
|
|
||||||
contentStyle={{
|
|
||||||
backgroundColor: isDark ? "#1f2937" : "white",
|
|
||||||
borderRadius: "0.375rem",
|
|
||||||
border: isDark ? "1px solid #374151" : "1px solid #e2e8f0",
|
|
||||||
fontSize: "0.75rem",
|
|
||||||
color: isDark ? "#e5e7eb" : "#1f2937",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Bar dataKey="importance" radius={[0, 4, 4, 0]}>
|
|
||||||
{sortedFeatures.map((entry, index) => (
|
|
||||||
<Cell key={`cell-${index}`} fill={getBarColor(entry.impact)} />
|
|
||||||
))}
|
|
||||||
</Bar>
|
|
||||||
</BarChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,94 +0,0 @@
|
|||||||
/*
|
|
||||||
========================================
|
|
||||||
File: frontend/features/model-explanation/components/price-comparison-chart.tsx
|
|
||||||
========================================
|
|
||||||
*/
|
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { useTheme } from "next-themes";
|
|
||||||
import {
|
|
||||||
BarChart,
|
|
||||||
Bar,
|
|
||||||
XAxis,
|
|
||||||
YAxis,
|
|
||||||
CartesianGrid,
|
|
||||||
Tooltip,
|
|
||||||
ResponsiveContainer,
|
|
||||||
Legend,
|
|
||||||
Cell,
|
|
||||||
} from "@/components/ui/chart"; // Using shared ui chart components
|
|
||||||
import type { ComparableProperty, PropertyBaseDetails } from "../types"; // Feature specific types
|
|
||||||
|
|
||||||
interface PriceComparisonChartProps {
|
|
||||||
property: PropertyBaseDetails & { name: string }; // Add name for the primary property
|
|
||||||
comparisons: ComparableProperty[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function PriceComparisonChart({ property, comparisons }: PriceComparisonChartProps) {
|
|
||||||
const { theme } = useTheme();
|
|
||||||
const isDark = theme === "dark";
|
|
||||||
|
|
||||||
// Combine property and comparisons for chart data
|
|
||||||
// Ensure the property being explained is included and identifiable
|
|
||||||
const data = [
|
|
||||||
{ ...property }, // Keep all details for tooltip if needed
|
|
||||||
...comparisons.map((c) => ({ ...c, name: c.address })), // Use address as name for comparisons
|
|
||||||
];
|
|
||||||
|
|
||||||
// Format the price for display
|
|
||||||
const formatPrice = (value: number) => {
|
|
||||||
return new Intl.NumberFormat("th-TH", {
|
|
||||||
style: "currency",
|
|
||||||
currency: "THB",
|
|
||||||
notation: "compact", // Use compact notation like 15M
|
|
||||||
minimumFractionDigits: 0,
|
|
||||||
maximumFractionDigits: 1,
|
|
||||||
}).format(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Custom tooltip content
|
|
||||||
const CustomTooltip = ({ active, payload, label }: any) => {
|
|
||||||
if (active && payload && payload.length) {
|
|
||||||
const data = payload[0].payload; // Get the data point for this bar
|
|
||||||
return (
|
|
||||||
<div className="p-2 bg-background border rounded shadow-lg text-xs">
|
|
||||||
<p className="font-bold">{label}</p>
|
|
||||||
<p>Price: {formatPrice(data.price)}</p>
|
|
||||||
<p>Size: {data.size} sqm</p>
|
|
||||||
<p>Age: {data.age} years</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
|
||||||
<BarChart data={data} margin={{ top: 5, right: 10, left: 40, bottom: 5 }}>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke={isDark ? "#374151" : "#e5e7eb"} />
|
|
||||||
<XAxis dataKey="name" stroke={isDark ? "#9ca3af" : "#6b7280"} fontSize={10} interval={0} />
|
|
||||||
<YAxis
|
|
||||||
tickFormatter={(value) => formatPrice(value)}
|
|
||||||
stroke={isDark ? "#9ca3af" : "#6b7280"}
|
|
||||||
fontSize={10}
|
|
||||||
width={40}
|
|
||||||
/>
|
|
||||||
<Tooltip
|
|
||||||
cursor={{ fill: isDark ? "rgba(107, 114, 128, 0.2)" : "rgba(209, 213, 219, 0.4)" }} // Subtle hover effect
|
|
||||||
content={<CustomTooltip />} // Use custom tooltip
|
|
||||||
wrapperStyle={{ zIndex: 100 }} // Ensure tooltip is on top
|
|
||||||
/>
|
|
||||||
{/* <Legend /> // Legend might be redundant if XAxis labels are clear */}
|
|
||||||
<Bar dataKey="price" name="Price" radius={[4, 4, 0, 0]}>
|
|
||||||
{data.map((entry, index) => (
|
|
||||||
<Cell
|
|
||||||
key={`cell-${index}`}
|
|
||||||
fill={entry.name === property.name ? "#3b82f6" : "#6b7280"} // Highlight the main property
|
|
||||||
fillOpacity={entry.name === property.name ? 1 : 0.7}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Bar>
|
|
||||||
</BarChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,50 +0,0 @@
|
|||||||
/*
|
|
||||||
========================================
|
|
||||||
File: frontend/features/model-explanation/types/index.ts (NEW - Dummy)
|
|
||||||
========================================
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Types specific to the Model Explanation feature
|
|
||||||
|
|
||||||
export interface FeatureImportance {
|
|
||||||
name: string;
|
|
||||||
importance: number; // e.g., percentage 0-100
|
|
||||||
value: string | number; // The actual value for the property being explained
|
|
||||||
impact: "positive" | "negative" | "neutral";
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ComparableProperty {
|
|
||||||
address: string;
|
|
||||||
price: number;
|
|
||||||
size: number;
|
|
||||||
age: number;
|
|
||||||
// Add other relevant comparison fields if needed
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PropertyBaseDetails {
|
|
||||||
address: string;
|
|
||||||
type: string;
|
|
||||||
size: number;
|
|
||||||
bedrooms?: number;
|
|
||||||
bathrooms?: number;
|
|
||||||
age: number;
|
|
||||||
floor?: number;
|
|
||||||
amenities?: string[];
|
|
||||||
predictedPrice: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface EnvironmentalFactors {
|
|
||||||
floodRisk: "low" | "moderate" | "high" | "unknown";
|
|
||||||
airQuality: "good" | "moderate" | "poor" | "unknown";
|
|
||||||
noiseLevel: "low" | "moderate" | "high" | "unknown";
|
|
||||||
// Add other factors like proximity scores etc.
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ModelExplanationData {
|
|
||||||
propertyDetails: PropertyBaseDetails;
|
|
||||||
similarProperties: ComparableProperty[];
|
|
||||||
features: FeatureImportance[];
|
|
||||||
environmentalFactors: EnvironmentalFactors;
|
|
||||||
confidence: number; // e.g., 0.92 for 92%
|
|
||||||
priceRange: { lower: number; upper: number };
|
|
||||||
}
|
|
||||||
@ -1,37 +1,19 @@
|
|||||||
/*
|
import * as React from "react"
|
||||||
========================================
|
|
||||||
File: frontend/hooks/use-mobile.tsx
|
|
||||||
========================================
|
|
||||||
*/
|
|
||||||
import * as React from "react";
|
|
||||||
|
|
||||||
const MOBILE_BREAKPOINT = 768; // Standard md breakpoint
|
const MOBILE_BREAKPOINT = 768
|
||||||
|
|
||||||
export function useIsMobile(): boolean {
|
export function useIsMobile() {
|
||||||
// Initialize state based on current window size (or undefined if SSR)
|
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
|
||||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
|
|
||||||
typeof window !== "undefined" ? window.innerWidth < MOBILE_BREAKPOINT : undefined
|
|
||||||
);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
// Ensure this runs only client-side
|
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
|
||||||
if (typeof window === "undefined") {
|
const onChange = () => {
|
||||||
return;
|
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||||
}
|
}
|
||||||
|
mql.addEventListener("change", onChange)
|
||||||
|
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||||
|
return () => mql.removeEventListener("change", onChange)
|
||||||
|
}, [])
|
||||||
|
|
||||||
const handleResize = () => {
|
return !!isMobile
|
||||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Set initial state correctly after mount
|
|
||||||
handleResize();
|
|
||||||
|
|
||||||
window.addEventListener("resize", handleResize);
|
|
||||||
|
|
||||||
// Cleanup listener on unmount
|
|
||||||
return () => window.removeEventListener("resize", handleResize);
|
|
||||||
}, []); // Empty dependency array ensures this runs only once on mount and cleanup on unmount
|
|
||||||
|
|
||||||
// Return false during SSR or initial client render before effect runs
|
|
||||||
return isMobile ?? false;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,106 +1,106 @@
|
|||||||
/*
|
"use client"
|
||||||
========================================
|
|
||||||
File: frontend/hooks/use-toast.ts
|
|
||||||
========================================
|
|
||||||
*/
|
|
||||||
"use client";
|
|
||||||
|
|
||||||
// Inspired by react-hot-toast library
|
// Inspired by react-hot-toast library
|
||||||
import * as React from "react";
|
import * as React from "react"
|
||||||
// Import types from the actual Toast component location
|
|
||||||
import type { ToastActionElement, ToastProps } from "@/components/ui/toast";
|
|
||||||
|
|
||||||
const TOAST_LIMIT = 1; // Show only one toast at a time
|
import type {
|
||||||
const TOAST_REMOVE_DELAY = 1000000; // A very long time (effectively manual dismiss only)
|
ToastActionElement,
|
||||||
|
ToastProps,
|
||||||
|
} from "@/components/ui/toast"
|
||||||
|
|
||||||
|
const TOAST_LIMIT = 1
|
||||||
|
const TOAST_REMOVE_DELAY = 1000000
|
||||||
|
|
||||||
type ToasterToast = ToastProps & {
|
type ToasterToast = ToastProps & {
|
||||||
id: string;
|
id: string
|
||||||
title?: React.ReactNode;
|
title?: React.ReactNode
|
||||||
description?: React.ReactNode;
|
description?: React.ReactNode
|
||||||
action?: ToastActionElement;
|
action?: ToastActionElement
|
||||||
};
|
}
|
||||||
|
|
||||||
const actionTypes = {
|
const actionTypes = {
|
||||||
ADD_TOAST: "ADD_TOAST",
|
ADD_TOAST: "ADD_TOAST",
|
||||||
UPDATE_TOAST: "UPDATE_TOAST",
|
UPDATE_TOAST: "UPDATE_TOAST",
|
||||||
DISMISS_TOAST: "DISMISS_TOAST",
|
DISMISS_TOAST: "DISMISS_TOAST",
|
||||||
REMOVE_TOAST: "REMOVE_TOAST",
|
REMOVE_TOAST: "REMOVE_TOAST",
|
||||||
} as const;
|
} as const
|
||||||
|
|
||||||
let count = 0;
|
let count = 0
|
||||||
|
|
||||||
function genId() {
|
function genId() {
|
||||||
count = (count + 1) % Number.MAX_SAFE_INTEGER;
|
count = (count + 1) % Number.MAX_SAFE_INTEGER
|
||||||
return count.toString();
|
return count.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
type ActionType = typeof actionTypes;
|
type ActionType = typeof actionTypes
|
||||||
|
|
||||||
type Action =
|
type Action =
|
||||||
| {
|
| {
|
||||||
type: ActionType["ADD_TOAST"];
|
type: ActionType["ADD_TOAST"]
|
||||||
toast: ToasterToast;
|
toast: ToasterToast
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: ActionType["UPDATE_TOAST"];
|
type: ActionType["UPDATE_TOAST"]
|
||||||
toast: Partial<ToasterToast>;
|
toast: Partial<ToasterToast>
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: ActionType["DISMISS_TOAST"];
|
type: ActionType["DISMISS_TOAST"]
|
||||||
toastId?: ToasterToast["id"];
|
toastId?: ToasterToast["id"]
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: ActionType["REMOVE_TOAST"];
|
type: ActionType["REMOVE_TOAST"]
|
||||||
toastId?: ToasterToast["id"];
|
toastId?: ToasterToast["id"]
|
||||||
};
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
toasts: ToasterToast[];
|
toasts: ToasterToast[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
|
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
|
||||||
|
|
||||||
const addToRemoveQueue = (toastId: string) => {
|
const addToRemoveQueue = (toastId: string) => {
|
||||||
if (toastTimeouts.has(toastId)) {
|
if (toastTimeouts.has(toastId)) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
toastTimeouts.delete(toastId);
|
toastTimeouts.delete(toastId)
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "REMOVE_TOAST",
|
type: "REMOVE_TOAST",
|
||||||
toastId: toastId,
|
toastId: toastId,
|
||||||
});
|
})
|
||||||
}, TOAST_REMOVE_DELAY);
|
}, TOAST_REMOVE_DELAY)
|
||||||
|
|
||||||
toastTimeouts.set(toastId, timeout);
|
toastTimeouts.set(toastId, timeout)
|
||||||
};
|
}
|
||||||
|
|
||||||
export const reducer = (state: State, action: Action): State => {
|
export const reducer = (state: State, action: Action): State => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case "ADD_TOAST":
|
case "ADD_TOAST":
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
// Slice ensures the limit is enforced
|
|
||||||
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
||||||
};
|
}
|
||||||
|
|
||||||
case "UPDATE_TOAST":
|
case "UPDATE_TOAST":
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
toasts: state.toasts.map((t) => (t.id === action.toast.id ? { ...t, ...action.toast } : t)),
|
toasts: state.toasts.map((t) =>
|
||||||
};
|
t.id === action.toast.id ? { ...t, ...action.toast } : t
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
case "DISMISS_TOAST": {
|
case "DISMISS_TOAST": {
|
||||||
const { toastId } = action;
|
const { toastId } = action
|
||||||
|
|
||||||
// Side effect: schedule removal for dismissed toasts
|
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
||||||
|
// but I'll keep it here for simplicity
|
||||||
if (toastId) {
|
if (toastId) {
|
||||||
addToRemoveQueue(toastId);
|
addToRemoveQueue(toastId)
|
||||||
} else {
|
} else {
|
||||||
state.toasts.forEach((toast) => {
|
state.toasts.forEach((toast) => {
|
||||||
addToRemoveQueue(toast.id);
|
addToRemoveQueue(toast.id)
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -109,48 +109,48 @@ export const reducer = (state: State, action: Action): State => {
|
|||||||
t.id === toastId || toastId === undefined
|
t.id === toastId || toastId === undefined
|
||||||
? {
|
? {
|
||||||
...t,
|
...t,
|
||||||
open: false, // Trigger the close animation
|
open: false,
|
||||||
}
|
}
|
||||||
: t
|
: t
|
||||||
),
|
),
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
case "REMOVE_TOAST":
|
case "REMOVE_TOAST":
|
||||||
if (action.toastId === undefined) {
|
if (action.toastId === undefined) {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
toasts: [], // Remove all toasts
|
toasts: [],
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
||||||
};
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const listeners: Array<(state: State) => void> = [];
|
|
||||||
|
|
||||||
let memoryState: State = { toasts: [] }; // In-memory state
|
|
||||||
|
|
||||||
function dispatch(action: Action) {
|
|
||||||
memoryState = reducer(memoryState, action);
|
|
||||||
listeners.forEach((listener) => {
|
|
||||||
listener(memoryState);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Toast = Omit<ToasterToast, "id">;
|
const listeners: Array<(state: State) => void> = []
|
||||||
|
|
||||||
|
let memoryState: State = { toasts: [] }
|
||||||
|
|
||||||
|
function dispatch(action: Action) {
|
||||||
|
memoryState = reducer(memoryState, action)
|
||||||
|
listeners.forEach((listener) => {
|
||||||
|
listener(memoryState)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type Toast = Omit<ToasterToast, "id">
|
||||||
|
|
||||||
function toast({ ...props }: Toast) {
|
function toast({ ...props }: Toast) {
|
||||||
const id = genId();
|
const id = genId()
|
||||||
|
|
||||||
const update = (props: ToasterToast) =>
|
const update = (props: ToasterToast) =>
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "UPDATE_TOAST",
|
type: "UPDATE_TOAST",
|
||||||
toast: { ...props, id },
|
toast: { ...props, id },
|
||||||
});
|
})
|
||||||
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
|
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "ADD_TOAST",
|
type: "ADD_TOAST",
|
||||||
@ -159,37 +159,36 @@ function toast({ ...props }: Toast) {
|
|||||||
id,
|
id,
|
||||||
open: true,
|
open: true,
|
||||||
onOpenChange: (open) => {
|
onOpenChange: (open) => {
|
||||||
if (!open) dismiss(); // Ensure dismiss is called when the toast closes itself
|
if (!open) dismiss()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: id,
|
id: id,
|
||||||
dismiss,
|
dismiss,
|
||||||
update,
|
update,
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function useToast() {
|
function useToast() {
|
||||||
const [state, setState] = React.useState<State>(memoryState);
|
const [state, setState] = React.useState<State>(memoryState)
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
listeners.push(setState);
|
listeners.push(setState)
|
||||||
return () => {
|
return () => {
|
||||||
// Clean up listener
|
const index = listeners.indexOf(setState)
|
||||||
const index = listeners.indexOf(setState);
|
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
listeners.splice(index, 1);
|
listeners.splice(index, 1)
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
}, [state]); // Only re-subscribe if state instance changes (it shouldn't)
|
}, [state])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
toast,
|
toast,
|
||||||
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
|
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { useToast, toast };
|
export { useToast, toast }
|
||||||
|
|||||||
BIN
frontend/public/map.png
Normal file
BIN
frontend/public/map.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 446 KiB |
@ -1,106 +1,135 @@
|
|||||||
/*
|
/* === src/services/apiClient.ts === */
|
||||||
========================================
|
/**
|
||||||
File: frontend/services/apiClient.ts (NEW - Dummy)
|
* API Client - Dummy Implementation
|
||||||
========================================
|
*
|
||||||
*/
|
* This provides a basic structure for making API calls.
|
||||||
import type { APIResponse } from "@/types/api"; // Import shared response type
|
* - It includes an Authorization header in each request (assuming token-based auth).
|
||||||
|
* - Replace `getAuthToken` with your actual token retrieval logic.
|
||||||
|
* - Consider using libraries like axios or ky for more robust features in a real app.
|
||||||
|
*/
|
||||||
|
|
||||||
// --- Dummy Auth Token ---
|
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "/api/v1"; // Example API base URL
|
||||||
// In a real app, this would come from localStorage, context, or a state manager after login
|
|
||||||
const DUMMY_AUTH_TOKEN = "Bearer dummy-jwt-token-12345";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base function for making API requests.
|
* Retrieves the authentication token.
|
||||||
* Includes dummy authorization header.
|
* Replace this with your actual implementation (e.g., from localStorage, context, state management).
|
||||||
|
* @returns {string | null} The auth token or null if not found.
|
||||||
*/
|
*/
|
||||||
async function fetchApi<T = any>(endpoint: string, options: RequestInit = {}): Promise<APIResponse<T>> {
|
const getAuthToken = (): string | null => {
|
||||||
const url = `${process.env.NEXT_PUBLIC_API_URL || "/api/v1"}${endpoint}`;
|
// Dummy implementation: Replace with your actual logic
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
// Example: return localStorage.getItem("authToken");
|
||||||
|
// For dummy purposes, returning a placeholder token
|
||||||
|
return "dummy-auth-token-12345";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
const defaultHeaders: HeadersInit = {
|
interface FetchOptions extends RequestInit {
|
||||||
"Content-Type": "application/json",
|
params?: Record<string, string | number | boolean>; // For query parameters
|
||||||
Authorization: DUMMY_AUTH_TOKEN, // Add dummy token here
|
/** If true, Content-Type header will not be set (e.g., for FormData) */
|
||||||
};
|
skipContentType?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
const config: RequestInit = {
|
/**
|
||||||
...options,
|
* Generic fetch function for API calls.
|
||||||
headers: {
|
* @template T The expected response type.
|
||||||
...defaultHeaders,
|
* @param {string} endpoint The API endpoint (e.g., '/users').
|
||||||
...options.headers,
|
* @param {FetchOptions} options Fetch options (method, body, headers, params).
|
||||||
},
|
* @returns {Promise<T>} The response data.
|
||||||
};
|
*/
|
||||||
|
async function apiClient<T>(endpoint: string, options: FetchOptions = {}): Promise<T> {
|
||||||
|
const { params, headers: customHeaders, skipContentType, body, ...fetchOptions } = options;
|
||||||
|
const token = getAuthToken();
|
||||||
|
|
||||||
console.log(`DUMMY API Client: Requesting ${config.method || "GET"} ${url}`);
|
// Construct URL
|
||||||
|
let url = `${API_BASE_URL}${endpoint}`;
|
||||||
|
if (params) {
|
||||||
|
const queryParams = new URLSearchParams();
|
||||||
|
Object.entries(params).forEach(([key, value]) => {
|
||||||
|
if (value !== undefined && value !== null) {
|
||||||
|
// Ensure value exists
|
||||||
|
queryParams.append(key, String(value));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const queryString = queryParams.toString();
|
||||||
|
if (queryString) {
|
||||||
|
url += `?${queryString}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Simulate network delay
|
// Prepare headers
|
||||||
await new Promise((resolve) => setTimeout(resolve, Math.random() * 300 + 100));
|
const headers = new Headers(customHeaders);
|
||||||
|
|
||||||
|
if (!skipContentType && body && !(body instanceof FormData)) {
|
||||||
|
if (!headers.has("Content-Type")) {
|
||||||
|
headers.set("Content-Type", "application/json");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token && !headers.has("Authorization")) {
|
||||||
|
headers.set("Authorization", `Bearer ${token}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare body - Stringify JSON unless it's FormData
|
||||||
|
let processedBody = body;
|
||||||
|
if (body && headers.get("Content-Type") === "application/json" && typeof body !== "string") {
|
||||||
|
processedBody = JSON.stringify(body);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// --- Simulate API Responses ---
|
const response = await fetch(url, {
|
||||||
// You can add more sophisticated simulation based on the endpoint
|
...fetchOptions,
|
||||||
if (endpoint.includes("error")) {
|
headers,
|
||||||
console.warn(`DUMMY API Client: Simulating error for ${url}`);
|
body: processedBody,
|
||||||
return { success: false, error: "Simulated server error" };
|
});
|
||||||
|
|
||||||
|
// Check for successful response
|
||||||
|
if (!response.ok) {
|
||||||
|
let errorData = { message: `HTTP error! status: ${response.status} ${response.statusText}` };
|
||||||
|
try {
|
||||||
|
// Try to parse specific error details from the API response
|
||||||
|
const jsonError = await response.json();
|
||||||
|
errorData = { ...errorData, ...jsonError };
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore if the error response is not JSON
|
||||||
|
}
|
||||||
|
console.error("API Error:", errorData);
|
||||||
|
throw new Error(errorData.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.method === "POST" || config.method === "PUT" || config.method === "DELETE") {
|
// Handle responses with no content (e.g., 204 No Content)
|
||||||
// Simulate simple success for modification requests
|
if (response.status === 204) {
|
||||||
console.log(`DUMMY API Client: Simulating success for ${config.method} ${url}`);
|
// For 204, there's no body, return undefined or null as appropriate for T
|
||||||
return { success: true, data: { message: "Operation successful (simulated)" } as T };
|
// Using 'as T' assumes the caller expects undefined/null for no content
|
||||||
|
return undefined as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simulate success for GET requests (return empty array or specific data based on endpoint)
|
// Parse the JSON response body for other successful responses
|
||||||
console.log(`DUMMY API Client: Simulating success for GET ${url}`);
|
const data: T = await response.json();
|
||||||
let responseData: any = [];
|
return data;
|
||||||
if (endpoint.includes("/map/pois")) responseData = []; // Let feature api add dummy data
|
|
||||||
if (endpoint.includes("/properties")) responseData = []; // Example
|
|
||||||
// Add more specific endpoint data simulation if needed
|
|
||||||
|
|
||||||
return { success: true, data: responseData as T };
|
|
||||||
|
|
||||||
// --- Real Fetch Logic (keep commented for dummy) ---
|
|
||||||
// const response = await fetch(url, config);
|
|
||||||
//
|
|
||||||
// if (!response.ok) {
|
|
||||||
// let errorData;
|
|
||||||
// try {
|
|
||||||
// errorData = await response.json();
|
|
||||||
// } catch (e) {
|
|
||||||
// errorData = { detail: response.statusText || "Unknown error" };
|
|
||||||
// }
|
|
||||||
// const errorMessage = errorData?.detail || `HTTP error ${response.status}`;
|
|
||||||
// console.error(`API Error (${response.status}) for ${url}:`, errorMessage);
|
|
||||||
// return { success: false, error: errorMessage, details: errorData };
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Handle cases with no content
|
|
||||||
// if (response.status === 204) {
|
|
||||||
// return { success: true, data: null as T };
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// const data: T = await response.json();
|
|
||||||
// return { success: true, data };
|
|
||||||
// --- End Real Fetch Logic ---
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Network or other error for ${url}:`, error);
|
console.error("API Client Fetch Error:", error);
|
||||||
const errorMessage = error instanceof Error ? error.message : "Network error or invalid response";
|
// Re-throw the error for handling by the calling code (e.g., React Query, component)
|
||||||
return { success: false, error: errorMessage };
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Convenience Methods ---
|
// --- Specific HTTP Method Helpers ---
|
||||||
const apiClient = {
|
|
||||||
get: <T = any>(endpoint: string, options?: RequestInit) => fetchApi<T>(endpoint, { ...options, method: "GET" }),
|
|
||||||
|
|
||||||
post: <T = any>(endpoint: string, body: any, options?: RequestInit) =>
|
export const api = {
|
||||||
fetchApi<T>(endpoint, { ...options, method: "POST", body: JSON.stringify(body) }),
|
get: <T>(endpoint: string, options?: FetchOptions) => apiClient<T>(endpoint, { ...options, method: "GET" }),
|
||||||
|
|
||||||
put: <T = any>(endpoint: string, body: any, options?: RequestInit) =>
|
post: <T>(endpoint: string, body: any, options?: FetchOptions) =>
|
||||||
fetchApi<T>(endpoint, { ...options, method: "PUT", body: JSON.stringify(body) }),
|
apiClient<T>(endpoint, { ...options, method: "POST", body }),
|
||||||
|
|
||||||
delete: <T = any>(endpoint: string, options?: RequestInit) => fetchApi<T>(endpoint, { ...options, method: "DELETE" }),
|
put: <T>(endpoint: string, body: any, options?: FetchOptions) =>
|
||||||
|
apiClient<T>(endpoint, { ...options, method: "PUT", body }),
|
||||||
|
|
||||||
patch: <T = any>(endpoint: string, body: any, options?: RequestInit) =>
|
patch: <T>(endpoint: string, body: any, options?: FetchOptions) =>
|
||||||
fetchApi<T>(endpoint, { ...options, method: "PATCH", body: JSON.stringify(body) }),
|
apiClient<T>(endpoint, { ...options, method: "PATCH", body }),
|
||||||
|
|
||||||
|
delete: <T>(endpoint: string, options?: FetchOptions) => apiClient<T>(endpoint, { ...options, method: "DELETE" }),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default apiClient;
|
export default api;
|
||||||
|
|||||||
@ -1,41 +1,175 @@
|
|||||||
/*
|
/* === src/types/api.ts === */
|
||||||
========================================
|
/**
|
||||||
File: frontend/types/api.ts (NEW - Dummy Shared Types)
|
* General API Response Types
|
||||||
========================================
|
*/
|
||||||
*/
|
export interface PaginatedResponse<T> {
|
||||||
|
count: number;
|
||||||
/** Generic API Response Structure */
|
next: string | null;
|
||||||
export type APIResponse<T> = { success: true; data: T } | { success: false; error: string; details?: any };
|
previous: string | null;
|
||||||
|
results: T[];
|
||||||
/** Represents a Point of Interest (can be property, cafe, etc.) */
|
|
||||||
export interface PointOfInterest {
|
|
||||||
id: string;
|
|
||||||
lat: number;
|
|
||||||
lng: number;
|
|
||||||
name: string;
|
|
||||||
type: string; // e.g., 'property', 'cafe', 'park', 'station'
|
|
||||||
price?: number; // Optional: for properties
|
|
||||||
rating?: number; // Optional: for amenities
|
|
||||||
// Add other relevant shared fields
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Basic Property Information */
|
export interface ApiErrorResponse {
|
||||||
export interface PropertySummary {
|
message: string;
|
||||||
id: string;
|
details?: Record<string, any> | string[]; // Can be an object or array of strings
|
||||||
address: string;
|
code?: string; // Optional error code
|
||||||
lat: number;
|
}
|
||||||
lng: number;
|
|
||||||
|
/**
|
||||||
|
* Represents a generic successful API operation, possibly with no specific data.
|
||||||
|
*/
|
||||||
|
export interface SuccessResponse {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Property Data Types
|
||||||
|
*/
|
||||||
|
export type PropertyType = "Condominium" | "House" | "Townhouse" | "Land" | "Apartment" | "Other";
|
||||||
|
export type OwnershipType = "Freehold" | "Leasehold";
|
||||||
|
export type FurnishingStatus = "Unfurnished" | "Partly Furnished" | "Fully Furnished";
|
||||||
|
|
||||||
|
export interface PriceHistoryEntry {
|
||||||
|
date: string; // Consider using Date object after parsing
|
||||||
price: number;
|
price: number;
|
||||||
type: string; // e.g., 'Condo', 'House'
|
|
||||||
size?: number; // sqm
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** User representation */
|
export interface MarketTrendData {
|
||||||
export interface User {
|
areaGrowthPercentage: number;
|
||||||
|
similarPropertiesCount: number;
|
||||||
|
averagePrice: number;
|
||||||
|
averagePricePerSqm: number;
|
||||||
|
priceTrend?: "Rising" | "Falling" | "Stable"; // Optional trend indicator
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EnvironmentalFactor {
|
||||||
|
type: "Flood Risk" | "Air Quality" | "Noise Level";
|
||||||
|
level: "Low" | "Moderate" | "High" | "Good" | "Fair" | "Poor"; // Adjust levels as needed
|
||||||
|
details?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NearbyFacility {
|
||||||
|
name: string;
|
||||||
|
type: "Transport" | "Shopping" | "Park" | "Hospital" | "School" | "Other";
|
||||||
|
distanceMeters: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Property {
|
||||||
id: string;
|
id: string;
|
||||||
username: string;
|
title: string;
|
||||||
email: string;
|
description: string;
|
||||||
// Add roles or other relevant fields
|
price: number;
|
||||||
|
currency?: string; // e.g., 'THB', 'USD'
|
||||||
|
location: {
|
||||||
|
address: string;
|
||||||
|
city: string;
|
||||||
|
district?: string;
|
||||||
|
postalCode?: string;
|
||||||
|
latitude?: number;
|
||||||
|
longitude?: number;
|
||||||
|
};
|
||||||
|
bedrooms: number;
|
||||||
|
bathrooms: number;
|
||||||
|
areaSqm: number;
|
||||||
|
propertyType: PropertyType;
|
||||||
|
images: string[]; // Array of image URLs
|
||||||
|
yearBuilt?: number;
|
||||||
|
floorLevel?: number;
|
||||||
|
totalFloors?: number;
|
||||||
|
parkingSpaces?: number;
|
||||||
|
furnishing: FurnishingStatus;
|
||||||
|
ownership: OwnershipType;
|
||||||
|
availabilityDate?: string; // Consider using Date object
|
||||||
|
isPremium: boolean;
|
||||||
|
features: string[];
|
||||||
|
amenities: string[];
|
||||||
|
priceHistory?: PriceHistoryEntry[];
|
||||||
|
marketTrends?: MarketTrendData;
|
||||||
|
environmentalFactors?: EnvironmentalFactor[];
|
||||||
|
nearbyFacilities?: NearbyFacility[];
|
||||||
|
agent?: {
|
||||||
|
// Optional agent info
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
contact: string;
|
||||||
|
};
|
||||||
|
dataSource?: string; // Origin of the data
|
||||||
|
createdAt: string; // ISO 8601 date string
|
||||||
|
updatedAt: string; // ISO 8601 date string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add other globally shared types (e.g., PipelineStatus, DataSourceType if needed FE side)
|
/**
|
||||||
|
* Data Pipeline Types
|
||||||
|
*/
|
||||||
|
export type PipelineStatus = "active" | "paused" | "error" | "idle" | "running";
|
||||||
|
export type SourceType = "Website" | "API" | "File Upload" | "Database";
|
||||||
|
|
||||||
|
export interface DataSource {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
type: SourceType;
|
||||||
|
url?: string; // For Website/API
|
||||||
|
lastUpdated: string;
|
||||||
|
recordCount: number;
|
||||||
|
status: "connected" | "error" | "pending";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DataPipeline {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
status: PipelineStatus;
|
||||||
|
lastRunAt: string | null;
|
||||||
|
nextRunAt: string | null;
|
||||||
|
runFrequency: string; // e.g., "Daily", "Hourly", "Manual"
|
||||||
|
sources: DataSource[];
|
||||||
|
totalRecords: number;
|
||||||
|
aiPowered: boolean;
|
||||||
|
errorDetails?: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model Types
|
||||||
|
*/
|
||||||
|
export type ModelType = "Regression" | "Neural Network" | "Geospatial" | "Time Series" | "Ensemble" | "Classification";
|
||||||
|
export type ModelStatus = "active" | "inactive" | "training" | "error" | "pending";
|
||||||
|
|
||||||
|
export interface Model {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
type: ModelType;
|
||||||
|
version: string;
|
||||||
|
status: ModelStatus;
|
||||||
|
isSystemModel: boolean; // Distinguishes system models from custom ones
|
||||||
|
dataSourceId?: string; // ID of the DataPipeline used for training (if custom)
|
||||||
|
dataSourceName?: string; // Name of the source for display
|
||||||
|
hyperparameters: Record<string, string | number | boolean>;
|
||||||
|
performanceMetrics?: Record<string, number>; // e.g., { accuracy: 0.92, mae: 150000 }
|
||||||
|
lastTrainedAt?: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map Related Types
|
||||||
|
*/
|
||||||
|
export interface MapLayer {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
type: "property" | "heatmap" | "environmental";
|
||||||
|
isVisible: boolean;
|
||||||
|
dataUrl?: string; // URL to fetch layer data
|
||||||
|
style?: Record<string, any>; // Mapbox style properties
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MapViewState {
|
||||||
|
longitude: number;
|
||||||
|
latitude: number;
|
||||||
|
zoom: number;
|
||||||
|
pitch?: number;
|
||||||
|
bearing?: number;
|
||||||
|
}
|
||||||
|
|||||||
@ -1,12 +1,22 @@
|
|||||||
/*
|
/* === src/types/index.ts === */
|
||||||
========================================
|
// Barrel file for exporting shared types
|
||||||
File: frontend/types/index.ts (NEW - Barrel File)
|
|
||||||
========================================
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Re-export shared types for easier importing
|
|
||||||
export * from "./api";
|
export * from "./api";
|
||||||
|
export * from "./user";
|
||||||
|
|
||||||
// You can add other shared types here or export from other files in this directory
|
// Example of another shared type definition
|
||||||
// export * from './user';
|
export interface SelectOption<T = string | number> {
|
||||||
|
value: T;
|
||||||
|
label: string;
|
||||||
|
icon?: React.ComponentType<{ className?: string }>; // Optional icon component
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generic type for UI components requiring an icon
|
||||||
|
export interface IconProps {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add other shared types as the application grows
|
||||||
// export * from './settings';
|
// export * from './settings';
|
||||||
|
// export * from './notifications';
|
||||||
|
|||||||
56
frontend/types/user.ts
Normal file
56
frontend/types/user.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
/* === src/types/user.ts === */
|
||||||
|
/**
|
||||||
|
* User Profile and Authentication Types
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface UserProfile {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
firstName?: string;
|
||||||
|
lastName?: string;
|
||||||
|
avatarUrl?: string; // URL to the user's avatar image
|
||||||
|
roles: UserRole[]; // Array of roles assigned to the user
|
||||||
|
preferences?: UserPreferences; // User-specific settings
|
||||||
|
isActive: boolean;
|
||||||
|
lastLogin?: string; // ISO 8601 date string
|
||||||
|
createdAt: string; // ISO 8601 date string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserRole = "admin" | "analyst" | "viewer" | "data_manager"; // Example roles
|
||||||
|
|
||||||
|
export interface UserPreferences {
|
||||||
|
theme?: "light" | "dark" | "system";
|
||||||
|
defaultMapLocation?: {
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
zoom: number;
|
||||||
|
};
|
||||||
|
notifications?: {
|
||||||
|
pipelineSuccess?: boolean;
|
||||||
|
pipelineError?: boolean;
|
||||||
|
newReports?: boolean;
|
||||||
|
};
|
||||||
|
// Add other user-specific preferences
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthState {
|
||||||
|
user: UserProfile | null;
|
||||||
|
token: string | null; // The authentication token (e.g., JWT)
|
||||||
|
isAuthenticated: boolean;
|
||||||
|
isLoading: boolean; // Tracks loading state during auth checks/login/logout
|
||||||
|
error: string | null; // Stores any authentication errors
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type for login credentials
|
||||||
|
export interface LoginCredentials {
|
||||||
|
emailOrUsername: string;
|
||||||
|
password?: string; // Password might not be needed for SSO flows
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type for the response after successful login
|
||||||
|
export interface LoginResponse {
|
||||||
|
user: UserProfile;
|
||||||
|
token: string;
|
||||||
|
refreshToken?: string; // Optional refresh token
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user