From 8cf3e365964b21bdccc464cc3e5e7966bc2aca02 Mon Sep 17 00:00:00 2001 From: Bianca Javier Date: Sat, 22 Nov 2025 16:33:21 -0500 Subject: [PATCH] edit read me --- cityassist/components/GoogleMapComponent.tsx | 134 +++++++++++++++++++ cityassist/components/ResultsPage.tsx | 107 ++++++++++++++- cityassist/constants.ts | 115 +++++++++++----- 3 files changed, 323 insertions(+), 33 deletions(-) create mode 100644 cityassist/components/GoogleMapComponent.tsx diff --git a/cityassist/components/GoogleMapComponent.tsx b/cityassist/components/GoogleMapComponent.tsx new file mode 100644 index 0000000..7944964 --- /dev/null +++ b/cityassist/components/GoogleMapComponent.tsx @@ -0,0 +1,134 @@ +"use client"; + +import React, { useEffect, useRef, useState } from "react"; +import { useLoadScript, GoogleMap, Marker, InfoWindow } from "@react-google-maps/api"; +import type { Resource, Coordinate } from "../types"; + +interface GoogleMapComponentProps { + resources: Resource[]; + center: Coordinate; + selectedId?: string; + onSelectResource: (id: string | undefined) => void; + query?: string; + onPlacesFound?: (resources: Resource[], summary: string) => void; +} + +const libraries: ("places")[] = ["places"]; + +export default function GoogleMapComponent({ + resources, + center, + selectedId, + onSelectResource, + query, + onPlacesFound, +}: GoogleMapComponentProps) { + const apiKey = (import.meta.env.VITE_GOOGLE_MAPS_API_KEY || "").toString(); + const { isLoaded, loadError } = useLoadScript({ + googleMapsApiKey: apiKey, + libraries, + }); + + const [mapInstance, setMapInstance] = useState(null); + const [infoOpenId, setInfoOpenId] = useState(null); + + useEffect(() => { + if (loadError) { + console.error("Google Maps failed to load:", loadError); + } + }, [loadError]); + + // Run Places textSearch when query changes + /* + useEffect(() => { + if (!isLoaded || !query || !mapInstance) return; + + const service = new google.maps.places.PlacesService(mapInstance); + const request: google.maps.places.TextSearchRequest = { + query, + location: new google.maps.LatLng(center.lat, center.lng), + radius: 5000, + }; + + service.textSearch(request, (results, status) => { + if (status === google.maps.places.PlacesServiceStatus.OK && results && results.length > 0) { + const mapped: Resource[] = results.map((r) => { + const type = r.types?.[0]?.replace(/_/g, " ") || "place"; + const address = r.formatted_address || r.vicinity || "unknown location"; + return { + id: r.place_id || r.id || Math.random().toString(36).slice(2, 9), + name: r.name || "", + description: `This is a ${type} located at ${address}.`, + category: (r.types && r.types[0]) || "place", + lat: r.geometry?.location?.lat() || 0, + lng: r.geometry?.location?.lng() || 0, + address: address, + hours: r.opening_hours?.weekday_text ? r.opening_hours.weekday_text.join("; ") : "Hours not available", + }; + }); + + if (onPlacesFound) { + onPlacesFound(mapped, `Showing ${mapped.length} places from Google for “${query}”`); + } + } else { + // no results or error — let caller handle fallback + if (onPlacesFound) onPlacesFound([], `No Google Places results for “${query}”.`); + } + }); + }, [isLoaded, query, center, onPlacesFound, mapInstance]); + */ + + if (!isLoaded) return
Loading map…
; + + return ( + setMapInstance(map)} + mapContainerClassName="w-full h-full" + mapContainerStyle={{ width: "100%", height: "100%" }} + // Use defaultCenter so the map is not forcibly re-centered on every render — + // this allows the user to pan/zoom freely. + defaultCenter={{ lat: center.lat, lng: center.lng }} + zoom={13} + options={{ disableDefaultUI: true, gestureHandling: 'greedy' }} + > + {/* user marker */} + + + {resources.map((r) => ( + { + onSelectResource(r.id); + setInfoOpenId(r.id); + }} + /> + ))} + + {infoOpenId && (() => { + const r = resources.find((x) => x.id === infoOpenId); + if (!r) return null; + return ( + { setInfoOpenId(null); onSelectResource(undefined); }}> +
+

{r.name}

+

{r.address}

+ Directions +
+
+ ); + })()} + +
+ ); +} diff --git a/cityassist/components/ResultsPage.tsx b/cityassist/components/ResultsPage.tsx index fd7cb5e..637a58b 100644 --- a/cityassist/components/ResultsPage.tsx +++ b/cityassist/components/ResultsPage.tsx @@ -14,9 +14,10 @@ import { DarkModeContext } from "../App"; interface ResultsPageProps { userLocation: Coordinate; + setUserLocation?: (c: Coordinate) => void; } -const ResultsPage: React.FC = ({ userLocation }) => { +const ResultsPage: React.FC = ({ userLocation, setUserLocation }) => { const { darkMode } = useContext(DarkModeContext); // Manual query parsing from hash const getQueryFromHash = () => { @@ -51,6 +52,8 @@ const ResultsPage: React.FC = ({ userLocation }) => { const [chatAnswer, setChatAnswer] = useState | null>(null); + const [showLocationInput, setShowLocationInput] = useState(false); + const [manualAddress, setManualAddress] = useState(""); // Detect crisis keywords const isCrisisQuery = (q: string) => { @@ -78,6 +81,30 @@ const ResultsPage: React.FC = ({ userLocation }) => { setChatAnswer(answer); }, [queryParam]); + const handleSetManualLocation = (e: React.FormEvent) => { + e.preventDefault(); + if (!manualAddress.trim()) return; + + // Simple manual coordinate parsing if user enters "lat, lng" + // Since we removed Google Geocoding, we can't easily convert address to coords client-side without an API key. + // For now, we'll just alert the user or try to parse coordinates. + const parts = manualAddress.split(","); + if (parts.length === 2) { + const lat = parseFloat(parts[0].trim()); + const lng = parseFloat(parts[1].trim()); + if (!isNaN(lat) && !isNaN(lng)) { + if (setUserLocation) { + setUserLocation({ lat, lng }); + } + setShowLocationInput(false); + setManualAddress(""); + return; + } + } + + alert("Please enter coordinates in 'lat, lng' format (e.g. 43.6532, -79.3832). Address lookup is currently disabled."); + }; + const performSearch = useCallback( async (searchQuery: string) => { if (!searchQuery.trim()) return; @@ -90,6 +117,53 @@ const ResultsPage: React.FC = ({ userLocation }) => { const answer = getQuickAnswer(searchQuery); setChatAnswer(answer); + // HARDCODED MODE: Filter local STATIC_RESOURCES instead of calling external APIs + // This ensures accurate, curated data is shown as requested. + const lowerQuery = searchQuery.toLowerCase(); + + // Simple keyword mapping for better natural language support + const keywords: Record = { + shelter: ["sleep", "bed", "homeless", "night", "stay", "housing", "shelter"], + food: ["eat", "hungry", "meal", "groceries", "food", "bank"], + health: ["sick", "doctor", "medical", "hurt", "pain", "clinic", "hospital", "health"], + legal: ["lawyer", "court", "legal", "rights", "eviction"], + crisis: ["suicide", "help", "emergency", "danger", "safe", "crisis"], + community: ["library", "internet", "wifi", "community", "support"] + }; + + // Determine target categories based on query + const targetCategories = Object.entries(keywords) + .filter(([_, words]) => words.some(w => lowerQuery.includes(w))) + .map(([cat]) => cat); + + const filtered = STATIC_RESOURCES.filter(r => { + // 1. Direct match + if (r.name.toLowerCase().includes(lowerQuery) || + r.category.toLowerCase().includes(lowerQuery) || + r.description.toLowerCase().includes(lowerQuery) || + r.address.toLowerCase().includes(lowerQuery)) { + return true; + } + // 2. Category match via keywords + if (targetCategories.includes(r.category)) { + return true; + } + return false; + }); + + setResources(filtered); + setSummary(filtered.length > 0 + ? `Found ${filtered.length} resources matching "${searchQuery}" from our verified directory.` + : `No verified resources found for "${searchQuery}". Try searching for "food", "shelter", or "health".` + ); + + if (filtered.length > 0) { + setSelectedId(filtered[0].id); + } + setLoading(false); + + /* + // External API calls disabled for hardcoded mode try { const result: AIResponse = await searchResourcesWithGemini( searchQuery, @@ -107,6 +181,7 @@ const ResultsPage: React.FC = ({ userLocation }) => { } finally { setLoading(false); } + */ }, [userLocation] ); @@ -130,7 +205,8 @@ const ResultsPage: React.FC = ({ userLocation }) => { }; const handleChipClick = (category: string) => { - navigate(`/map?q=${encodeURIComponent("Find " + category)}`); + // Directly search for the category name to match our hardcoded data + navigate(`/map?q=${encodeURIComponent(category)}`); }; const handleBack = () => { @@ -254,6 +330,31 @@ const ResultsPage: React.FC = ({ userLocation }) => { + {/* Manual location setter - in case geolocation is denied */} +
+ + + {showLocationInput && ( +
+ setManualAddress(e.target.value)} + placeholder="Enter lat, lng" + className="flex-1 px-2 py-1 rounded-md border text-sm" + /> + +
+ )} +
+ {/* Quick Filters */}
{["Food", "Shelter", "Health", "Legal"].map((cat) => ( @@ -325,7 +426,7 @@ const ResultsPage: React.FC = ({ userLocation }) => { darkMode ? "text-gray-200" : "text-blue-900" }`} > - CityAssist AI + 6ixAssist AI