diff --git a/frontend/app/(sidebar)/maps/page.tsx b/frontend/app/(sidebar)/maps/page.tsx index 65630b2..197a70b 100644 --- a/frontend/app/(sidebar)/maps/page.tsx +++ b/frontend/app/(sidebar)/maps/page.tsx @@ -1,12 +1,12 @@ -"use client" +"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 { 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, @@ -28,29 +28,30 @@ import { Star, Clock, RefreshCw, -} from "lucide-react" -import Link from "next/link" -import { TopNavigation } from "@/components/navigation/top-navigation" +} from "lucide-react"; +import Link from "next/link"; +import { TopNavigation } from "@/components/navigation/top-navigation"; +import MapWithSearch from "@/components/map/map-with-search"; 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 [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 }]) + setMessages([...messages, { role: "user", content: message }]); // Simulate AI response setTimeout(() => { setMessages((prev) => [ @@ -60,37 +61,41 @@ export default function MapsPage() { 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("") + ]); + }, 1000); + setMessage(""); } - } + }; const handleZoomIn = () => { - setMapZoom((prev) => Math.min(prev + 1, 20)) - } + setMapZoom((prev) => Math.min(prev + 1, 20)); + }; const handleZoomOut = () => { - setMapZoom((prev) => Math.max(prev - 1, 10)) - } + setMapZoom((prev) => Math.max(prev - 1, 10)); + }; const handlePropertyClick = () => { - setShowPropertyInfo(true) - setShowFilters(false) - setShowChat(false) - } + setShowPropertyInfo(true); + setShowFilters(false); + setShowChat(false); + }; return (
- {/* Map Container */} -
- {/* Map Placeholder - In a real implementation, this would be a map component */} +
-
Map View
+ +
+ Map View +
{/* Sample Property Markers */} -
+
@@ -100,7 +105,10 @@ export default function MapsPage() {
-
+
@@ -110,7 +118,10 @@ export default function MapsPage() {
-
+
@@ -125,37 +136,20 @@ export default function MapsPage() { {/* Top Navigation Bar */} - {/* Map Controls */} -
- - -
{/* Map Overlay Controls */}
-
@@ -244,12 +247,16 @@ export default function MapsPage() {
-

Environmental Factors

+

+ Environmental Factors +

Flood Risk - Moderate + + Moderate +
@@ -314,14 +321,21 @@ export default function MapsPage() { > -
-

Information in radius will be analyzed

+

+ Information in radius will be analyzed +

Using: {selectedModel} @@ -334,7 +348,9 @@ export default function MapsPage() { Area Price History

10,000,000 Baht

-

Overall Price History of this area

+

+ Overall Price History of this area +

{/* Simple line chart simulation */} @@ -358,7 +374,9 @@ export default function MapsPage() { Price Prediction

15,000,000 Baht

-

The estimated price based on various factors.

+

+ The estimated price based on various factors. +

{/* Simple line chart simulation */} @@ -406,9 +424,12 @@ export default function MapsPage() {
-
New BTS Extension Planned
+
+ New BTS Extension Planned +

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

@@ -418,9 +439,12 @@ export default function MapsPage() { -
Property Tax Changes
+
+ Property Tax Changes +

- New property tax regulations will take effect next month affecting luxury condominiums. + New property tax regulations will take effect next month + affecting luxury condominiums.

@@ -453,13 +477,22 @@ export default function MapsPage() { Property Filters
-
- + Basic Advanced @@ -468,7 +501,9 @@ export default function MapsPage() {
- +
- + @@ -511,7 +550,9 @@ export default function MapsPage() {
- +
฿{priceRange[0].toLocaleString()} ฿{priceRange[1].toLocaleString()} @@ -526,7 +567,9 @@ export default function MapsPage() {
- +
Low Flood Risk @@ -545,7 +588,9 @@ export default function MapsPage() {
- +
@@ -560,7 +605,11 @@ export default function MapsPage() {
- + @@ -590,7 +639,12 @@ export default function MapsPage() { Chat Assistant
-
@@ -598,10 +652,17 @@ export default function MapsPage() {
{messages.map((msg, index) => ( -
+
{msg.content} @@ -619,10 +680,15 @@ export default function MapsPage() { value={message} onChange={(e) => setMessage(e.target.value)} onKeyDown={(e) => { - if (e.key === "Enter") handleSendMessage() + if (e.key === "Enter") handleSendMessage(); }} /> -
@@ -631,7 +697,7 @@ export default function MapsPage() { )} {/* Map Legend */} -
+ {/*
Property Status
@@ -647,8 +713,7 @@ export default function MapsPage() { Sold
-
+
*/}
- ) + ); } - diff --git a/frontend/components/map/map-with-search.tsx b/frontend/components/map/map-with-search.tsx new file mode 100644 index 0000000..4318580 --- /dev/null +++ b/frontend/components/map/map-with-search.tsx @@ -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(null); + const inputRef = useRef(null); + const mapInstanceRef = useRef(null); + const [mapType, setMapType] = useState("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 ( +
+
+ + +
+
+
+); + +} diff --git a/frontend/components/navigation/top-navigation.tsx b/frontend/components/navigation/top-navigation.tsx index 553d21c..40a5847 100644 --- a/frontend/components/navigation/top-navigation.tsx +++ b/frontend/components/navigation/top-navigation.tsx @@ -34,18 +34,6 @@ export function TopNavigation() { BorBann -
-
- -
- -
-
-
diff --git a/frontend/package.json b/frontend/package.json index c3602eb..aa5d25f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", @@ -94,6 +95,7 @@ "@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", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index ecd41ca..de9f337 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -5,6 +5,9 @@ overrides: '@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) @@ -172,6 +175,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 @@ -305,6 +311,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: @@ -1997,6 +2007,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