1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
/**
* This application creates "The Here Times", a map-based news aggregator.
* It uses the Google Maps JavaScript API for map rendering, Geonames for location data,
* and the NewsAPI for fetching news articles.
* The app displays news for the top 12 most populous cities/neighborhoods in the current map view.
*
* We'll use the following approach:
* 1. Use Google Maps JavaScript API for rendering the map
* 2. Use Geonames API to get top 12 most populous cities within the map bounds
* 3. Fetch news data from NewsAPI based on these locations
* 4. Display news articles in a side drawer, only showing articles that contain the city's name in the title
* 5. Place markers on the map for each location (exactly 12)
* 6. Add a search bar to allow users to search for specific locations
* 7. Recenter the map when a location is clicked or searched
*/
import { Loader } from "https://esm.sh/@googlemaps/js-api-loader@1.16.2";
import ReactDOM from "https://esm.sh/react-dom@18.2.0";
import React from "https://esm.sh/react@18.2.0";
function App() {
const mapRef = React.useRef(null);
const [map, setMap] = React.useState(null);
const [center, setCenter] = React.useState({ lat: 40.7128, lng: -74.006 });
const [zoom, setZoom] = React.useState(10);
const [news, setNews] = React.useState([]);
const [locations, setLocations] = React.useState([]);
const [isDrawerOpen, setIsDrawerOpen] = React.useState(false);
const [error, setError] = React.useState(null);
const markersRef = React.useRef([]);
const [googleMaps, setGoogleMaps] = React.useState(null);
const [selectedLocation, setSelectedLocation] = React.useState(null);
const [searchQuery, setSearchQuery] = React.useState("");
const searchBoxRef = React.useRef(null);
React.useEffect(() => {
const initMap = async () => {
try {
const response = await fetch("/api/maps-key");
const { apiKey } = await response.json();
const loader = new Loader({
apiKey: apiKey,
version: "weekly",
libraries: ["places"],
});
const google = await loader.load();
setGoogleMaps(google.maps);
const newMap = new google.maps.Map(mapRef.current, {
center: center,
zoom: zoom,
mapTypeControl: false,
});
setMap(newMap);
// Initialize SearchBox after Google Maps is fully loaded
const searchInput = document.getElementById("search-input");
const searchBox = new google.maps.places.SearchBox(searchInput);
searchBoxRef.current = searchBox;
newMap.controls[google.maps.ControlPosition.TOP_LEFT].push(document.getElementById("search-container"));
newMap.addListener("bounds_changed", () => {
searchBox.setBounds(newMap.getBounds());
});
searchBox.addListener("places_changed", () => {
const places = searchBox.getPlaces();
if (places.length === 0) return;
const bounds = new google.maps.LatLngBounds();
places.forEach((place) => {
if (place.geometry && place.geometry.location) {
if (place.geometry.viewport) {
bounds.union(place.geometry.viewport);
} else {
bounds.extend(place.geometry.location);
}
}
});
newMap.fitBounds(bounds);
newMap.setZoom(Math.min(newMap.getZoom(), 12)); // Limit zoom level
setSearchQuery("");
});
newMap.addListener("idle", async () => {
const newCenter = newMap.getCenter();
setCenter({ lat: newCenter.lat(), lng: newCenter.lng() });
setZoom(newMap.getZoom());
const bounds = newMap.getBounds();
const ne = bounds.getNorthEast();
const sw = bounds.getSouthWest();
await fetchLocationsAndNews(sw.lat(), sw.lng(), ne.lat(), ne.lng());
});
} catch (error) {
console.error("Error initializing map:", error);