mirror of
https://github.com/borbann-platform/backend-api.git
synced 2025-12-19 12:44:04 +01:00
feat: implement MapWithSearch component for enhanced map functionality with location search and type selection
This commit is contained in:
parent
6443f5e893
commit
9eaa5b2761
@ -1,12 +1,12 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import { useState, useRef } from "react"
|
import { useState, useRef } from "react";
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent } from "@/components/ui/card"
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Slider } from "@/components/ui/slider"
|
import { Slider } from "@/components/ui/slider";
|
||||||
import { Switch } from "@/components/ui/switch"
|
import { Switch } from "@/components/ui/switch";
|
||||||
import {
|
import {
|
||||||
MapPin,
|
MapPin,
|
||||||
Home,
|
Home,
|
||||||
@ -28,29 +28,30 @@ import {
|
|||||||
Star,
|
Star,
|
||||||
Clock,
|
Clock,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
} from "lucide-react"
|
} from "lucide-react";
|
||||||
import Link from "next/link"
|
import Link from "next/link";
|
||||||
import { TopNavigation } from "@/components/navigation/top-navigation"
|
import { TopNavigation } from "@/components/navigation/top-navigation";
|
||||||
|
import MapWithSearch from "@/components/map/map-with-search";
|
||||||
|
|
||||||
export default function MapsPage() {
|
export default function MapsPage() {
|
||||||
const [showFilters, setShowFilters] = useState(false)
|
const [showFilters, setShowFilters] = useState(false);
|
||||||
const [showAnalytics, setShowAnalytics] = useState(false)
|
const [showAnalytics, setShowAnalytics] = useState(false);
|
||||||
const [showChat, setShowChat] = useState(false)
|
const [showChat, setShowChat] = useState(false);
|
||||||
const [showPropertyInfo, setShowPropertyInfo] = useState(false)
|
const [showPropertyInfo, setShowPropertyInfo] = useState(false);
|
||||||
const [activeTab, setActiveTab] = useState("basic")
|
const [activeTab, setActiveTab] = useState("basic");
|
||||||
const [priceRange, setPriceRange] = useState([5000000, 20000000])
|
const [priceRange, setPriceRange] = useState([5000000, 20000000]);
|
||||||
const [radius, setRadius] = useState(30)
|
const [radius, setRadius] = useState(30);
|
||||||
const [message, setMessage] = useState("")
|
const [message, setMessage] = useState("");
|
||||||
const [messages, setMessages] = useState([{ role: "assistant", content: "Hi! How can I help you today?" }])
|
const [messages, setMessages] = useState([
|
||||||
const [mapZoom, setMapZoom] = useState(14)
|
{ role: "assistant", content: "Hi! How can I help you today?" },
|
||||||
const [selectedModel, setSelectedModel] = useState("Standard ML Model v2.4")
|
]);
|
||||||
const mapRef = useRef(null)
|
const [mapZoom, setMapZoom] = useState(14);
|
||||||
|
const [selectedModel, setSelectedModel] = useState("Standard ML Model v2.4");
|
||||||
|
const mapRef = useRef(null);
|
||||||
|
|
||||||
const handleSendMessage = () => {
|
const handleSendMessage = () => {
|
||||||
if (message.trim()) {
|
if (message.trim()) {
|
||||||
setMessages([...messages, { role: "user", content: message }])
|
setMessages([...messages, { role: "user", content: message }]);
|
||||||
// Simulate AI response
|
// Simulate AI response
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setMessages((prev) => [
|
setMessages((prev) => [
|
||||||
@ -60,37 +61,41 @@ export default function MapsPage() {
|
|||||||
content:
|
content:
|
||||||
"I can provide information about properties in this area. Would you like to know about flood risks, air quality, or nearby amenities?",
|
"I can provide information about properties in this area. Would you like to know about flood risks, air quality, or nearby amenities?",
|
||||||
},
|
},
|
||||||
])
|
]);
|
||||||
}, 1000)
|
}, 1000);
|
||||||
setMessage("")
|
setMessage("");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleZoomIn = () => {
|
const handleZoomIn = () => {
|
||||||
setMapZoom((prev) => Math.min(prev + 1, 20))
|
setMapZoom((prev) => Math.min(prev + 1, 20));
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleZoomOut = () => {
|
const handleZoomOut = () => {
|
||||||
setMapZoom((prev) => Math.max(prev - 1, 10))
|
setMapZoom((prev) => Math.max(prev - 1, 10));
|
||||||
}
|
};
|
||||||
|
|
||||||
const handlePropertyClick = () => {
|
const handlePropertyClick = () => {
|
||||||
setShowPropertyInfo(true)
|
setShowPropertyInfo(true);
|
||||||
setShowFilters(false)
|
setShowFilters(false);
|
||||||
setShowChat(false)
|
setShowChat(false);
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative h-screen w-full overflow-hidden bg-gray-100 dark:bg-gray-900">
|
<div className="relative h-screen w-full overflow-hidden bg-gray-100 dark:bg-gray-900">
|
||||||
{/* Map Container */}
|
<div>
|
||||||
<div className="">
|
|
||||||
{/* Map Placeholder - In a real implementation, this would be a map component */}
|
|
||||||
<div className="absolute inset-0 flex items-center justify-center">
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
<div className="text-2xl text-muted-foreground opacity-0">Map View</div>
|
<MapWithSearch />
|
||||||
|
<div className="text-2xl text-muted-foreground opacity-0">
|
||||||
|
Map View
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Sample Property Markers */}
|
{/* Sample Property Markers */}
|
||||||
<div className="absolute left-1/4 top-1/3 text-primary cursor-pointer group" onClick={handlePropertyClick}>
|
<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="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>
|
<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" />
|
<MapPin className="h-10 w-10 text-green-500 drop-shadow-xl" />
|
||||||
@ -100,7 +105,10 @@ export default function MapsPage() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute left-1/2 top-1/2 text-primary cursor-pointer group" onClick={handlePropertyClick}>
|
<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="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>
|
<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" />
|
<MapPin className="h-10 w-10 text-yellow-500 drop-shadow-xl" />
|
||||||
@ -110,7 +118,10 @@ export default function MapsPage() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute right-1/4 top-2/3 text-primary cursor-pointer group" onClick={handlePropertyClick}>
|
<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="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>
|
<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" />
|
<MapPin className="h-10 w-10 text-red-500 drop-shadow-xl" />
|
||||||
@ -125,37 +136,20 @@ export default function MapsPage() {
|
|||||||
{/* Top Navigation Bar */}
|
{/* Top Navigation Bar */}
|
||||||
<TopNavigation />
|
<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 */}
|
{/* Map Overlay Controls */}
|
||||||
<div className="absolute bottom-8 left-1/2 -translate-x-1/2 flex gap-2 z-10">
|
<div className="absolute bottom-8 left-1/2 -translate-x-1/2 flex gap-2 z-10">
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="icon"
|
size="icon"
|
||||||
className={`h-12 w-12 rounded-full bg-background/95 backdrop-blur-sm shadow-md ${showAnalytics ? "bg-primary text-primary-foreground" : ""}`}
|
className={`h-12 w-12 rounded-full bg-background/95 backdrop-blur-sm shadow-md ${
|
||||||
|
showAnalytics ? "bg-primary text-primary-foreground" : ""
|
||||||
|
}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowAnalytics(!showAnalytics)
|
setShowAnalytics(!showAnalytics);
|
||||||
if (showAnalytics) {
|
if (showAnalytics) {
|
||||||
setShowFilters(false)
|
setShowFilters(false);
|
||||||
setShowPropertyInfo(false)
|
setShowPropertyInfo(false);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -164,12 +158,14 @@ export default function MapsPage() {
|
|||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="icon"
|
size="icon"
|
||||||
className={`h-12 w-12 rounded-full bg-background/95 backdrop-blur-sm shadow-md ${showFilters ? "bg-primary text-primary-foreground" : ""}`}
|
className={`h-12 w-12 rounded-full bg-background/95 backdrop-blur-sm shadow-md ${
|
||||||
|
showFilters ? "bg-primary text-primary-foreground" : ""
|
||||||
|
}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowFilters(!showFilters)
|
setShowFilters(!showFilters);
|
||||||
if (showFilters) {
|
if (showFilters) {
|
||||||
setShowAnalytics(false)
|
setShowAnalytics(false);
|
||||||
setShowPropertyInfo(false)
|
setShowPropertyInfo(false);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -178,11 +174,13 @@ export default function MapsPage() {
|
|||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="icon"
|
size="icon"
|
||||||
className={`h-12 w-12 rounded-full bg-background/95 backdrop-blur-sm shadow-md ${showChat ? "bg-primary text-primary-foreground" : ""}`}
|
className={`h-12 w-12 rounded-full bg-background/95 backdrop-blur-sm shadow-md ${
|
||||||
|
showChat ? "bg-primary text-primary-foreground" : ""
|
||||||
|
}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowChat(!showChat)
|
setShowChat(!showChat);
|
||||||
if (showChat) {
|
if (showChat) {
|
||||||
setShowPropertyInfo(false)
|
setShowPropertyInfo(false);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -199,7 +197,12 @@ export default function MapsPage() {
|
|||||||
<span className="font-medium">Property Details</span>
|
<span className="font-medium">Property Details</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => setShowPropertyInfo(false)}>
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8"
|
||||||
|
onClick={() => setShowPropertyInfo(false)}
|
||||||
|
>
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@ -244,12 +247,16 @@ export default function MapsPage() {
|
|||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<h4 className="font-medium text-sm mb-2">Environmental Factors</h4>
|
<h4 className="font-medium text-sm mb-2">
|
||||||
|
Environmental Factors
|
||||||
|
</h4>
|
||||||
<div className="grid grid-cols-3 gap-2">
|
<div className="grid grid-cols-3 gap-2">
|
||||||
<div className="flex flex-col items-center p-2 border rounded-md">
|
<div className="flex flex-col items-center p-2 border rounded-md">
|
||||||
<Droplets className="h-5 w-5 text-blue-500 mb-1" />
|
<Droplets className="h-5 w-5 text-blue-500 mb-1" />
|
||||||
<span className="text-xs font-medium">Flood Risk</span>
|
<span className="text-xs font-medium">Flood Risk</span>
|
||||||
<Badge className="mt-1 text-xs bg-amber-500">Moderate</Badge>
|
<Badge className="mt-1 text-xs bg-amber-500">
|
||||||
|
Moderate
|
||||||
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-center p-2 border rounded-md">
|
<div className="flex flex-col items-center p-2 border rounded-md">
|
||||||
<Wind className="h-5 w-5 text-purple-500 mb-1" />
|
<Wind className="h-5 w-5 text-purple-500 mb-1" />
|
||||||
@ -314,14 +321,21 @@ export default function MapsPage() {
|
|||||||
>
|
>
|
||||||
<RefreshCw className="h-4 w-4" />
|
<RefreshCw className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => setShowAnalytics(false)}>
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8"
|
||||||
|
onClick={() => setShowAnalytics(false)}
|
||||||
|
>
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="map-overlay-content">
|
<div className="map-overlay-content">
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<p className="text-sm text-muted-foreground">Information in radius will be analyzed</p>
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Information in radius will be analyzed
|
||||||
|
</p>
|
||||||
<Badge variant="outline" className="text-xs">
|
<Badge variant="outline" className="text-xs">
|
||||||
Using: {selectedModel}
|
Using: {selectedModel}
|
||||||
</Badge>
|
</Badge>
|
||||||
@ -334,7 +348,9 @@ export default function MapsPage() {
|
|||||||
<span className="font-medium">Area Price History</span>
|
<span className="font-medium">Area Price History</span>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-2xl font-bold mb-1">10,000,000 Baht</h3>
|
<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>
|
<p className="text-xs text-muted-foreground mb-3">
|
||||||
|
Overall Price History of this area
|
||||||
|
</p>
|
||||||
|
|
||||||
<div className="h-20 w-full relative">
|
<div className="h-20 w-full relative">
|
||||||
{/* Simple line chart simulation */}
|
{/* Simple line chart simulation */}
|
||||||
@ -358,7 +374,9 @@ export default function MapsPage() {
|
|||||||
<span className="font-medium">Price Prediction</span>
|
<span className="font-medium">Price Prediction</span>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-2xl font-bold mb-1">15,000,000 Baht</h3>
|
<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>
|
<p className="text-xs text-muted-foreground mb-3">
|
||||||
|
The estimated price based on various factors.
|
||||||
|
</p>
|
||||||
|
|
||||||
<div className="h-20 w-full relative">
|
<div className="h-20 w-full relative">
|
||||||
{/* Simple line chart simulation */}
|
{/* Simple line chart simulation */}
|
||||||
@ -406,9 +424,12 @@ export default function MapsPage() {
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-3">
|
<CardContent className="p-3">
|
||||||
<h5 className="text-sm font-medium">New BTS Extension Planned</h5>
|
<h5 className="text-sm font-medium">
|
||||||
|
New BTS Extension Planned
|
||||||
|
</h5>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
The BTS Skytrain will be extended to cover more areas in Sukhumvit by 2025.
|
The BTS Skytrain will be extended to cover more areas in
|
||||||
|
Sukhumvit by 2025.
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-center text-xs text-muted-foreground mt-1">
|
<div className="flex items-center text-xs text-muted-foreground mt-1">
|
||||||
<Clock className="h-3 w-3 mr-1" />
|
<Clock className="h-3 w-3 mr-1" />
|
||||||
@ -418,9 +439,12 @@ export default function MapsPage() {
|
|||||||
</Card>
|
</Card>
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-3">
|
<CardContent className="p-3">
|
||||||
<h5 className="text-sm font-medium">Property Tax Changes</h5>
|
<h5 className="text-sm font-medium">
|
||||||
|
Property Tax Changes
|
||||||
|
</h5>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
New property tax regulations will take effect next month affecting luxury condominiums.
|
New property tax regulations will take effect next month
|
||||||
|
affecting luxury condominiums.
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-center text-xs text-muted-foreground mt-1">
|
<div className="flex items-center text-xs text-muted-foreground mt-1">
|
||||||
<Clock className="h-3 w-3 mr-1" />
|
<Clock className="h-3 w-3 mr-1" />
|
||||||
@ -453,13 +477,22 @@ export default function MapsPage() {
|
|||||||
<span className="font-medium">Property Filters</span>
|
<span className="font-medium">Property Filters</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => setShowFilters(false)}>
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8"
|
||||||
|
onClick={() => setShowFilters(false)}
|
||||||
|
>
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Tabs defaultValue="basic" value={activeTab} onValueChange={setActiveTab}>
|
<Tabs
|
||||||
|
defaultValue="basic"
|
||||||
|
value={activeTab}
|
||||||
|
onValueChange={setActiveTab}
|
||||||
|
>
|
||||||
<TabsList className="w-full grid grid-cols-2">
|
<TabsList className="w-full grid grid-cols-2">
|
||||||
<TabsTrigger value="basic">Basic</TabsTrigger>
|
<TabsTrigger value="basic">Basic</TabsTrigger>
|
||||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||||
@ -468,7 +501,9 @@ export default function MapsPage() {
|
|||||||
<TabsContent value="basic" className="p-4">
|
<TabsContent value="basic" className="p-4">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="text-sm font-medium mb-1.5 block">Area Radius</label>
|
<label className="text-sm font-medium mb-1.5 block">
|
||||||
|
Area Radius
|
||||||
|
</label>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Slider
|
<Slider
|
||||||
defaultValue={[30]}
|
defaultValue={[30]}
|
||||||
@ -483,7 +518,9 @@ export default function MapsPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="text-sm font-medium mb-1.5 block">Time Period</label>
|
<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">
|
<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="all">All Time</option>
|
||||||
<option value="1m">Last Month</option>
|
<option value="1m">Last Month</option>
|
||||||
@ -494,7 +531,9 @@ export default function MapsPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="text-sm font-medium mb-1.5 block">Property Type</label>
|
<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">
|
<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="any">Any Type</option>
|
||||||
<option value="house">House</option>
|
<option value="house">House</option>
|
||||||
@ -511,7 +550,9 @@ export default function MapsPage() {
|
|||||||
<TabsContent value="advanced" className="p-4">
|
<TabsContent value="advanced" className="p-4">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="text-sm font-medium mb-1.5 block">Price Range</label>
|
<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">
|
<div className="flex items-center justify-between text-xs text-muted-foreground mb-2">
|
||||||
<span>฿{priceRange[0].toLocaleString()}</span>
|
<span>฿{priceRange[0].toLocaleString()}</span>
|
||||||
<span>฿{priceRange[1].toLocaleString()}</span>
|
<span>฿{priceRange[1].toLocaleString()}</span>
|
||||||
@ -526,7 +567,9 @@ export default function MapsPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-medium mb-1.5 block">Environmental Factors</label>
|
<label className="text-sm font-medium mb-1.5 block">
|
||||||
|
Environmental Factors
|
||||||
|
</label>
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-sm">Low Flood Risk</span>
|
<span className="text-sm">Low Flood Risk</span>
|
||||||
@ -545,7 +588,9 @@ export default function MapsPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="text-sm font-medium mb-1.5 block">Facilities Nearby</label>
|
<label className="text-sm font-medium mb-1.5 block">
|
||||||
|
Facilities Nearby
|
||||||
|
</label>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<input type="checkbox" id="bts" className="h-4 w-4" />
|
<input type="checkbox" id="bts" className="h-4 w-4" />
|
||||||
@ -560,7 +605,11 @@ export default function MapsPage() {
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<input type="checkbox" id="hospital" className="h-4 w-4" />
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="hospital"
|
||||||
|
className="h-4 w-4"
|
||||||
|
/>
|
||||||
<label htmlFor="hospital" className="text-sm">
|
<label htmlFor="hospital" className="text-sm">
|
||||||
Hospitals
|
Hospitals
|
||||||
</label>
|
</label>
|
||||||
@ -590,7 +639,12 @@ export default function MapsPage() {
|
|||||||
<span className="font-medium">Chat Assistant</span>
|
<span className="font-medium">Chat Assistant</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => setShowChat(false)}>
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8"
|
||||||
|
onClick={() => setShowChat(false)}
|
||||||
|
>
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@ -598,10 +652,17 @@ export default function MapsPage() {
|
|||||||
|
|
||||||
<div className="flex-1 overflow-y-auto p-3 space-y-3">
|
<div className="flex-1 overflow-y-auto p-3 space-y-3">
|
||||||
{messages.map((msg, index) => (
|
{messages.map((msg, index) => (
|
||||||
<div key={index} className={`flex ${msg.role === "user" ? "justify-end" : "justify-start"}`}>
|
<div
|
||||||
|
key={index}
|
||||||
|
className={`flex ${
|
||||||
|
msg.role === "user" ? "justify-end" : "justify-start"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className={`max-w-[80%] rounded-lg px-3 py-2 ${
|
className={`max-w-[80%] rounded-lg px-3 py-2 ${
|
||||||
msg.role === "user" ? "bg-primary text-primary-foreground" : "bg-muted"
|
msg.role === "user"
|
||||||
|
? "bg-primary text-primary-foreground"
|
||||||
|
: "bg-muted"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{msg.content}
|
{msg.content}
|
||||||
@ -619,10 +680,15 @@ export default function MapsPage() {
|
|||||||
value={message}
|
value={message}
|
||||||
onChange={(e) => setMessage(e.target.value)}
|
onChange={(e) => setMessage(e.target.value)}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Enter") handleSendMessage()
|
if (e.key === "Enter") handleSendMessage();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Button variant="default" size="icon" className="h-10 w-10" onClick={handleSendMessage}>
|
<Button
|
||||||
|
variant="default"
|
||||||
|
size="icon"
|
||||||
|
className="h-10 w-10"
|
||||||
|
onClick={handleSendMessage}
|
||||||
|
>
|
||||||
<Send className="h-4 w-4" />
|
<Send className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@ -631,7 +697,7 @@ export default function MapsPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Map Legend */}
|
{/* 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="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="text-xs font-medium mb-1">Property Status</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
@ -647,8 +713,7 @@ export default function MapsPage() {
|
|||||||
<span className="text-xs">Sold</span>
|
<span className="text-xs">Sold</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
111
frontend/components/map/map-with-search.tsx
Normal file
111
frontend/components/map/map-with-search.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
@ -34,18 +34,6 @@ export function TopNavigation() {
|
|||||||
<Home className="h-5 w-5" />
|
<Home className="h-5 w-5" />
|
||||||
<span className="font-semibold">BorBann</span>
|
<span className="font-semibold">BorBann</span>
|
||||||
</Link>
|
</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">
|
<div className="flex items-center gap-2">
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
|
|||||||
@ -9,6 +9,7 @@
|
|||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@googlemaps/js-api-loader": "^1.16.8",
|
||||||
"@hookform/resolvers": "^3.9.1",
|
"@hookform/resolvers": "^3.9.1",
|
||||||
"@radix-ui/react-accordion": "^1.2.3",
|
"@radix-ui/react-accordion": "^1.2.3",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.6",
|
"@radix-ui/react-alert-dialog": "^1.1.6",
|
||||||
@ -94,6 +95,7 @@
|
|||||||
"@radix-ui/react-toggle-group": "^1.1.1",
|
"@radix-ui/react-toggle-group": "^1.1.1",
|
||||||
"@radix-ui/react-tooltip": "latest",
|
"@radix-ui/react-tooltip": "latest",
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
|
"@types/google.maps": "^3.58.1",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "19.0.10",
|
"@types/react": "19.0.10",
|
||||||
"@types/react-dom": "19.0.4",
|
"@types/react-dom": "19.0.4",
|
||||||
|
|||||||
@ -5,6 +5,9 @@ overrides:
|
|||||||
'@types/react-dom': 19.0.4
|
'@types/react-dom': 19.0.4
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@googlemaps/js-api-loader':
|
||||||
|
specifier: ^1.16.8
|
||||||
|
version: 1.16.8
|
||||||
'@hookform/resolvers':
|
'@hookform/resolvers':
|
||||||
specifier: ^3.9.1
|
specifier: ^3.9.1
|
||||||
version: 3.9.1(react-hook-form@7.54.1)
|
version: 3.9.1(react-hook-form@7.54.1)
|
||||||
@ -172,6 +175,9 @@ devDependencies:
|
|||||||
'@eslint/eslintrc':
|
'@eslint/eslintrc':
|
||||||
specifier: ^3
|
specifier: ^3
|
||||||
version: 3.0.0
|
version: 3.0.0
|
||||||
|
'@types/google.maps':
|
||||||
|
specifier: ^3.58.1
|
||||||
|
version: 3.58.1
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^20
|
specifier: ^20
|
||||||
version: 20.0.0
|
version: 20.0.0
|
||||||
@ -305,6 +311,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==}
|
resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==}
|
||||||
dev: false
|
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):
|
/@hookform/resolvers@3.9.1(react-hook-form@7.54.1):
|
||||||
resolution: {integrity: sha512-ud2HqmGBM0P0IABqoskKWI6PEf6ZDDBZkFqe2Vnl+mTHCEHzr3ISjjZyCwTjC/qpL25JC9aIDkloQejvMeq0ug==}
|
resolution: {integrity: sha512-ud2HqmGBM0P0IABqoskKWI6PEf6ZDDBZkFqe2Vnl+mTHCEHzr3ISjjZyCwTjC/qpL25JC9aIDkloQejvMeq0ug==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -1997,6 +2007,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==}
|
resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==}
|
||||||
dev: false
|
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:
|
/@types/json5@0.0.29:
|
||||||
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
|
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user