Merge pull request #2 from Sosokker/develop

ui: add pipeline UI scaffold
This commit is contained in:
Sirin Puenggun 2025-05-14 14:59:03 +07:00 committed by GitHub
commit 2073befd68
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
50 changed files with 3645 additions and 2336 deletions

View File

@ -1,407 +0,0 @@
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&#10;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>
)
}

View File

@ -1,242 +0,0 @@
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>
}
}

View File

@ -1,687 +0,0 @@
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>
)
}

View File

@ -1,654 +0,0 @@
"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,
RefreshCw,
} from "lucide-react"
import Link from "next/link"
import { TopNavigation } from "@/components/navigation/top-navigation"
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 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 */}
<TopNavigation />
{/* 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>
)
}

View File

@ -0,0 +1,126 @@
"use client";
import { AnimatePresence, motion } from "framer-motion";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { AddDataSource } from "@/components/pipeline/add-data-source";
import { PipelineAiAssistant } from "@/components/pipeline/ai-assistant";
import { PipelineDetails } from "@/components/pipeline/details";
import { ScheduleAndInformation } from "@/components/pipeline/schedule-and-information";
import { PipelineSummary } from "@/components/pipeline/summary";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Form } from "@/components/ui/form";
import { cn } from "@/lib/utils";
import { pipelineSchema, PipelineFormValues } from "@/lib/validations/pipeline";
import { zodResolver } from "@hookform/resolvers/zod";
const TOTAL_STEPS = 5;
const stepComponents = [
<PipelineDetails key="details" />,
<AddDataSource key="datasource" />,
<PipelineAiAssistant key="ai" />,
<ScheduleAndInformation key="schedule" />,
<PipelineSummary key="summary" />,
,
];
const motionVariants = {
enter: { opacity: 0, x: 50 },
center: { opacity: 1, x: 0 },
exit: { opacity: 0, x: -50 },
};
export default function CratePipelineForm() {
const [step, setStep] = useState(0);
const isFirstStep = step === 0;
const isLastStep = step === TOTAL_STEPS - 1;
const form = useForm<PipelineFormValues>({
resolver: zodResolver(pipelineSchema),
});
const { handleSubmit, reset } = form;
const onSubmit = async (formData: unknown) => {
if (!isLastStep) return setStep((s) => s + 1);
console.log(formData);
reset();
setStep(0);
toast.success("Form successfully submitted");
};
const handleBack = () => setStep((s) => (s > 0 ? s - 1 : s));
const StepForm = (
<Form {...form}>
<form onSubmit={handleSubmit(onSubmit)} className="grid gap-y-4">
{stepComponents[step]}
<div className="flex justify-between">
<Button
type="button"
size="sm"
className="font-medium cursor-pointer"
onClick={handleBack}
disabled={isFirstStep}
>
Back
</Button>
<Button
type="submit"
size="sm"
className="font-medium cursor-pointer"
>
{isLastStep ? "Create Pipeline" : "Next"}
</Button>
</div>
</form>
</Form>
);
return (
<div className="space-y-4">
{/* stepper */}
<div className="flex items-center justify-center">
{Array.from({ length: TOTAL_STEPS }).map((_, index) => (
<div key={index} className="flex items-center">
<div
className={cn(
"w-4 h-4 rounded-full transition-all duration-300 ease-in-out",
index <= step ? "bg-primary" : "bg-primary/30"
)}
/>
{index < TOTAL_STEPS - 1 && (
<div
className={cn(
"w-8 h-0.5",
index < step ? "bg-primary" : "bg-primary/30"
)}
/>
)}
</div>
))}
</div>
{/* animated form */}
<Card className="shadow-sm">
<CardContent>
<AnimatePresence mode="wait">
<motion.div
key={step}
variants={motionVariants}
initial="enter"
animate="center"
exit="exit"
transition={{ duration: 0.1 }}
>
{StepForm}
</motion.div>
</AnimatePresence>
</CardContent>
</Card>
</div>
);
}

View File

@ -0,0 +1,24 @@
import { Button } from "@/components/ui/button";
import { ArrowLeft } from "lucide-react";
import Link from "next/link";
import CratePipelineForm from "./create-pipeline-multiform";
export default function CreatePipelinePage() {
return (
<div className="container mx-auto p-6">
<div className="mt-6">
<Link href="/data-pipeline">
<Button variant="outline" className="mb-6 cursor-pointer">
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Pipelines
</Button>
</Link>
<CratePipelineForm />
<div className="mt-6 flex justify-end space-x-4">
{/* <Button variant="outline">Save as Draft</Button>
<Button>Create Pipeline</Button> */}
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,159 @@
"use client";
import PageHeader from "@/components/page-header";
import { PipelineCard } from "@/components/pipeline/card";
import { Button } from "@/components/ui/button";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Pipeline } from "@/lib/api/pipelines/types";
import { Plus } from "lucide-react";
import Link from "next/link";
import { useEffect, useState } from "react";
export default function DataPipelinePage() {
const [pipelines, setPipelines] = useState<Pipeline[]>([]);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchPipelines = async () => {
try {
// const data = await listPipelines();
// setPipelines(data);
} catch (err) {
console.error("Error fetching pipelines:", err);
setError("Failed to load pipelines");
}
};
fetchPipelines();
}, []);
return (
<div className="container mx-auto p-6">
{error && <p className="text-red-500">{error}</p>}
<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 cursor-pointer">
<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
name="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
name="Rental Market Data"
description="Collects rental prices and availability"
status="active"
lastRun="Yesterday"
nextRun="In 3 days"
sources={2}
records={830}
aiPowered={true}
/>
<PipelineCard
name="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
name="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
name="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}
/>
{/* mock pipeline card with data */}
<PipelineCard
name="Rental Market Data"
description="Collects rental prices and availability"
status="active"
lastRun="Yesterday"
nextRun="In 3 days"
sources={2}
records={830}
aiPowered={true}
/>
<PipelineCard
name="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
name="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>
);
}

View File

@ -0,0 +1,105 @@
import { Button } from "@/components/ui/button";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { ArrowLeft, Edit, Play, Trash, Copy } from "lucide-react";
import Link from "next/link";
import PageHeader from "@/components/page-header";
import { PipelineStatus } from "@/components/pipeline/status";
import { PipelineDataSource } from "@/components/pipeline/data-source";
import { PipelineExportData } from "@/components/pipeline/export-data";
import { PipelineDataSchema } from "@/components/pipeline/data-schema";
import { PipelineDataPreview } from "@/components/pipeline/data-preview";
import { PipelineOutputConfig } from "@/components/pipeline/output-config";
import { PipelineRunHistory } from "@/components/pipeline/run-history";
import { PipelineSettings } from "@/components/pipeline/settings";
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">
<PipelineStatus />
<PipelineDataSource />
<PipelineExportData />
</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">
<PipelineDataSchema />
</TabsContent>
<TabsContent value="preview" className="mt-4">
<PipelineDataPreview />
</TabsContent>
<TabsContent value="output" className="mt-4">
<PipelineOutputConfig />
</TabsContent>
<TabsContent value="history" className="mt-4">
<PipelineRunHistory />
</TabsContent>
<TabsContent value="settings" className="mt-4">
<PipelineSettings />
</TabsContent>
</Tabs>
</div>
</div>
);
}

View File

@ -0,0 +1,16 @@
"use client";
import Sidebar from "@/components/sidebar";
export default function AppLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<div className="flex h-screen">
<Sidebar />
<div className="flex-1 overflow-auto">{children}</div>
</div>
);
}

View File

