mirror of
https://github.com/borbann-platform/backend-api.git
synced 2025-12-18 12:14:05 +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);
|
||||
|
||||
@plugin "tailwindcss-animate";
|
||||
@import "tailwindcss";
|
||||
|
||||
@plugin 'tailwindcss-animate';
|
||||
|
||||
@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 {
|
||||
--color-background: hsl(var(--background));
|
||||
--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,
|
||||
so we've added these compatibility styles to make sure everything still
|
||||
|
||||
@ -1,22 +1,19 @@
|
||||
/*
|
||||
========================================
|
||||
File: frontend/app/layout.tsx
|
||||
========================================
|
||||
*/
|
||||
import type React from "react";
|
||||
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 { ThemeProvider } from "@/components/common/ThemeProvider"; // Correct path
|
||||
import { ThemeController } from "@/components/common/ThemeController"; // Correct path
|
||||
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)
|
||||
import { ThemeProvider } from "@/components/theme-provider";
|
||||
import Sidebar from "@/components/sidebar";
|
||||
|
||||
// Setup font
|
||||
const inter = Inter({ subsets: ["latin"], variable: "--font-sans" }); // Define CSS variable
|
||||
const poppins = Poppins({
|
||||
subsets: ["latin"],
|
||||
weight: ["300", "400", "500", "600", "700"],
|
||||
variable: "--font-poppins",
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Borbann - Data Platform", // More specific title
|
||||
description: "Data integration, analysis, and visualization platform.",
|
||||
title: "BorBann - Property Analytics",
|
||||
description: "Automated data integration pipeline and property analytics for real estate",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
@ -26,19 +23,12 @@ export default function RootLayout({
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
{" "}
|
||||
{/* suppressHydrationWarning needed for next-themes */}
|
||||
<body className={`${inter.variable} font-sans`}>
|
||||
{" "}
|
||||
{/* Apply font class */}
|
||||
{/* ThemeProvider should wrap everything for theme context */}
|
||||
<body className={poppins.className}>
|
||||
<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
|
||||
{/* ThemeController can wrap specific parts or the whole app */}
|
||||
{/* If placed here, it controls the base theme and color scheme */}
|
||||
<ThemeController defaultColorScheme="Blue">{children}</ThemeController>
|
||||
{/* Include Toaster components for notifications */}
|
||||
<SonnerToaster />
|
||||
<RadixToaster /> {/* Include if using the useToast hook */}
|
||||
<div className="flex h-screen">
|
||||
<Sidebar />
|
||||
<div className="flex-1 overflow-auto">{children}</div>
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</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 @@
|
||||
/*
|
||||
========================================
|
||||
File: frontend/app/page.tsx
|
||||
========================================
|
||||
*/
|
||||
import Image from "next/image";
|
||||
import Link from "next/link"; // Import Link
|
||||
import { Button } from "@/components/ui/button"; // Import common UI
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { ArrowRight, BarChart2, Building, Database, LineChart, MapPin, Zap } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function Home() {
|
||||
export default function LandingPage() {
|
||||
return (
|
||||
<div className="flex flex-col min-h-screen items-center justify-center p-8 sm:p-20 text-center">
|
||||
{/* Replace with your actual logo/branding */}
|
||||
<h1 className="text-4xl font-bold mb-4">Welcome to Borbann</h1>
|
||||
<p className="text-lg text-muted-foreground mb-8">Your data integration and analysis platform.</p>
|
||||
<div className="min-h-screen">
|
||||
{/* Hero Section */}
|
||||
<section className="bg-gradient-to-b from-background to-muted py-20">
|
||||
<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">
|
||||
Property Analytics Platform
|
||||
</Badge>
|
||||
<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>
|
||||
</Button>
|
||||
<Button size="lg" variant="outline" asChild>
|
||||
<Link href="/data-pipeline">Manage Data Pipelines</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-4 items-center">
|
||||
<Link href="/map">
|
||||
<Button size="lg">Go to Map</Button>
|
||||
</Link>
|
||||
<Link href="/documentation">
|
||||
{" "}
|
||||
{/* Example link */}
|
||||
<Button size="lg" variant="outline">
|
||||
Read Docs
|
||||
{/* 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 className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<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>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Optional: Add more introductory content or links */}
|
||||
<footer className="absolute bottom-8 text-sm text-muted-foreground">
|
||||
© {new Date().getFullYear()} Borbann Project.
|
||||
</footer>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,24 +1,26 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"style": "default",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.ts",
|
||||
"css": "app/globals.css",
|
||||
"css": "src/styles/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"@": "@/src",
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks",
|
||||
"features": "@/features",
|
||||
"types": "@/types",
|
||||
"services": "@/services"
|
||||
"services": "@/services",
|
||||
"store": "@/store",
|
||||
"types": "@/types"
|
||||
},
|
||||
"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 @@
|
||||
/*
|
||||
========================================
|
||||
File: frontend/hooks/use-mobile.tsx
|
||||
========================================
|
||||
*/
|
||||
import * as React from "react";
|
||||
import * as React from "react"
|
||||
|
||||
const MOBILE_BREAKPOINT = 768; // Standard md breakpoint
|
||||
const MOBILE_BREAKPOINT = 768
|
||||
|
||||
export function useIsMobile(): boolean {
|
||||
// Initialize state based on current window size (or undefined if SSR)
|
||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
|
||||
typeof window !== "undefined" ? window.innerWidth < MOBILE_BREAKPOINT : undefined
|
||||
);
|
||||
export function useIsMobile() {
|
||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
|
||||
|
||||
React.useEffect(() => {
|
||||
// Ensure this runs only client-side
|
||||
if (typeof window === "undefined") {
|
||||
return;
|
||||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
|
||||
const onChange = () => {
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||
}
|
||||
mql.addEventListener("change", onChange)
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||
return () => mql.removeEventListener("change", onChange)
|
||||
}, [])
|
||||
|
||||
const handleResize = () => {
|
||||
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;
|
||||
return !!isMobile
|
||||
}
|
||||
|
||||
@ -1,106 +1,106 @@
|
||||
/*
|
||||
========================================
|
||||
File: frontend/hooks/use-toast.ts
|
||||
========================================
|
||||
*/
|
||||
"use client";
|
||||
"use client"
|
||||
|
||||
// Inspired by react-hot-toast library
|
||||
import * as React from "react";
|
||||
// Import types from the actual Toast component location
|
||||
import type { ToastActionElement, ToastProps } from "@/components/ui/toast";
|
||||
import * as React from "react"
|
||||
|
||||
const TOAST_LIMIT = 1; // Show only one toast at a time
|
||||
const TOAST_REMOVE_DELAY = 1000000; // A very long time (effectively manual dismiss only)
|
||||
import type {
|
||||
ToastActionElement,
|
||||
ToastProps,
|
||||
} from "@/components/ui/toast"
|
||||
|
||||
const TOAST_LIMIT = 1
|
||||
const TOAST_REMOVE_DELAY = 1000000
|
||||
|
||||
type ToasterToast = ToastProps & {
|
||||
id: string;
|
||||
title?: React.ReactNode;
|
||||
description?: React.ReactNode;
|
||||
action?: ToastActionElement;
|
||||
};
|
||||
id: string
|
||||
title?: React.ReactNode
|
||||
description?: React.ReactNode
|
||||
action?: ToastActionElement
|
||||
}
|
||||
|
||||
const actionTypes = {
|
||||
ADD_TOAST: "ADD_TOAST",
|
||||
UPDATE_TOAST: "UPDATE_TOAST",
|
||||
DISMISS_TOAST: "DISMISS_TOAST",
|
||||
REMOVE_TOAST: "REMOVE_TOAST",
|
||||
} as const;
|
||||
} as const
|
||||
|
||||
let count = 0;
|
||||
let count = 0
|
||||
|
||||
function genId() {
|
||||
count = (count + 1) % Number.MAX_SAFE_INTEGER;
|
||||
return count.toString();
|
||||
count = (count + 1) % Number.MAX_SAFE_INTEGER
|
||||
return count.toString()
|
||||
}
|
||||
|
||||
type ActionType = typeof actionTypes;
|
||||
type ActionType = typeof actionTypes
|
||||
|
||||
type Action =
|
||||
| {
|
||||
type: ActionType["ADD_TOAST"];
|
||||
toast: ToasterToast;
|
||||
type: ActionType["ADD_TOAST"]
|
||||
toast: ToasterToast
|
||||
}
|
||||
| {
|
||||
type: ActionType["UPDATE_TOAST"];
|
||||
toast: Partial<ToasterToast>;
|
||||
type: ActionType["UPDATE_TOAST"]
|
||||
toast: Partial<ToasterToast>
|
||||
}
|
||||
| {
|
||||
type: ActionType["DISMISS_TOAST"];
|
||||
toastId?: ToasterToast["id"];
|
||||
type: ActionType["DISMISS_TOAST"]
|
||||
toastId?: ToasterToast["id"]
|
||||
}
|
||||
| {
|
||||
type: ActionType["REMOVE_TOAST"];
|
||||
toastId?: ToasterToast["id"];
|
||||
};
|
||||
type: ActionType["REMOVE_TOAST"]
|
||||
toastId?: ToasterToast["id"]
|
||||
}
|
||||
|
||||
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) => {
|
||||
if (toastTimeouts.has(toastId)) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
toastTimeouts.delete(toastId);
|
||||
toastTimeouts.delete(toastId)
|
||||
dispatch({
|
||||
type: "REMOVE_TOAST",
|
||||
toastId: toastId,
|
||||
});
|
||||
}, TOAST_REMOVE_DELAY);
|
||||
})
|
||||
}, TOAST_REMOVE_DELAY)
|
||||
|
||||
toastTimeouts.set(toastId, timeout);
|
||||
};
|
||||
toastTimeouts.set(toastId, timeout)
|
||||
}
|
||||
|
||||
export const reducer = (state: State, action: Action): State => {
|
||||
switch (action.type) {
|
||||
case "ADD_TOAST":
|
||||
return {
|
||||
...state,
|
||||
// Slice ensures the limit is enforced
|
||||
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
||||
};
|
||||
}
|
||||
|
||||
case "UPDATE_TOAST":
|
||||
return {
|
||||
...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": {
|
||||
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) {
|
||||
addToRemoveQueue(toastId);
|
||||
addToRemoveQueue(toastId)
|
||||
} else {
|
||||
state.toasts.forEach((toast) => {
|
||||
addToRemoveQueue(toast.id);
|
||||
});
|
||||
addToRemoveQueue(toast.id)
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
@ -109,48 +109,48 @@ export const reducer = (state: State, action: Action): State => {
|
||||
t.id === toastId || toastId === undefined
|
||||
? {
|
||||
...t,
|
||||
open: false, // Trigger the close animation
|
||||
open: false,
|
||||
}
|
||||
: t
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
case "REMOVE_TOAST":
|
||||
if (action.toastId === undefined) {
|
||||
return {
|
||||
...state,
|
||||
toasts: [], // Remove all toasts
|
||||
};
|
||||
toasts: [],
|
||||
}
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
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) {
|
||||
const id = genId();
|
||||
const id = genId()
|
||||
|
||||
const update = (props: ToasterToast) =>
|
||||
dispatch({
|
||||
type: "UPDATE_TOAST",
|
||||
toast: { ...props, id },
|
||||
});
|
||||
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
|
||||
})
|
||||
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
|
||||
|
||||
dispatch({
|
||||
type: "ADD_TOAST",
|
||||
@ -159,37 +159,36 @@ function toast({ ...props }: Toast) {
|
||||
id,
|
||||
open: true,
|
||||
onOpenChange: (open) => {
|
||||
if (!open) dismiss(); // Ensure dismiss is called when the toast closes itself
|
||||
if (!open) dismiss()
|
||||
},
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
return {
|
||||
id: id,
|
||||
dismiss,
|
||||
update,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function useToast() {
|
||||
const [state, setState] = React.useState<State>(memoryState);
|
||||
const [state, setState] = React.useState<State>(memoryState)
|
||||
|
||||
React.useEffect(() => {
|
||||
listeners.push(setState);
|
||||
listeners.push(setState)
|
||||
return () => {
|
||||
// Clean up listener
|
||||
const index = listeners.indexOf(setState);
|
||||
const index = listeners.indexOf(setState)
|
||||
if (index > -1) {
|
||||
listeners.splice(index, 1);
|
||||
listeners.splice(index, 1)
|
||||
}
|
||||
};
|
||||
}, [state]); // Only re-subscribe if state instance changes (it shouldn't)
|
||||
}
|
||||
}, [state])
|
||||
|
||||
return {
|
||||
...state,
|
||||
toast,
|
||||
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 @@
|
||||
/*
|
||||
========================================
|
||||
File: frontend/services/apiClient.ts (NEW - Dummy)
|
||||
========================================
|
||||
*/
|
||||
import type { APIResponse } from "@/types/api"; // Import shared response type
|
||||
/* === src/services/apiClient.ts === */
|
||||
/**
|
||||
* API Client - Dummy Implementation
|
||||
*
|
||||
* This provides a basic structure for making API calls.
|
||||
* - 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 ---
|
||||
// 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";
|
||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "/api/v1"; // Example API base URL
|
||||
|
||||
/**
|
||||
* Base function for making API requests.
|
||||
* Includes dummy authorization header.
|
||||
* Retrieves the authentication token.
|
||||
* 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 url = `${process.env.NEXT_PUBLIC_API_URL || "/api/v1"}${endpoint}`;
|
||||
const getAuthToken = (): string | null => {
|
||||
// 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 = {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: DUMMY_AUTH_TOKEN, // Add dummy token here
|
||||
};
|
||||
interface FetchOptions extends RequestInit {
|
||||
params?: Record<string, string | number | boolean>; // For query parameters
|
||||
/** If true, Content-Type header will not be set (e.g., for FormData) */
|
||||
skipContentType?: boolean;
|
||||
}
|
||||
|
||||
const config: RequestInit = {
|
||||
...options,
|
||||
headers: {
|
||||
...defaultHeaders,
|
||||
...options.headers,
|
||||
},
|
||||
};
|
||||
/**
|
||||
* Generic fetch function for API calls.
|
||||
* @template T The expected response type.
|
||||
* @param {string} endpoint The API endpoint (e.g., '/users').
|
||||
* @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
|
||||
await new Promise((resolve) => setTimeout(resolve, Math.random() * 300 + 100));
|
||||
// Prepare headers
|
||||
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 {
|
||||
// --- Simulate API Responses ---
|
||||
// You can add more sophisticated simulation based on the endpoint
|
||||
if (endpoint.includes("error")) {
|
||||
console.warn(`DUMMY API Client: Simulating error for ${url}`);
|
||||
return { success: false, error: "Simulated server error" };
|
||||
const response = await fetch(url, {
|
||||
...fetchOptions,
|
||||
headers,
|
||||
body: processedBody,
|
||||
});
|
||||
|
||||
// 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") {
|
||||
// Simulate simple success for modification requests
|
||||
console.log(`DUMMY API Client: Simulating success for ${config.method} ${url}`);
|
||||
return { success: true, data: { message: "Operation successful (simulated)" } as T };
|
||||
// Handle responses with no content (e.g., 204 No Content)
|
||||
if (response.status === 204) {
|
||||
// For 204, there's no body, return undefined or null as appropriate for 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)
|
||||
console.log(`DUMMY API Client: Simulating success for GET ${url}`);
|
||||
let responseData: any = [];
|
||||
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 ---
|
||||
// Parse the JSON response body for other successful responses
|
||||
const data: T = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error(`Network or other error for ${url}:`, error);
|
||||
const errorMessage = error instanceof Error ? error.message : "Network error or invalid response";
|
||||
return { success: false, error: errorMessage };
|
||||
console.error("API Client Fetch Error:", error);
|
||||
// Re-throw the error for handling by the calling code (e.g., React Query, component)
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Convenience Methods ---
|
||||
const apiClient = {
|
||||
get: <T = any>(endpoint: string, options?: RequestInit) => fetchApi<T>(endpoint, { ...options, method: "GET" }),
|
||||
// --- Specific HTTP Method Helpers ---
|
||||
|
||||
post: <T = any>(endpoint: string, body: any, options?: RequestInit) =>
|
||||
fetchApi<T>(endpoint, { ...options, method: "POST", body: JSON.stringify(body) }),
|
||||
export const api = {
|
||||
get: <T>(endpoint: string, options?: FetchOptions) => apiClient<T>(endpoint, { ...options, method: "GET" }),
|
||||
|
||||
put: <T = any>(endpoint: string, body: any, options?: RequestInit) =>
|
||||
fetchApi<T>(endpoint, { ...options, method: "PUT", body: JSON.stringify(body) }),
|
||||
post: <T>(endpoint: string, body: any, options?: FetchOptions) =>
|
||||
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) =>
|
||||
fetchApi<T>(endpoint, { ...options, method: "PATCH", body: JSON.stringify(body) }),
|
||||
patch: <T>(endpoint: string, body: any, options?: FetchOptions) =>
|
||||
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 @@
|
||||
/*
|
||||
========================================
|
||||
File: frontend/types/api.ts (NEW - Dummy Shared Types)
|
||||
========================================
|
||||
*/
|
||||
|
||||
/** Generic API Response Structure */
|
||||
export type APIResponse<T> = { success: true; data: T } | { success: false; error: string; details?: any };
|
||||
|
||||
/** 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
|
||||
/* === src/types/api.ts === */
|
||||
/**
|
||||
* General API Response Types
|
||||
*/
|
||||
export interface PaginatedResponse<T> {
|
||||
count: number;
|
||||
next: string | null;
|
||||
previous: string | null;
|
||||
results: T[];
|
||||
}
|
||||
|
||||
/** Basic Property Information */
|
||||
export interface PropertySummary {
|
||||
id: string;
|
||||
address: string;
|
||||
lat: number;
|
||||
lng: number;
|
||||
export interface ApiErrorResponse {
|
||||
message: string;
|
||||
details?: Record<string, any> | string[]; // Can be an object or array of strings
|
||||
code?: string; // Optional error code
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
type: string; // e.g., 'Condo', 'House'
|
||||
size?: number; // sqm
|
||||
}
|
||||
|
||||
/** User representation */
|
||||
export interface User {
|
||||
export interface MarketTrendData {
|
||||
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;
|
||||
username: string;
|
||||
email: string;
|
||||
// Add roles or other relevant fields
|
||||
title: string;
|
||||
description: string;
|
||||
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 @@
|
||||
/*
|
||||
========================================
|
||||
File: frontend/types/index.ts (NEW - Barrel File)
|
||||
========================================
|
||||
*/
|
||||
/* === src/types/index.ts === */
|
||||
// Barrel file for exporting shared types
|
||||
|
||||
// Re-export shared types for easier importing
|
||||
export * from "./api";
|
||||
export * from "./user";
|
||||
|
||||
// You can add other shared types here or export from other files in this directory
|
||||
// export * from './user';
|
||||
// Example of another shared type definition
|
||||
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 './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