feat: implement MapWithSearch component for enhanced map functionality with location search and type selection

This commit is contained in:
Pattadon 2025-04-11 11:27:23 +07:00
parent 6443f5e893
commit 9eaa5b2761
5 changed files with 296 additions and 116 deletions

View File

@ -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> );
)
} }

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

@ -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>

View File

@ -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",

View File

@ -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