@ -0,0 +1,186 @@
"use client";
import { AnalyticsPanel } from "@/components/map/analytics-panel";
import { ChatPanel } from "@/components/map/chat-panel";
import { FiltersPanel } from "@/components/map/filters-panel";
import { PropertyInfoPanel } from "@/components/map/property-info-panel";
import MapWithSearch from "@/components/map/map-with-search";
import { TopNavigation } from "@/components/navigation/top-navigation";
import { Button } from "@/components/ui/button";
import { BarChart2, Filter, MessageCircle } from "lucide-react";
import { useRef, useState } from "react";
import Draggable from "react-draggable";
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 analyticsRef = useRef<HTMLDivElement>(null);
const filtersRef = useRef<HTMLDivElement>(null);
const chatRef = useRef<HTMLDivElement>(null);
const propertyInfoRef = useRef<HTMLDivElement>(null);
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">
<div>
<div className="absolute inset-0 flex items-center justify-center">
<MapWithSearch />
</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 */}
<TopNavigation />
{/* 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) {
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) {
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 && (
<Draggable nodeRef={propertyInfoRef as React.RefObject<HTMLElement>}>
<div ref={propertyInfoRef}>
<PropertyInfoPanel setShowPropertyInfo={setShowPropertyInfo} />
</div>
</Draggable>
)}
{/* Analytics Panel */}
{showAnalytics && (
<Draggable nodeRef={analyticsRef as React.RefObject<HTMLElement>}>
<div ref={analyticsRef}>
<AnalyticsPanel setShowAnalytics={setShowAnalytics} />
</div>
</Draggable>
)}
{showFilters && (
<Draggable nodeRef={filtersRef as React.RefObject<HTMLElement>}>
<div ref={filtersRef}>
<FiltersPanel setShowFilters={setShowFilters} />
</div>
</Draggable>
)}
{showChat && (
<Draggable nodeRef={chatRef as React.RefObject<HTMLElement>}>
<div ref={chatRef}>
<ChatPanel setShowChat={setShowChat} />
</div>
</Draggable>
)}
{/* 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>
);
}

View File

@ -1,31 +1,32 @@
"use client";
import { useState } from "react";
// import { ModelCard } from "@/components/models/model-card";
import PageHeader from "@/components/page-header";
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 {
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 { Switch } from "@/components/ui/switch";
import { Progress } from "@/components/ui/progress";
import { Switch } from "@/components/ui/switch";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Textarea } from "@/components/ui/textarea";
import { useModelState } from "@/store/model-store";
import {
AlertTriangle,
BrainCircuit,
Clock,
Check,
Database,
Play,
Plus,
Settings,
Sliders,
Trash2,
AlertTriangle,
Check,
ArrowRight,
Info,
} from "lucide-react";
import Link from "next/link";
import PageHeader from "@/components/page-header";
import { useState } from "react";
import { useShallow } from "zustand/react/shallow";
export default function ModelsPage() {
const [activeTab, setActiveTab] = useState("my-models");
@ -34,85 +35,36 @@ export default function ModelsPage() {
const [isTraining, setIsTraining] = useState(false);
const [modelName, setModelName] = useState("");
const [modelDescription, setModelDescription] = useState("");
const { models } = useModelState(
useShallow((state) => ({
models: state.models,
}))
);
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: "pipeline-1",
name: "Property Listings",
records: 1240,
lastUpdated: "2 hours ago",
},
{
id: "model-2",
name: "Enhanced Neural Network v1.8",
type: "Neural Network",
hyperparameters: {
layers: "4",
neurons: "128,64,32,16",
dropout: "0.2",
id: "pipeline-2",
name: "Rental Market Data",
records: 830,
lastUpdated: "Yesterday",
},
dataSource: "System Base Model",
status: "active",
{
id: "pipeline-3",
name: "Price Comparison",
records: 1560,
lastUpdated: "2 days ago",
},
{
id: "pipeline-4",
name: "Commercial Properties",
records: 450,
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,
},
];
@ -146,7 +98,11 @@ export default function ModelsPage() {
]}
/>
<Tabs defaultValue="my-models" className="mt-6" onValueChange={setActiveTab}>
<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>
@ -155,7 +111,10 @@ export default function ModelsPage() {
</TabsList>
{activeTab !== "train-model" && (
<Button onClick={() => setActiveTab("train-model")} className="gap-2">
<Button
onClick={() => setActiveTab("train-model")}
className="gap-2"
>
<Plus className="h-4 w-4" />
Train New Model
</Button>
@ -164,35 +123,34 @@ export default function ModelsPage() {
<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} />
))}
{/* {models} */}
</div>
{models.filter((model) => !model.isSystem).length === 0 && (
{models && (
<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>
<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.
Train your first custom model to get started with personalized
property predictions.
</p>
<Button onClick={() => setActiveTab("train-model")}>Train Your First Model</Button>
<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 className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{models.map((model) => (
<ModelCard model={model} />
))}
</div>
</div> */}
</TabsContent>
<TabsContent value="train-model">
@ -215,7 +173,9 @@ export default function ModelsPage() {
</div>
<div className="space-y-2">
<Label htmlFor="model-description">Description (Optional)</Label>
<Label htmlFor="model-description">
Description (Optional)
</Label>
<Textarea
id="model-description"
placeholder="Describe the purpose of this model..."
@ -237,28 +197,42 @@ export default function ModelsPage() {
</div>
<div className="pt-2">
<h3 className="text-sm font-medium mb-2">Advanced Settings</h3>
<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>
<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>
<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>
<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>
@ -272,7 +246,9 @@ export default function ModelsPage() {
<Card className="mb-6">
<CardHeader>
<CardTitle>Select Data Source</CardTitle>
<CardDescription>Choose a data pipeline to train your model</CardDescription>
<CardDescription>
Choose a data pipeline to train your model
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
@ -280,20 +256,26 @@ export default function ModelsPage() {
<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"
selectedPipeline === pipeline.id
? "border-primary bg-primary/5"
: "hover:border-primary/50"
}`}
onClick={() => setSelectedPipeline(pipeline.id)}>
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}
{pipeline.records.toLocaleString()} records
Updated {pipeline.lastUpdated}
</p>
</div>
</div>
{selectedPipeline === pipeline.id && <Check className="h-5 w-5 text-primary" />}
{selectedPipeline === pipeline.id && (
<Check className="h-5 w-5 text-primary" />
)}
</div>
</div>
))}
@ -304,7 +286,9 @@ export default function ModelsPage() {
<Card>
<CardHeader>
<CardTitle>Training Process</CardTitle>
<CardDescription>Monitor and control the training process</CardDescription>
<CardDescription>
Monitor and control the training process
</CardDescription>
</CardHeader>
<CardContent>
{isTraining ? (
@ -335,7 +319,11 @@ export default function ModelsPage() {
</div>
{trainingProgress < 100 && (
<Button variant="outline" className="w-full" onClick={() => setIsTraining(false)}>
<Button
variant="outline"
className="w-full"
onClick={() => setIsTraining(false)}
>
Cancel Training
</Button>
)}
@ -363,7 +351,9 @@ export default function ModelsPage() {
<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>
<p className="text-sm text-muted-foreground">
Configure your settings and start training
</p>
</div>
</div>
@ -383,13 +373,18 @@ export default function ModelsPage() {
</div>
<div className="flex gap-2">
<Button variant="outline" className="flex-1" onClick={() => setActiveTab("my-models")}>
<Button
variant="outline"
className="flex-1"
onClick={() => setActiveTab("my-models")}
>
Cancel
</Button>
<Button
className="flex-1 gap-2"
onClick={handleStartTraining}
disabled={!selectedPipeline || !modelName}>
disabled={!selectedPipeline || !modelName}
>
<Play className="h-4 w-4" />
Start Training
</Button>
@ -405,96 +400,3 @@ export default function ModelsPage() {
</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>
);
}

View File

@ -1,6 +1,7 @@
@import "tailwindcss";
@plugin 'tailwindcss-animate';
@plugin 'tailwind-scrollbar-hide';
@custom-variant dark (&:is(.dark *));

View File

@ -3,7 +3,6 @@ import type { Metadata } from "next";
import { Poppins } from "next/font/google";
import "./globals.css";
import { ThemeProvider } from "@/components/theme-provider";
import Sidebar from "@/components/sidebar";
const poppins = Poppins({
subsets: ["latin"],
@ -26,7 +25,6 @@ export default function RootLayout({
<body className={poppins.className}>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
<div className="flex h-screen">
<Sidebar />
<div className="flex-1 overflow-auto">{children}</div>
</div>
</ThemeProvider>

View File

@ -0,0 +1,195 @@
import { useModelState } from "@/store/model-store";
import {
BarChart2,
Clock,
Droplets,
LineChart,
Link,
MessageCircle,
Newspaper,
RefreshCw,
Wind,
X,
} from "lucide-react";
import { useShallow } from "zustand/react/shallow";
import { Badge } from "../ui/badge";
import { Button } from "../ui/button";
import { Card, CardContent } from "../ui/card";
export function AnalyticsPanel({
setShowAnalytics,
}: {
setShowAnalytics: (show: boolean) => void;
}) {
const { selectedModel, setSelectedModel } = useModelState(
useShallow((state) => ({
selectedModel: state.selectedModel,
setSelectedModel: state.setSelectedModel,
models: state.models,
}))
);
return (
<div className="absolute top-20 right-4 w-96 max-h-[800px] bg-background p-5 rounded-md overflow-y-auto z-20 map-overlay overflow-hidden scrollbar-hide cursor-grab">
<div className="map-overlay-header flex w-full">
<div className="flex justify-between w-full items-center gap-2">
<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>
<div className="map-overlay-content">
<div className="flex items-center justify-between">
<p className="text-sm text-muted-foreground">
Information in radius will be analyzed
</p>
</div>
<div className="mt-2 mb-2">
<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>
);
}

View File

@ -0,0 +1,97 @@
import { MessageCircle, Send, X } from "lucide-react";
import { useState } from "react";
import { Button } from "../ui/button";
export function ChatPanel({
setShowChat,
}: {
setShowChat: (show: boolean) => void;
}) {
const [message, setMessage] = useState("");
const [messages, setMessages] = useState([
{ role: "assistant", content: "Hi! How can I help you today?" },
]);
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("");
}
};
return (
<div className="absolute top-20 right-4 w-96 h-[500px] map-overlay z-20 flex flex-col bg-background p-5 rounded-md scrollbar-hide cursor-grab">
<div className="map-overlay-header">
<div className="flex justify-between w-full items-center gap-2">
<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>
<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>
);
}

View File

@ -0,0 +1,175 @@
import { Filter, X } from "lucide-react";
import { useState } from "react";
import { Button } from "../ui/button";
import { Slider } from "../ui/slider";
import { Switch } from "../ui/switch";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs";
export function FiltersPanel({
setShowFilters,
}: {
setShowFilters: (show: boolean) => void;
}) {
const [activeTab, setActiveTab] = useState("basic");
const [priceRange, setPriceRange] = useState([5000000, 20000000]);
const [radius, setRadius] = useState(30);
return (
<div className="absolute top-20 right-4 w-96 map-overlay z-20 bg-background p-5 rounded-md overflow-y-auto scrollbar-hide cursor-grab">
<div className="map-overlay-header flex w-full">
<div className="flex justify-between w-full items-center gap-2">
<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>
</div>
<Tabs
defaultValue="basic"
value={activeTab}
onValueChange={setActiveTab}
className="mt-4"
>
<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>
);
}

View File

@ -0,0 +1,111 @@
"use client";
import { useEffect, useRef, useState } from "react";
import { Loader } from "@googlemaps/js-api-loader";
import { Input } from "../ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "../ui/select";
declare global {
interface Window {
google: typeof google;
}
}
const GOOGLE_MAPS_API_KEY = process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY!;
export default function MapWithSearch() {
const mapRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
const mapInstanceRef = useRef<google.maps.Map | null>(null);
const [mapType, setMapType] = useState<string>("roadmap");
useEffect(() => {
const loader = new Loader({
apiKey: GOOGLE_MAPS_API_KEY,
version: "weekly",
libraries: ["places"],
});
loader.load().then(() => {
if (!mapRef.current || !inputRef.current) return;
const map = new google.maps.Map(mapRef.current, {
center: { lat: 13.7563, lng: 100.5018 },
zoom: 13,
gestureHandling: "greedy",
mapTypeControl: false,
mapTypeId: mapType,
});
mapInstanceRef.current = map;
const input = inputRef.current;
const searchBox = new google.maps.places.SearchBox(input);
map.addListener("bounds_changed", () => {
searchBox.setBounds(map.getBounds()!);
});
let marker: google.maps.Marker;
searchBox.addListener("places_changed", () => {
const places = searchBox.getPlaces();
if (!places || places.length === 0) return;
const place = places[0];
if (!place.geometry || !place.geometry.location) return;
map.panTo(place.geometry.location);
map.setZoom(15);
if (marker) marker.setMap(null);
marker = new google.maps.Marker({
map,
position: place.geometry.location,
});
});
});
}, []);
useEffect(() => {
if (mapInstanceRef.current) {
mapInstanceRef.current.setMapTypeId(mapType as google.maps.MapTypeId);
}
}, [mapType]);
return (
<div className="relative flex flex-row w-full h-full">
<div className="absolute mt-18 z-50 flex gap-2 rounded-md shadow-md">
<Input
ref={inputRef}
type="text"
placeholder="Search locations..."
className="w-[300px]"
/>
<Select
onValueChange={(value) => setMapType(value as google.maps.MapTypeId)}
defaultValue="roadmap"
>
<SelectTrigger className="w-[140px]">
<SelectValue placeholder="Map Type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="roadmap">Roadmap</SelectItem>
<SelectItem value="satellite">Satellite</SelectItem>
<SelectItem value="hybrid">Hybrid</SelectItem>
<SelectItem value="terrain">Terrain</SelectItem>
</SelectContent>
</Select>
</div>
<div ref={mapRef} className="w-full h-full rounded-md" />
</div>
);
}

View File

@ -0,0 +1,134 @@
import {
Bath,
BedDouble,
Building,
Droplets,
Home,
Link,
MapPin,
Star,
Sun,
Wind,
X,
} from "lucide-react";
import { Badge } from "../ui/badge";
import { Button } from "../ui/button";
export function PropertyInfoPanel({
setShowPropertyInfo,
}: {
setShowPropertyInfo: (show: boolean) => void;
}) {
return (
<div className="absolute top-20 right-4 w-96 map-overlay z-20 bg-background p-5 rounded-md overflow-y-auto scrollbar-hide cursor-grab">
<div className="map-overlay-header">
<div className="flex justify-between w-full items-center gap-2">
<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>
<div className="map-overlay-content mt-2">
<div className="relative mb-4">
<img
src="/map.png?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>
);
}

View File

@ -0,0 +1,128 @@
// import {
// ArrowRight,
// Clock,
// Database,
// Info,
// Link,
// Settings,
// Sliders,
// Trash2,
// } from "lucide-react";
// import { Badge } from "../ui/badge";
// import { Button } from "../ui/button";
// import {
// Card,
// CardContent,
// CardDescription,
// CardFooter,
// CardHeader,
// CardTitle,
// } from "../ui/card";
// interface ModelCardProps {
// model: {
// id: string;
// name: string;
// type: string;
// // hyperparameters: {
// // [key: string]: string;
// // };
// // dataSource: string;
// status: string;
// // lastUpdated: string;
// // isSystem: boolean;
// };
// }
// export function ModelCard({ model }: ModelCardProps) {
// return (
// <Card>
// <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>
// <span title="This is a pre-trained system model">
// <Info className="h-4 w-4 text-muted-foreground cursor-help" />
// </span>
// </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>
// );
// }

View File

@ -14,17 +14,16 @@ import {
} from "lucide-react";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { useTopNavigationStore } from "@/store/top-navgation-store";
import { useModelState } from "@/store/model-store";
import { useShallow } from "zustand/react/shallow";
export function TopNavigation() {
const { selectedModel, setSelectedModel, models } = useTopNavigationStore(
useShallow(
(state) => ({
const { selectedModel, setSelectedModel, models } = useModelState(
useShallow((state) => ({
selectedModel: state.selectedModel,
setSelectedModel: state.setSelectedModel,
models: state.models,
})),
}))
);
return (
@ -33,18 +32,6 @@ export function TopNavigation() {
<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>

View File

@ -0,0 +1,210 @@
import { DatabaseIcon, FileUp, Globe, Plus, Trash2 } from "lucide-react";
import { useState } from "react";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "../ui/accordion";
import { Badge } from "../ui/badge";
import { Button } from "../ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "../ui/card";
import { Input } from "../ui/input";
import { Label } from "../ui/label";
import { Textarea } from "../ui/textarea";
type SourceType = "website" | "file" | "api";
type Source = {
id: string;
type: SourceType;
};
export function AddDataSource() {
const [sources, setSources] = useState<Source[]>([
{ id: "source-1", type: "website" },
{ id: "source-2", type: "file" },
{ id: "source-3", type: "api" },
]);
const addSource = (type: SourceType) => {
const newId = `source-${Date.now()}`;
setSources((prev) => [...prev, { id: newId, type }]);
};
const removeSource = (id: string) => {
setSources((prev) => prev.filter((source) => source.id !== id));
};
const renderSourceItem = (source: Source) => {
const commonProps = {
className: "border rounded-md mb-4 data-source-card",
value: source.id,
};
return (
<AccordionItem key={source.id} {...commonProps}>
<AccordionTrigger className="px-4">
<div className="flex items-center">
{source.type === "website" && (
<Globe className="mr-2 h-5 w-5 text-primary" />
)}
{source.type === "file" && (
<FileUp className="mr-2 h-5 w-5 text-primary" />
)}
{source.type === "api" && (
<DatabaseIcon className="mr-2 h-5 w-5 text-primary" />
)}
<span>{`${
source.type.charAt(0).toUpperCase() + source.type.slice(1)
} Source`}</span>
</div>
</AccordionTrigger>
<AccordionContent className="px-4 pb-4">
{source.type === "website" && (
<div className="space-y-4">
<div className="space-y-2">
<Label>Website URL</Label>
<Input placeholder="https://example.com/listings" />
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label>Additional URLs (optional)</Label>
<Badge variant="outline" className="text-xs">
Pattern Detection
</Badge>
</div>
<Textarea
placeholder="https://example.com/page2&#10;https://example.com/page3"
rows={3}
/>
</div>
<div className="flex justify-end">
<Button
variant="outline"
size="sm"
className="text-destructive"
onClick={() => removeSource(source.id)}
>
<Trash2 className="mr-2 h-4 w-4" />
Remove Source
</Button>
</div>
</div>
)}
{source.type === "file" && (
<div className="space-y-4">
<div className="space-y-2">
<Label>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>
<Input type="file" className="mt-2 cursor-pointer" />
</div>
</div>
</div>
<div className="flex justify-end">
<Button
variant="outline"
size="sm"
className="text-destructive"
onClick={() => removeSource(source.id)}
>
<Trash2 className="mr-2 h-4 w-4" />
Remove Source
</Button>
</div>
</div>
)}
{source.type === "api" && (
<div className="space-y-4">
<div className="space-y-2">
<Label>API Endpoint URL</Label>
<Input placeholder="https://api.example.com/data" />
</div>
<div className="space-y-2">
<Label>Authentication Type</Label>
<select className="w-full border rounded-md px-3 py-2 text-sm">
<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"
onClick={() => removeSource(source.id)}
>
<Trash2 className="mr-2 h-4 w-4" />
Remove Source
</Button>
</div>
</div>
)}
</AccordionContent>
</AccordionItem>
);
};
return (
<Card className="border-0 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={sources[0]?.id}
>
{sources.map(renderSourceItem)}
</Accordion>
<div className="flex flex-col gap-2 mt-4">
<Button
variant="outline"
type="button"
className="w-full justify-start gap-2"
onClick={() => addSource("website")}
>
<Plus className="h-4 w-4" />
Add Website Source
</Button>
<Button
variant="outline"
type="button"
className="w-full justify-start gap-2"
onClick={() => addSource("file")}
>
<Plus className="h-4 w-4" />
Add File Upload Source
</Button>
<Button
variant="outline"
type="button"
className="w-full justify-start gap-2"
onClick={() => addSource("api")}
>
<Plus className="h-4 w-4" />
Add API Source
</Button>
</div>
</CardContent>
</Card>
);
}

View File

@ -0,0 +1,77 @@
import { useFormContext } from "react-hook-form";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "../ui/card";
import { Label } from "../ui/label";
import { Textarea } from "../ui/textarea";
export function PipelineAiAssistant() {
const {
handleSubmit,
reset,
register,
formState: { errors },
} = useFormContext();
return (
<Card className="mt-6 border-0 hover:border-highlight-border transition-all duration-200">
<CardHeader>
<CardTitle>AI Assistant</CardTitle>
<CardDescription>Customize how AI processes your data</CardDescription>
</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"
{...register("aiPrompt")}
/>
<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>
);
}

View File

@ -0,0 +1,22 @@
import { Badge } from "@/components/ui/badge";
export 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>;
}
}

View File

@ -0,0 +1,130 @@
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
AlertTriangle,
Clock,
Copy,
Database,
Pause,
Play,
RefreshCw,
} from "lucide-react";
import Link from "next/link";
import { StatusBadge } from "./badge";
interface PipelineCardProps {
name: string;
description: string;
status: "active" | "paused" | "error";
lastRun: string;
nextRun: string;
sources: number;
records: number;
error?: string;
aiPowered?: boolean;
}
export function PipelineCard({
name,
description,
status,
lastRun,
nextRun,
sources,
records,
error,
}: 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">{name}</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/${name.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>
);
}

View File

@ -0,0 +1,76 @@
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
} from "../ui/card";
export function PipelineDataPreview() {
return (
<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>
);
}

View File

@ -0,0 +1,246 @@
import { Plus } from "lucide-react";
import { Button } from "../ui/button";
import { Label } from "../ui/label";
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
} from "../ui/card";
import { Input } from "../ui/input";
import { Badge } from "../ui/badge";
export function PipelineDataSchema() {
return (
<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>
);
}

View File

@ -0,0 +1,48 @@
import { Badge } from "../ui/badge";
import { Card, CardHeader, CardTitle, CardContent } from "../ui/card";
export function PipelineDataSource() {
return (
<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>
);
}

View File

@ -0,0 +1,81 @@
"use client";
import { useFormContext } from "react-hook-form";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "../ui/card";
import { Input } from "../ui/input";
import { Label } from "../ui/label";
import { Textarea } from "../ui/textarea";
export function PipelineDetails() {
const {
handleSubmit,
reset,
register,
formState: { errors },
} = useFormContext();
return (
<Card className="border-0 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"
{...register("name")}
/>
</div>
{errors.name && (
<p className="text-sm text-destructive mt-1">
{typeof errors.name?.message === "string"
? errors.name.message
: ""}
</p>
)}
<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}
{...register("description")}
/>
</div>
{errors.description && (
<p className="text-sm text-destructive mt-1">
{typeof errors.description?.message === "string"
? errors.description.message
: ""}
</p>
)}
<div className="space-y-2">
<Label htmlFor="tags">Tags (optional)</Label>
<Input
id="tags"
placeholder="e.g., real-estate, properties, listings"
{...register("tags")}
/>
<p className="text-xs text-muted-foreground mt-1">
Separate tags with commas
</p>
</div>
</div>
</CardContent>
</Card>
);
}

View File

@ -0,0 +1,119 @@
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { Download } from "lucide-react";
import { Button } from "../ui/button";
import { Card, CardHeader, CardTitle, CardContent } from "../ui/card";
export function PipelineExportData() {
return (
<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>
);
}

View File

@ -0,0 +1,96 @@
import { Check, Download } from "lucide-react";
import { Badge } from "../ui/badge";
import { Button } from "../ui/button";
import { Label } from "../ui/label";
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
} from "../ui/card";
export function PipelineOutputConfig() {
return (
<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>
);
}

View File

@ -0,0 +1,109 @@
import { Badge } from "../ui/badge";
import { Button } from "../ui/button";
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
} from "../ui/card";
export function PipelineRunHistory() {
return (
<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>
);
}

View File

@ -0,0 +1,167 @@
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
export function ScheduleAndInformation() {
return (
<Card className="mt-6 border-0 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>
);
}

View File

@ -0,0 +1,100 @@
import { Button } from "../ui/button";
import { Label } from "../ui/label";
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
} from "../ui/card";
import { Input } from "../ui/input";
export function PipelineSettings() {
return (
<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>
);
}

View File

@ -0,0 +1,41 @@
import { Badge } from "../ui/badge";
import { Card, CardHeader, CardTitle, CardContent } from "../ui/card";
export function PipelineStatus() {
return (
<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>
);
}

View File

@ -0,0 +1,118 @@
"use client";
import { Badge } from "@/components/ui/badge";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { useFormContext } from "react-hook-form";
export const PipelineSummary = () => {
const { getValues } = useFormContext();
const values = getValues();
const tags = values.tags
? values.tags
.split(",")
.map((tag: string) => tag.trim()) // trim each tag
.filter(Boolean) // filter out empty strings
: [];
return (
<Card className="mt-6 border-0 hover:border-highlight-border transition-all duration-200">
<CardHeader>
<CardTitle>Pipeline Summary</CardTitle>
<CardDescription>
A quick overview of the pipeline configuration. Review before
launching.
</CardDescription>
</CardHeader>
<CardContent className="space-y-6 pt-6">
{/* details */}
<section>
<h3 className="text-xl font-semibold">Pipeline Details</h3>
<div className="space-y-2">
<div>
<strong>Name:</strong> {values.name || "—"}
</div>
<div>
<strong>Description:</strong> {values.description || "—"}
</div>
<div>
<strong>Tags:</strong>
<div className="flex flex-wrap gap-2 mt-2">
{tags.length > 0 ? (
tags.map((tag: string, index: number) => (
<Badge key={index} variant="outline">
{tag}
</Badge>
))
) : (
<div className="text-sm text-muted-foreground">
No tags added
</div>
)}
</div>
</div>
</div>
</section>
{/* data sources */}
<section>
<h3 className="text-xl font-semibold">Data Sources</h3>
{values.dataSources && values.dataSources.length > 0 ? (
<ul className="list-disc pl-6 space-y-1">
{values.dataSources.map((src: string, index: number) => (
<li key={index}>{src}</li>
))}
</ul>
) : (
<div className="text-sm text-muted-foreground">
No data sources added.
</div>
)}
</section>
{/* AI Assistant */}
<section>
<h3 className="text-xl font-semibold">AI Assistant</h3>
<div className="space-y-2">
<div>
<strong>Prompt:</strong> {values.aiPrompt || "—"}
</div>
<div>
<strong>Mode:</strong> {values.aiMode || "Default"}
</div>
</div>
</section>
{/* schedule */}
<section>
<h3 className="text-xl font-semibold">Schedule</h3>
<div className="space-y-2">
<div>
<strong>Frequency:</strong> {values.schedule || "—"}
</div>
<div>
<strong>Start Date:</strong> {values.startDate || "—"}
</div>
<div>
<strong>Timezone:</strong> {values.timezone || "—"}
</div>
</div>
</section>
{/* form Inputs */}
<section>
<h3 className="text-xl font-semibold">Form Inputs</h3>
<div className="text-sm text-muted-foreground">
No input fields added yet.
</div>
</section>
</CardContent>
</Card>
);
};

View File

@ -2,10 +2,32 @@
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
const Collapsible = CollapsiblePrimitive.Root
function Collapsible({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
}
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
function CollapsibleTrigger({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
return (
<CollapsiblePrimitive.CollapsibleTrigger
data-slot="collapsible-trigger"
{...props}
/>
)
}
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
function CollapsibleContent({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
return (
<CollapsiblePrimitive.CollapsibleContent
data-slot="collapsible-content"
{...props}
/>
)
}
export { Collapsible, CollapsibleTrigger, CollapsibleContent }

View File

@ -0,0 +1,109 @@
import { Pipeline, PipelineCreate, Run } from "./types";
const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL;
// if (typeof window !== "undefined") {
// console.log(API_BASE);
// }
// utility for handling fetch responses
async function handleResponse<T>(res: Response): Promise<T> {
if (!res.ok) {
const errorBody = await res.json();
throw new Error(JSON.stringify(errorBody));
}
return res.json();
}
// GET /pipelines
export async function listPipelines(): Promise<Pipeline[]> {
const res = await fetch(`${API_BASE}/pipelines`);
return handleResponse<Pipeline[]>(res);
}
// POST /pipelines
export async function createPipeline(
payload: PipelineCreate
): Promise<Pipeline> {
const res = await fetch(`${API_BASE}/pipelines`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
return handleResponse<Pipeline>(res);
}
// GET /pipelines/{pipeline_id}
export async function getPipeline(pipeline_id: string): Promise<Pipeline> {
const res = await fetch(`${API_BASE}/pipelines/${pipeline_id}`);
return handleResponse<Pipeline>(res);
}
// POST /pipelines/{pipeline_id}/run
export async function runPipeline(pipeline_id: string): Promise<Run> {
const res = await fetch(`${API_BASE}/pipelines/${pipeline_id}/run`, {
method: "POST",
});
return handleResponse<Run>(res);
}
// GET /pipelines/{pipeline_id}/runs
export async function listRuns(pipeline_id: string): Promise<Run[]> {
const res = await fetch(`${API_BASE}/pipelines/${pipeline_id}/runs`);
return handleResponse<Run[]>(res);
}
// GET /pipelines/{pipeline_id}/runs/{run_id}
export async function getRun(
pipeline_id: string,
run_id: string
): Promise<Run> {
const res = await fetch(
`${API_BASE}/pipelines/${pipeline_id}/runs/${run_id}`
);
return handleResponse<Run>(res);
}
// GET /pipelines/{pipeline_id}/runs/{run_id}/results
export async function getRunResults(
pipeline_id: string,
run_id: string
): Promise<any[]> {
const res = await fetch(
`${API_BASE}/pipelines/${pipeline_id}/runs/${run_id}/results`
);
return handleResponse<any[]>(res);
}
// GET /pipelines/{pipeline_id}/runs/{run_id}/error
export async function getRunError(
pipeline_id: string,
run_id: string
): Promise<string> {
const res = await fetch(
`${API_BASE}/pipelines/${pipeline_id}/runs/${run_id}/error`
);
return handleResponse<string>(res);
}
// SSE: /pipelines/{pipeline_id}/runs/{run_id}/logs/stream
export function streamLogs(
pipeline_id: string,
run_id: string,
onMessage: (data: string) => void,
onError?: (event: Event) => void
): EventSource {
const url = `${API_BASE}/pipelines/${pipeline_id}/runs/${run_id}/logs/stream`;
const eventSource = new EventSource(url);
eventSource.onmessage = (event) => {
onMessage(event.data);
};
eventSource.onerror = (event) => {
if (onError) onError(event);
eventSource.close();
};
return eventSource;
}

View File

@ -0,0 +1,64 @@
// types.ts
export interface ApiConfig {
url: string;
token?: string | null;
}
export interface ApiSource {
type: "api";
config: ApiConfig;
}
export interface FileConfig {
path: string;
format?: "csv" | "json" | "sqlite";
}
export interface FileSource {
type: "file";
config: FileConfig;
}
export interface ScrapeConfig {
urls: string[];
schema_file?: string | null;
prompt?: string | null;
}
export interface ScrapeSource {
type: "scrape";
config: ScrapeConfig;
}
export type DataSource = ApiSource | FileSource | ScrapeSource;
export interface Pipeline {
id: string;
name?: string | null;
sources: DataSource[];
created_at: string;
}
export interface PipelineCreate {
name?: string | null;
sources: DataSource[];
}
export interface Run {
id: string;
pipeline_id: string;
status: "PENDING" | "RUNNING" | "COMPLETED" | "FAILED";
started_at: string;
finished_at?: string | null;
}
export interface ValidationError {
loc: (string | number)[];
msg: string;
type: string;
}
export interface HTTPValidationError {
detail: ValidationError[];
}

View File

@ -0,0 +1,10 @@
import { z } from "zod";
export const pipelineSchema = z.object({
name: z.string().min(1, "Pipeline name is required"),
description: z.string().min(1, "Description is required"),
aiPrompt: z.string().optional(),
tags: z.string().optional(),
});
export type PipelineFormValues = z.infer<typeof pipelineSchema>;

View File

@ -9,6 +9,7 @@
"lint": "next lint"
},
"dependencies": {
"@googlemaps/js-api-loader": "^1.16.8",
"@hookform/resolvers": "^3.9.1",
"@radix-ui/react-accordion": "^1.2.3",
"@radix-ui/react-alert-dialog": "^1.1.6",
@ -46,6 +47,7 @@
"cmdk": "1.1.1",
"date-fns": "4.1.0",
"embla-carousel-react": "8.5.1",
"framer-motion": "^12.9.1",
"input-otp": "1.4.1",
"lucide-react": "^0.487.0",
"next": "15.2.1",
@ -53,12 +55,13 @@
"react": "19.0.0",
"react-day-picker": "8.10.1",
"react-dom": "19.0.0",
"react-draggable": "^4.4.6",
"react-hook-form": "^7.54.1",
"react-resizable-panels": "^2.1.7",
"recharts": "^2.15.2",
"sonner": "^1.7.1",
"tailwind-merge": "^3.2.0",
"tailwindcss": "^4.1.3",
"tailwind-scrollbar-hide": "^2.0.0",
"tailwindcss-animate": "^1.0.7",
"vaul": "^0.9.6",
"zod": "^3.24.1",
@ -94,12 +97,13 @@
"@radix-ui/react-toggle-group": "^1.1.1",
"@radix-ui/react-tooltip": "latest",
"@tailwindcss/postcss": "^4",
"@types/google.maps": "^3.58.1",
"@types/node": "^20",
"@types/react": "19.0.10",
"@types/react-dom": "19.0.4",
"eslint": "^9",
"eslint-config-next": "15.2.1",
"tailwindcss": "^4",
"tailwindcss": "^4.1.3",
"typescript": "^5"
},
"pnpm": {

View File

@ -1,10 +1,17 @@
lockfileVersion: '6.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
overrides:
'@types/react': 19.0.10
'@types/react-dom': 19.0.4
dependencies:
'@googlemaps/js-api-loader':
specifier: ^1.16.8
version: 1.16.8
'@hookform/resolvers':
specifier: ^3.9.1
version: 3.9.1(react-hook-form@7.54.1)
@ -116,6 +123,9 @@ dependencies:
embla-carousel-react:
specifier: 8.5.1
version: 8.5.1(react@19.0.0)
framer-motion:
specifier: ^12.9.1
version: 12.9.1(react-dom@19.0.0)(react@19.0.0)
input-otp:
specifier: 1.4.1
version: 1.4.1(react-dom@19.0.0)(react@19.0.0)
@ -137,6 +147,9 @@ dependencies:
react-dom:
specifier: 19.0.0
version: 19.0.0(react@19.0.0)
react-draggable:
specifier: ^4.4.6
version: 4.4.6(react-dom@19.0.0)(react@19.0.0)
react-hook-form:
specifier: ^7.54.1
version: 7.54.1(react@19.0.0)
@ -152,9 +165,9 @@ dependencies:
tailwind-merge:
specifier: ^3.2.0
version: 3.2.0
tailwindcss:
specifier: ^4.1.3
version: 4.1.3
tailwind-scrollbar-hide:
specifier: ^2.0.0
version: 2.0.0(tailwindcss@4.1.3)
tailwindcss-animate:
specifier: ^1.0.7
version: 1.0.7(tailwindcss@4.1.3)
@ -172,6 +185,9 @@ devDependencies:
'@eslint/eslintrc':
specifier: ^3
version: 3.0.0
'@types/google.maps':
specifier: ^3.58.1
version: 3.58.1
'@types/node':
specifier: ^20
version: 20.0.0
@ -181,6 +197,9 @@ devDependencies:
eslint-config-next:
specifier: 15.2.1
version: 15.2.1(eslint@9.0.0)(typescript@5.0.2)
tailwindcss:
specifier: ^4.1.3
version: 4.1.3
typescript:
specifier: ^5
version: 5.0.2
@ -305,6 +324,10 @@ packages:
resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==}
dev: false
/@googlemaps/js-api-loader@1.16.8:
resolution: {integrity: sha512-CROqqwfKotdO6EBjZO/gQGVTbeDps5V7Mt9+8+5Q+jTg5CRMi3Ii/L9PmV3USROrt2uWxtGzJHORmByxyo9pSQ==}
dev: false
/@hookform/resolvers@3.9.1(react-hook-form@7.54.1):
resolution: {integrity: sha512-ud2HqmGBM0P0IABqoskKWI6PEf6ZDDBZkFqe2Vnl+mTHCEHzr3ISjjZyCwTjC/qpL25JC9aIDkloQejvMeq0ug==}
peerDependencies:
@ -644,8 +667,8 @@ packages:
/@radix-ui/react-accordion@1.2.3(@types/react-dom@19.0.4)(@types/react@19.0.10)(react-dom@19.0.0)(react@19.0.0):
resolution: {integrity: sha512-RIQ15mrcvqIkDARJeERSuXSry2N8uYnxkdDetpfmalT/+0ntOXLkFOsh9iwlAsCv+qcmhZjbdJogIm6WBa6c4A==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
'@types/react': 19.0.10
'@types/react-dom': 19.0.4
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
@ -672,8 +695,8 @@ packages:
/@radix-ui/react-alert-dialog@1.1.6(@types/react-dom@19.0.4)(@types/react@19.0.10)(react-dom@19.0.0)(react@19.0.0):
resolution: {integrity: sha512-p4XnPqgej8sZAAReCAKgz1REYZEBLR8hU9Pg27wFnCWIMc8g1ccCs0FjBcy05V15VTu8pAePw/VDYeOm/uZ6yQ==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
'@types/react': 19.0.10
'@types/react-dom': 19.0.4
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
@ -697,8 +720,8 @@ packages:
/@radix-ui/react-arrow@1.1.2(@types/react-dom@19.0.4)(@types/react@19.0.10)(react-dom@19.0.0)(react@19.0.0):
resolution: {integrity: sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
'@types/react': 19.0.10
'@types/react-dom': 19.0.4
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
@ -717,8 +740,8 @@ packages:
/@radix-ui/react-aspect-ratio@1.1.2(@types/react-dom@19.0.4)(@types/react@19.0.10)(react-dom@19.0.0)(react@19.0.0):
resolution: {integrity: sha512-TaJxYoCpxJ7vfEkv2PTNox/6zzmpKXT6ewvCuf2tTOIVN45/Jahhlld29Yw4pciOXS2Xq91/rSGEdmEnUWZCqA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
'@types/react': 19.0.10
'@types/react-dom': 19.0.4
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
@ -737,8 +760,8 @@ packages:
/@radix-ui/react-avatar@1.1.3(@types/react-dom@19.0.4)(@types/react@19.0.10)(react-dom@19.0.0)(react@19.0.0):
resolution: {integrity: sha512-Paen00T4P8L8gd9bNsRMw7Cbaz85oxiv+hzomsRZgFm2byltPFDtfcoqlWJ8GyZlIBWgLssJlzLCnKU0G0302g==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
'@types/react': 19.0.10
'@types/react-dom': 19.0.4
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
@ -760,8 +783,8 @@ packages:
/@radix-ui/react-checkbox@1.1.4(@types/react-dom@19.0.4)(@types/react@19.0.10)(react-dom@19.0.0)(react@19.0.0):
resolution: {integrity: sha512-wP0CPAHq+P5I4INKe3hJrIa1WoNqqrejzW+zoU0rOvo1b9gDEJJFl2rYfO1PYJUQCc2H1WZxIJmyv9BS8i5fLw==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
'@types/react': 19.0.10
'@types/react-dom': 19.0.4
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
@ -787,8 +810,8 @@ packages:
/@radix-ui/react-collapsible@1.1.3(@types/react-dom@19.0.4)(@types/react@19.0.10)(react-dom@19.0.0)(react@19.0.0):
resolution: {integrity: sha512-jFSerheto1X03MUC0g6R7LedNW9EEGWdg9W1+MlpkMLwGkgkbUXLPBH/KIuWKXUoeYRVY11llqbTBDzuLg7qrw==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
'@types/react': 19.0.10
'@types/react-dom': 19.0.4
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
@ -814,8 +837,8 @@ packages:
/@radix-ui/react-collection@1.1.2(@types/react-dom@19.0.4)(@types/react@19.0.10)(react-dom@19.0.0)(react@19.0.0):
resolution: {integrity: sha512-9z54IEKRxIa9VityapoEYMuByaG42iSy1ZXlY2KcuLSEtq8x4987/N6m15ppoMffgZX72gER2uHe1D9Y6Unlcw==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
'@types/react': 19.0.10
'@types/react-dom': 19.0.4
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
@ -837,7 +860,7 @@ packages:
/@radix-ui/react-compose-refs@1.1.1(@types/react@19.0.10)(react@19.0.0):
resolution: {integrity: sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==}
peerDependencies:
'@types/react': '*'
'@types/react': 19.0.10
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
@ -850,8 +873,8 @@ packages:
/@radix-ui/react-context-menu@2.2.6(@types/react-dom@19.0.4)(@types/react@19.0.10)(react-dom@19.0.0)(react@19.0.0):
resolution: {integrity: sha512-aUP99QZ3VU84NPsHeaFt4cQUNgJqFsLLOt/RbbWXszZ6MP0DpDyjkFZORr4RpAEx3sUBk+Kc8h13yGtC5Qw8dg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
'@types/react': 19.0.10
'@types/react-dom': 19.0.4
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
@ -875,7 +898,7 @@ packages:
/@radix-ui/react-context@1.1.1(@types/react@19.0.10)(react@19.0.0):
resolution: {integrity: sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==}
peerDependencies:
'@types/react': '*'
'@types/react': 19.0.10
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
@ -888,8 +911,8 @@ packages:
/@radix-ui/react-dialog@1.1.6(@types/react-dom@19.0.4)(@types/react@19.0.10)(react-dom@19.0.0)(react@19.0.0):
resolution: {integrity: sha512-/IVhJV5AceX620DUJ4uYVMymzsipdKBzo3edo+omeskCKGm9FRHM0ebIdbPnlQVJqyuHbuBltQUOG2mOTq2IYw==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
'@types/react': 19.0.10
'@types/react-dom': 19.0.4
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
@ -921,7 +944,7 @@ packages:
/@radix-ui/react-direction@1.1.0(@types/react@19.0.10)(react@19.0.0):
resolution: {integrity: sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==}
peerDependencies:
'@types/react': '*'
'@types/react': 19.0.10
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
@ -934,8 +957,8 @@ packages:
/@radix-ui/react-dismissable-layer@1.1.5(@types/react-dom@19.0.4)(@types/react@19.0.10)(react-dom@19.0.0)(react@19.0.0):
resolution: {integrity: sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
'@types/react': 19.0.10
'@types/react-dom': 19.0.4
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
@ -958,8 +981,8 @@ packages:
/@radix-ui/react-dropdown-menu@2.1.6(@types/react-dom@19.0.4)(@types/react@19.0.10)(react-dom@19.0.0)(react@19.0.0):
resolution: {integrity: sha512-no3X7V5fD487wab/ZYSHXq3H37u4NVeLDKI/Ks724X/eEFSSEFYZxWgsIlr1UBeEyDaM29HM5x9p1Nv8DuTYPA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
'@types/react': 19.0.10
'@types/react-dom': 19.0.4
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
@ -984,7 +1007,7 @@ packages:
/@radix-ui/react-focus-guards@1.1.1(@types/react@19.0.10)(react@19.0.0):
resolution: {integrity: sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==}
peerDependencies:
'@types/react': '*'
'@types/react': 19.0.10
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
@ -997,8 +1020,8 @@ packages:
/@radix-ui/react-focus-scope@1.1.2(@types/react-dom@19.0.4)(@types/react@19.0.10)(react-dom@19.0.0)(react@19.0.0):
resolution: {integrity: sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
'@types/react': 19.0.10
'@types/react-dom': 19.0.4
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
@ -1019,8 +1042,8 @@ packages:
/@radix-ui/react-hover-card@1.1.6(@types/react-dom@19.0.4)(@types/react@19.0.10)(react-dom@19.0.0)(react@19.0.0):
resolution: {integrity: sha512-E4ozl35jq0VRlrdc4dhHrNSV0JqBb4Jy73WAhBEK7JoYnQ83ED5r0Rb/XdVKw89ReAJN38N492BAPBZQ57VmqQ==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
'@types/react': 19.0.10
'@types/react-dom': 19.0.4
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
@ -1047,7 +1070,7 @@ packages:
/@radix-ui/react-id@1.1.0(@types/react@19.0.10)(react@19.0.0):
resolution: {integrity: sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==}
peerDependencies:
'@types/react': '*'
'@types/react': 19.0.10
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
@ -1061,8 +1084,8 @@ packages:
/@radix-ui/react-label@2.1.2(@types/react-dom@19.0.4)(@types/react@19.0.10)(react-dom@19.0.0)(react@19.0.0):
resolution: {integrity: sha512-zo1uGMTaNlHehDyFQcDZXRJhUPDuukcnHz0/jnrup0JA6qL+AFpAnty+7VKa9esuU5xTblAZzTGYJKSKaBxBhw==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
'@types/react': 19.0.10
'@types/react-dom': 19.0.4
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
@ -1081,8 +1104,8 @@ packages:
/@radix-ui/react-menu@2.1.6(@types/react-dom@19.0.4)(@types/react@19.0.10)(react-dom@19.0.0)(react@19.0.0):
resolution: {integrity: sha512-tBBb5CXDJW3t2mo9WlO7r6GTmWV0F0uzHZVFmlRmYpiSK1CDU5IKojP1pm7oknpBOrFZx/YgBRW9oorPO2S/Lg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
'@types/react': 19.0.10
'@types/react-dom': 19.0.4
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
@ -1118,8 +1141,8 @@ packages:
/@radix-ui/react-menubar@1.1.6(@types/react-dom@19.0.4)(@types/react@19.0.10)(react-dom@19.0.0)(react@19.0.0):
resolution: {integrity: sha512-FHq7+3DlXwh/7FOM4i0G4bC4vPjiq89VEEvNF4VMLchGnaUuUbE5uKXMUCjdKaOghEEMeiKa5XCa2Pk4kteWmg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
'@types/react': 19.0.10
'@types/react-dom': 19.0.4
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
@ -1147,8 +1170,8 @@ packages:
/@radix-ui/react-navigation-menu@1.2.5(@types/react-dom@19.0.4)(@types/react@19.0.10)(react-dom@19.0.0)(react@19.0.0):
resolution: {integrity: sha512-myMHHQUZ3ZLTi8W381/Vu43Ia0NqakkQZ2vzynMmTUtQQ9kNkjzhOwkZC9TAM5R07OZUVIQyHC06f/9JZJpvvA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
'@types/react': 19.0.10
'@types/react-dom': 19.0.4
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
@ -1180,8 +1203,8 @@ packages:
/@radix-ui/react-popover@1.1.6(@types/react-dom@19.0.4)(@types/react@19.0.10)(react-dom@19.0.0)(react@19.0.0):
resolution: {integrity: sha512-NQouW0x4/GnkFJ/pRqsIS3rM/k97VzKnVb2jB7Gq7VEGPy5g7uNV1ykySFt7eWSp3i2uSGFwaJcvIRJBAHmmFg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
'@types/react': 19.0.10
'@types/react-dom': 19.0.4
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
@ -1214,8 +1237,8 @@ packages:
/@radix-ui/react-popper@1.2.2(@types/react-dom@19.0.4)(@types/react@19.0.10)(react-dom@19.0.0)(react@19.0.0):
resolution: {integrity: sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
'@types/react': 19.0.10
'@types/react-dom': 19.0.4
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
@ -1243,8 +1266,8 @@ packages:
/@radix-ui/react-portal@1.1.4(@types/react-dom@19.0.4)(@types/react@19.0.10)(react-dom@19.0.0)(react@19.0.0):
resolution: {integrity: sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
'@types/react': 19.0.10
'@types/react-dom': 19.0.4
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
@ -1264,8 +1287,8 @@ packages:
/@radix-ui/react-presence@1.1.2(@types/react-dom@19.0.4)(@types/react@19.0.10)(react-dom@19.0.0)(react@19.0.0):
resolution: {integrity: sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
'@types/react': 19.0.10
'@types/react-dom': 19.0.4
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
@ -1285,8 +1308,8 @@ packages:
/@radix-ui/react-primitive@2.0.2(@types/react-dom@19.0.4)(@types/react@19.0.10)(react-dom@19.0.0)(react@19.0.0):
resolution: {integrity: sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
'@types/react': 19.0.10
'@types/react-dom': 19.0.4
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
@ -1305,8 +1328,8 @@ packages:
/@radix-ui/react-progress@1.1.2(@types/react-dom@19.0.4)(@types/react@19.0.10)(react-dom@19.0.0)(react@19.0.0):
resolution: {integrity: sha512-u1IgJFQ4zNAUTjGdDL5dcl/U8ntOR6jsnhxKb5RKp5Ozwl88xKR9EqRZOe/Mk8tnx0x5tNUe2F+MzsyjqMg0MA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
'@types/react': 19.0.10
'@types/react-dom': 19.0.4
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
@ -1326,8 +1349,8 @@ packages:
/@radix-ui/react-radio-group@1.2.3(@types/react-dom@19.0.4)(@types/react@19.0.10)(react-dom@19.0.0)(react@19.0.0):
resolution: {integrity: sha512-xtCsqt8Rp09FK50ItqEqTJ7Sxanz8EM8dnkVIhJrc/wkMMomSmXHvYbhv3E7Zx4oXh98aaLt9W679SUYXg4IDA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
'@types/react': 19.0.10
'@types/react-dom': 19.0.4
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
@ -1355,8 +1378,8 @@ packages:
/@radix-ui/react-roving-focus@1.1.2(@types/react-dom@19.0.4)(@types/react@19.0.10)(react-dom@19.0.0)(react@19.0.0):
resolution: {integrity: sha512-zgMQWkNO169GtGqRvYrzb0Zf8NhMHS2DuEB/TiEmVnpr5OqPU3i8lfbxaAmC2J/KYuIQxyoQQ6DxepyXp61/xw==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
'@types/react': 19.0.10
'@types/react-dom': 19.0.4
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
@ -1383,8 +1406,8 @@ packages:
/@radix-ui/react-scroll-area@1.2.3(@types/react-dom@19.0.4)(@types/react@19.0.10)(react-dom@19.0.0)(react@19.0.0):
resolution: {integrity: sha512-l7+NNBfBYYJa9tNqVcP2AGvxdE3lmE6kFTBXdvHgUaZuy+4wGCL1Cl2AfaR7RKyimj7lZURGLwFO59k4eBnDJQ==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
'@types/react': 19.0.10
'@types/react-dom': 19.0.4
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
@ -1411,8 +1434,8 @@ packages:
/@radix-ui/react-select@2.1.6(@types/react-dom@19.0.4)(@types/react@19.0.10)(react-dom@19.0.0)(react@19.0.0):
resolution: {integrity: sha512-T6ajELxRvTuAMWH0YmRJ1qez+x4/7Nq7QIx7zJ0VK3qaEWdnWpNbEDnmWldG1zBDwqrLy5aLMUWcoGirVj5kMg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
'@types/react': 19.0.10
'@types/react-dom': 19.0.4
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
@ -1451,8 +1474,8 @@ packages:
/@radix-ui/react-separator@1.1.2(@types/react-dom@19.0.4)(@types/react@19.0.10)(react-dom@19.0.0)(react@19.0.0):
resolution: {integrity: sha512-oZfHcaAp2Y6KFBX6I5P1u7CQoy4lheCGiYj+pGFrHy8E/VNRb5E39TkTr3JrV520csPBTZjkuKFdEsjS5EUNKQ==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
'@types/react': 19.0.10
'@types/react-dom': 19.0.4
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
@ -1471,8 +1494,8 @@ packages:
/@radix-ui/react-slider@1.2.3(@types/react-dom@19.0.4)(@types/react@19.0.10)(react-dom@19.0.0)(react@19.0.0):
resolution: {integrity: sha512-nNrLAWLjGESnhqBqcCNW4w2nn7LxudyMzeB6VgdyAnFLC6kfQgnAjSL2v6UkQTnDctJBlxrmxfplWS4iYjdUTw==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
'@types/react': 19.0.10
'@types/react-dom': 19.0.4
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
@ -1501,7 +1524,7 @@ packages:
/@radix-ui/react-slot@1.1.2(@types/react@19.0.10)(react@19.0.0):
resolution: {integrity: sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==}
peerDependencies:
'@types/react': '*'
'@types/react': 19.0.10
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
@ -1515,8 +1538,8 @@ packages:
/@radix-ui/react-switch@1.1.3(@types/react-dom@19.0.4)(@types/react@19.0.10)(react-dom@19.0.0)(react@19.0.0):
resolution: {integrity: sha512-1nc+vjEOQkJVsJtWPSiISGT6OKm4SiOdjMo+/icLxo2G4vxz1GntC5MzfL4v8ey9OEfw787QCD1y3mUv0NiFEQ==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
'@types/react': 19.0.10
'@types/react-dom': 19.0.4
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
@ -1541,8 +1564,8 @@ packages:
/@radix-ui/react-tabs@1.1.3(@types/react-dom@19.0.4)(@types/react@19.0.10)(react-dom@19.0.0)(react@19.0.0):
resolution: {integrity: sha512-9mFyI30cuRDImbmFF6O2KUJdgEOsGh9Vmx9x/Dh9tOhL7BngmQPQfwW4aejKm5OHpfWIdmeV6ySyuxoOGjtNng==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
'@types/react': 19.0.10
'@types/react-dom': 19.0.4
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
@ -1568,8 +1591,8 @@ packages:
/@radix-ui/react-toast@1.2.6(@types/react-dom@19.0.4)(@types/react@19.0.10)(react-dom@19.0.0)(react@19.0.0):
resolution: {integrity: sha512-gN4dpuIVKEgpLn1z5FhzT9mYRUitbfZq9XqN/7kkBMUgFTzTG8x/KszWJugJXHcwxckY8xcKDZPz7kG3o6DsUA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
'@types/react': 19.0.10
'@types/react-dom': 19.0.4
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
@ -1599,8 +1622,8 @@ packages:
/@radix-ui/react-toggle-group@1.1.2(@types/react-dom@19.0.4)(@types/react@19.0.10)(react-dom@19.0.0)(react@19.0.0):
resolution: {integrity: sha512-JBm6s6aVG/nwuY5eadhU2zDi/IwYS0sDM5ZWb4nymv/hn3hZdkw+gENn0LP4iY1yCd7+bgJaCwueMYJIU3vk4A==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
'@types/react': 19.0.10
'@types/react-dom': 19.0.4
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
@ -1625,8 +1648,8 @@ packages:
/@radix-ui/react-toggle@1.1.2(@types/react-dom@19.0.4)(@types/react@19.0.10)(react-dom@19.0.0)(react@19.0.0):
resolution: {integrity: sha512-lntKchNWx3aCHuWKiDY+8WudiegQvBpDRAYL8dKLRvKEH8VOpl0XX6SSU/bUBqIRJbcTy4+MW06Wv8vgp10rzQ==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
'@types/react': 19.0.10
'@types/react-dom': 19.0.4
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
@ -1647,8 +1670,8 @@ packages:
/@radix-ui/react-tooltip@1.1.8(@types/react-dom@19.0.4)(@types/react@19.0.10)(react-dom@19.0.0)(react@19.0.0):
resolution: {integrity: sha512-YAA2cu48EkJZdAMHC0dqo9kialOcRStbtiY4nJPaht7Ptrhcvpo+eDChaM6BIs8kL6a8Z5l5poiqLnXcNduOkA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
'@types/react': 19.0.10
'@types/react-dom': 19.0.4
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
@ -1678,7 +1701,7 @@ packages:
/@radix-ui/react-use-callback-ref@1.1.0(@types/react@19.0.10)(react@19.0.0):
resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==}
peerDependencies:
'@types/react': '*'
'@types/react': 19.0.10
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
@ -1691,7 +1714,7 @@ packages:
/@radix-ui/react-use-controllable-state@1.1.0(@types/react@19.0.10)(react@19.0.0):
resolution: {integrity: sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==}
peerDependencies:
'@types/react': '*'
'@types/react': 19.0.10
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
@ -1705,7 +1728,7 @@ packages:
/@radix-ui/react-use-escape-keydown@1.1.0(@types/react@19.0.10)(react@19.0.0):
resolution: {integrity: sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==}
peerDependencies:
'@types/react': '*'
'@types/react': 19.0.10
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
@ -1719,7 +1742,7 @@ packages:
/@radix-ui/react-use-layout-effect@1.1.0(@types/react@19.0.10)(react@19.0.0):
resolution: {integrity: sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==}
peerDependencies:
'@types/react': '*'
'@types/react': 19.0.10
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
@ -1732,7 +1755,7 @@ packages:
/@radix-ui/react-use-previous@1.1.0(@types/react@19.0.10)(react@19.0.0):
resolution: {integrity: sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==}
peerDependencies:
'@types/react': '*'
'@types/react': 19.0.10
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
@ -1745,7 +1768,7 @@ packages:
/@radix-ui/react-use-rect@1.1.0(@types/react@19.0.10)(react@19.0.0):
resolution: {integrity: sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==}
peerDependencies:
'@types/react': '*'
'@types/react': 19.0.10
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
@ -1759,7 +1782,7 @@ packages:
/@radix-ui/react-use-size@1.1.0(@types/react@19.0.10)(react@19.0.0):
resolution: {integrity: sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==}
peerDependencies:
'@types/react': '*'
'@types/react': 19.0.10
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
@ -1773,8 +1796,8 @@ packages:
/@radix-ui/react-visually-hidden@1.1.2(@types/react-dom@19.0.4)(@types/react@19.0.10)(react-dom@19.0.0)(react@19.0.0):
resolution: {integrity: sha512-1SzA4ns2M1aRlvxErqhLHsBHoS5eI5UUcI2awAMgGUp4LoaoWOKYmvqDY2s/tltuPkh3Yk77YF/r3IRj+Amx4Q==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
'@types/react': 19.0.10
'@types/react-dom': 19.0.4
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
@ -1997,6 +2020,10 @@ packages:
resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==}
dev: false
/@types/google.maps@3.58.1:
resolution: {integrity: sha512-X9QTSvGJ0nCfMzYOnaVs/k6/4L+7F5uCS+4iUmkLEls6J9S/Phv+m/i3mDeyc49ZBgwab3EFO1HEoBY7k98EGQ==}
dev: true
/@types/json5@0.0.29:
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
dev: true
@ -2008,7 +2035,7 @@ packages:
/@types/react-dom@19.0.4(@types/react@19.0.10):
resolution: {integrity: sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg==}
peerDependencies:
'@types/react': ^19.0.0
'@types/react': 19.0.10
dependencies:
'@types/react': 19.0.10
dev: false
@ -2534,6 +2561,11 @@ packages:
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
dev: false
/clsx@1.2.1:
resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==}
engines: {node: '>=6'}
dev: false
/clsx@2.1.1:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
@ -2567,6 +2599,7 @@ packages:
/color-string@1.9.1:
resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==}
requiresBuild: true
dependencies:
color-name: 1.1.4
simple-swizzle: 0.2.2
@ -2576,6 +2609,7 @@ packages:
/color@4.2.3:
resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
engines: {node: '>=12.5.0'}
requiresBuild: true
dependencies:
color-convert: 2.0.1
color-string: 1.9.1
@ -3354,6 +3388,27 @@ packages:
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
dev: false
/framer-motion@12.9.1(react-dom@19.0.0)(react@19.0.0):
resolution: {integrity: sha512-dZBp2TO0a39Cc24opshlLoM0/OdTZVKzcXWuhntfwy2Qgz3t9+N4sTyUqNANyHaRFiJUWbwwsXeDvQkEBPky+g==}
peerDependencies:
'@emotion/is-prop-valid': '*'
react: ^18.0.0 || ^19.0.0
react-dom: ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@emotion/is-prop-valid':
optional: true
react:
optional: true
react-dom:
optional: true
dependencies:
motion-dom: 12.9.1
motion-utils: 12.8.3
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
tslib: 2.8.1
dev: false
/function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
dev: true
@ -3560,6 +3615,7 @@ packages:
/is-arrayish@0.3.2:
resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==}
requiresBuild: true
dev: false
optional: true
@ -4003,6 +4059,16 @@ packages:
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
dev: true
/motion-dom@12.9.1:
resolution: {integrity: sha512-xqXEwRLDYDTzOgXobSoWtytRtGlf7zdkRfFbrrdP7eojaGQZ5Go4OOKtgnx7uF8sAkfr1ZjMvbCJSCIT2h6fkQ==}
dependencies:
motion-utils: 12.8.3
dev: false
/motion-utils@12.8.3:
resolution: {integrity: sha512-GYVauZEbca8/zOhEiYOY9/uJeedYQld6co/GJFKOy//0c/4lDqk0zB549sBYqqV2iMuX+uHrY1E5zd8A2L+1Lw==}
dev: false
/ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
dev: true
@ -4283,6 +4349,18 @@ packages:
scheduler: 0.25.0
dev: false
/react-draggable@4.4.6(react-dom@19.0.0)(react@19.0.0):
resolution: {integrity: sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==}
peerDependencies:
react: '>= 16.3.0'
react-dom: '>= 16.3.0'
dependencies:
clsx: 1.2.1
prop-types: 15.8.1
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
dev: false
/react-hook-form@7.54.1(react@19.0.0):
resolution: {integrity: sha512-PUNzFwQeQ5oHiiTUO7GO/EJXGEtuun2Y1A59rLnZBBj+vNEOWt/3ERTiG1/zt7dVeJEM+4vDX/7XQ/qanuvPMg==}
engines: {node: '>=18.0.0'}
@ -4303,7 +4381,7 @@ packages:
resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==}
engines: {node: '>=10'}
peerDependencies:
'@types/react': '*'
'@types/react': 19.0.10
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@types/react':
@ -4319,7 +4397,7 @@ packages:
resolution: {integrity: sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==}
engines: {node: '>=10'}
peerDependencies:
'@types/react': '*'
'@types/react': 19.0.10
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
@ -4361,7 +4439,7 @@ packages:
resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==}
engines: {node: '>=10'}
peerDependencies:
'@types/react': '*'
'@types/react': 19.0.10
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
@ -4644,6 +4722,7 @@ packages:
/simple-swizzle@0.2.2:
resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
requiresBuild: true
dependencies:
is-arrayish: 0.3.2
dev: false
@ -4790,6 +4869,14 @@ packages:
resolution: {integrity: sha512-FQT/OVqCD+7edmmJpsgCsY820RTD5AkBryuG5IUqR5YQZSdj5xlH5nLgH7YPths7WsLPSpSBNneJdM8aS8aeFA==}
dev: false
/tailwind-scrollbar-hide@2.0.0(tailwindcss@4.1.3):
resolution: {integrity: sha512-lqiIutHliEiODwBRHy4G2+Tcayo2U7+3+4frBmoMETD72qtah+XhOk5XcPzC1nJvXhXUdfl2ajlMhUc2qC6CIg==}
peerDependencies:
tailwindcss: '>=3.0.0 || >= 4.0.0 || >= 4.0.0-beta.8 || >= 4.0.0-alpha.20'
dependencies:
tailwindcss: 4.1.3
dev: false
/tailwindcss-animate@1.0.7(tailwindcss@4.1.3):
resolution: {integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==}
peerDependencies:
@ -4804,7 +4891,6 @@ packages:
/tailwindcss@4.1.3:
resolution: {integrity: sha512-2Q+rw9vy1WFXu5cIxlvsabCwhU2qUwodGq03ODhLJ0jW4ek5BUtoCsnLB0qG+m8AHgEsSJcJGDSDe06FXlP74g==}
dev: false
/tapable@2.2.1:
resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==}
@ -4969,7 +5055,7 @@ packages:
resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==}
engines: {node: '>=10'}
peerDependencies:
'@types/react': '*'
'@types/react': 19.0.10
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
@ -4984,7 +5070,7 @@ packages:
resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==}
engines: {node: '>=10'}
peerDependencies:
'@types/react': '*'
'@types/react': 19.0.10
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
@ -5108,7 +5194,7 @@ packages:
resolution: {integrity: sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==}
engines: {node: '>=12.20.0'}
peerDependencies:
'@types/react': '>=18.0.0'
'@types/react': 19.0.10
immer: '>=9.0.6'
react: '>=18.0.0'
use-sync-external-store: '>=1.2.0'

View File

@ -1,13 +1,13 @@
import { create } from "zustand";
type TopNavigationState = {
type ModelState = {
selectedModel: string;
setSelectedModel: (model: string) => void;
models: string[];
setModels: (models: string[]) => void;
};
export const useTopNavigationStore = create<TopNavigationState>((set) => ({
export const useModelState = create<ModelState>((set) => ({
selectedModel: "Standard ML Model v2.4",
setSelectedModel: (model) => set({ selectedModel: model }),
models: [

View File

@ -26,9 +26,18 @@ export interface SuccessResponse {
/**
* Property Data Types
*/
export type PropertyType = "Condominium" | "House" | "Townhouse" | "Land" | "Apartment" | "Other";
export type PropertyType =
| "Condominium"
| "House"
| "Townhouse"
| "Land"
| "Apartment"
| "Other";
export type OwnershipType = "Freehold" | "Leasehold";
export type FurnishingStatus = "Unfurnished" | "Partly Furnished" | "Fully Furnished";
export type FurnishingStatus =
| "Unfurnished"
| "Partly Furnished"
| "Fully Furnished";
export interface PriceHistoryEntry {
date: string; // Consider using Date object after parsing
@ -134,8 +143,19 @@ export interface DataPipeline {
/**
* Model Types
*/
export type ModelType = "Regression" | "Neural Network" | "Geospatial" | "Time Series" | "Ensemble" | "Classification";
export type ModelStatus = "active" | "inactive" | "training" | "error" | "pending";
export type ModelType =
| "Regression"
| "Neural Network"
| "Geospatial"
| "Time Series"
| "Ensemble"
| "Classification";
export type ModelStatus =
| "active"
| "inactive"
| "training"
| "error"
| "pending";
export interface Model {
id: string;