"use client"; import { useEffect, useMemo, useRef, useState } from "react"; import Image from "next/image"; import type { Map as LeafletMap, Marker, TileLayer } from "leaflet"; import { ChevronLeft, Coffee, Heart, Layers, LocateFixed, MapPin, Minus, PanelLeft, Plus, Search, X, } from "lucide-react"; import { COFFEE_SHOPS, type CoffeeShop } from "~/lib/coffee-shops"; import "leaflet/dist/leaflet.css"; type MapStyle = "street" | "satellite"; const images = [ "amami-kitchen", "culture-coffee", "7th-st-cafe", "tastecraft-cafe", "paris-bakery-caf", "cycleup-coffee", "cornerstone-kitchen", "gram-s-eatery", "starbucks-coffee", "barnes-noble-caf", "starbucks-giant", "dunkin", "alee-s-cafe", "all-star-bagels", "panera-bread", "street-of-shops", ]; function shopImage(shop: CoffeeShop) { return `/coffee/shops/${images[shop.id - 1]}.jpg`; } const tiles: Record = { street: { url: "https://tile.openstreetmap.org/{z}/{x}/{y}.png", attribution: '© OpenStreetMap contributors', }, satellite: { url: "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}", attribution: "Tiles © Esri", }, }; export function CoffeeMirror() { const [selected, setSelected] = useState(null); const [drawerOpen, setDrawerOpen] = useState(true); const [query, setQuery] = useState(""); const [favorites, setFavorites] = useState([]); const [style, setStyle] = useState("street"); const [styleOpen, setStyleOpen] = useState(false); const [welcomeOpen, setWelcomeOpen] = useState(false); const [mapFailed, setMapFailed] = useState(false); const [locationError, setLocationError] = useState(""); const mapElement = useRef(null); const map = useRef(null); const tileLayer = useRef(null); const markers = useRef(new Map()); useEffect(() => { const timeout = window.setTimeout(() => { setDrawerOpen(window.innerWidth >= 640); try { const saved = JSON.parse(localStorage.getItem("lewisburg-coffee-favorites") ?? "[]") as unknown; if (Array.isArray(saved)) setFavorites(saved.filter((id): id is number => typeof id === "number")); setWelcomeOpen(!localStorage.getItem("hasSeenWelcome")); } catch { setFavorites([]); } }, 0); return () => window.clearTimeout(timeout); }, []); useEffect(() => { let cancelled = false; let resizeObserver: ResizeObserver | undefined; const savedMarkers = markers.current; import("leaflet").then((L) => { if (cancelled || !mapElement.current) return; const instance = L.map(mapElement.current, { center: [40.9645, -76.8845], zoom: 15, zoomControl: false, }); map.current = instance; tileLayer.current = L.tileLayer(tiles.street.url, { attribution: tiles.street.attribution, maxZoom: 19, }).addTo(instance); COFFEE_SHOPS.forEach((shop) => { const marker = L.marker([shop.lat, shop.lng], { title: shop.name, icon: L.divIcon({ className: "coffee-mirror-pin", html: "", iconSize: [18, 18], iconAnchor: [9, 9] }), }).addTo(instance); marker.bindTooltip(shop.name); marker.on("click", () => { setSelected(shop); setDrawerOpen(true); }); markers.current.set(shop.id, marker); }); resizeObserver = new ResizeObserver(() => instance.invalidateSize()); resizeObserver.observe(mapElement.current); requestAnimationFrame(() => instance.invalidateSize()); }).catch(() => { if (!cancelled) setMapFailed(true); }); return () => { cancelled = true; resizeObserver?.disconnect(); map.current?.remove(); map.current = null; tileLayer.current = null; savedMarkers.clear(); }; }, []); useEffect(() => { if (!map.current || !tileLayer.current) return; const L = tileLayer.current; L.setUrl(tiles[style].url); L.options.attribution = tiles[style].attribution; }, [style]); useEffect(() => { if (!selected || !map.current) return; const instance = map.current; const target = instance.project([selected.lat, selected.lng], 16); const offset = window.innerWidth >= 640 && drawerOpen ? 200 : 0; instance.flyTo(instance.unproject(target.subtract([offset, 0]), 16), 16, { duration: 0.8 }); markers.current.forEach((marker, id) => marker.getElement()?.classList.toggle("is-selected", id === selected.id)); }, [selected, drawerOpen]); const filtered = useMemo(() => COFFEE_SHOPS.filter((shop) => `${shop.name} ${shop.address}`.toLowerCase().includes(query.toLowerCase().trim()), ), [query]); const favoriteShops = filtered.filter((shop) => favorites.includes(shop.id)); const otherShops = filtered.filter((shop) => !favorites.includes(shop.id)); function toggleFavorite(id: number) { setFavorites((current) => { const next = current.includes(id) ? current.filter((item) => item !== id) : [...current, id]; localStorage.setItem("lewisburg-coffee-favorites", JSON.stringify(next)); return next; }); } function closeWelcome() { setWelcomeOpen(false); localStorage.setItem("hasSeenWelcome", "true"); } function locate() { setLocationError(""); if (!navigator.geolocation) { setLocationError("Location is unavailable in this browser."); return; } navigator.geolocation.getCurrentPosition( ({ coords }) => map.current?.flyTo([coords.latitude, coords.longitude], 16), () => setLocationError("Couldn’t get your location."), ); } return (
{mapFailed &&

The map could not load. You can still browse the coffee shops.

}
{locationError && {locationError}}
{styleOpen &&
{(["street", "satellite"] as const).map((option) => )}
}
© {new Date().getFullYear()} Sean O’ConnorMap data © OpenStreetMap contributors
{welcomeOpen &&
event.stopPropagation()}>

Welcome to the Lewisburg Coffee Map

Discover the best coffee spots in Lewisburg, PA.

Created by Sean O’Connor
  • Explore the map and select a coffee shop.
  • Search the list to find your next cup.
  • Use the location button to see what’s nearby.
}
); } function ShopRow({ shop, favorite, onSelect, onFavorite }: { shop: CoffeeShop; favorite: boolean; onSelect: (shop: CoffeeShop) => void; onFavorite: (id: number) => void }) { return
; }