From ba9cbe18f468fcddd852396bbcc4b4aa47ba563a Mon Sep 17 00:00:00 2001 From: Sean O'Connor Date: Fri, 5 Dec 2025 02:17:18 -0500 Subject: [PATCH] feat: Introduce a toggleable discovery drawer with shop search and a new 'locate me' map control. --- src/app/page.tsx | 11 +- src/components/Drawer.tsx | 243 +++++++++++++++++++------------ src/components/LocateControl.tsx | 35 +++++ src/components/Map.tsx | 8 +- src/components/MapLoader.tsx | 6 +- src/components/Navbar.tsx | 51 +++++-- src/components/ui/input.tsx | 21 +++ 7 files changed, 264 insertions(+), 111 deletions(-) create mode 100644 src/components/LocateControl.tsx create mode 100644 src/components/ui/input.tsx diff --git a/src/app/page.tsx b/src/app/page.tsx index 1422364..c8526a7 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -9,6 +9,7 @@ import { WelcomeModal } from "~/components/WelcomeModal"; export default function HomePage() { const [selectedShop, setSelectedShop] = useState(null); + const [isDiscoveryOpen, setIsDiscoveryOpen] = useState(true); return (
@@ -16,15 +17,23 @@ export default function HomePage() {
setSelectedShop(shop)} + onShopSelect={(shop: typeof COFFEE_SHOPS[0]) => { + setSelectedShop(shop); + setIsDiscoveryOpen(true); + }} selectedShop={selectedShop} + isDiscoveryOpen={isDiscoveryOpen} + onToggleDiscovery={() => setIsDiscoveryOpen(!isDiscoveryOpen)} />
{/* Right Drawer */} setSelectedShop(null)} + isOpen={isDiscoveryOpen} />
diff --git a/src/components/Drawer.tsx b/src/components/Drawer.tsx index b196efc..549a192 100644 --- a/src/components/Drawer.tsx +++ b/src/components/Drawer.tsx @@ -1,27 +1,34 @@ -import { X, MapPin, Globe, Phone, Coffee, ExternalLink } from "lucide-react"; +import { X, MapPin, Globe, Phone, Coffee, ExternalLink, Search, ChevronRight } from "lucide-react"; import { Card } from "~/components/ui/card"; import { Button } from "~/components/ui/button"; import { ScrollArea } from "~/components/ui/scroll-area"; import { Separator } from "~/components/ui/separator"; import { Skeleton } from "~/components/ui/skeleton"; +import { Input } from "~/components/ui/input"; import { useState, useEffect } from "react"; -interface DrawerProps { - shop: { - id: number; - name: string; - description: string; - image: string; - address: string; - phone: string; - website: string; - lat: number; - lng: number; - } | null; - onClose: () => void; +interface CoffeeShop { + id: number; + name: string; + description: string; + image: string; + address: string; + phone: string; + website: string; + lat: number; + lng: number; } -export default function Drawer({ shop, onClose }: DrawerProps) { +interface DrawerProps { + shop: CoffeeShop | null; + shops: CoffeeShop[]; + onSelect: (shop: CoffeeShop) => void; + onClose: () => void; + isOpen: boolean; +} + +export default function Drawer({ shop, shops, onSelect, onClose, isOpen }: DrawerProps) { + const [searchQuery, setSearchQuery] = useState(""); const [imageLoading, setImageLoading] = useState(true); // Reset loading state when shop changes @@ -31,106 +38,154 @@ export default function Drawer({ shop, onClose }: DrawerProps) { } }, [shop]); - if (!shop) return null; + const filteredShops = shops.filter(s => + s.name.toLowerCase().includes(searchQuery.toLowerCase()) || + s.description.toLowerCase().includes(searchQuery.toLowerCase()) + ); return (
- {shop && ( - - {/* Header Image */} -
- {imageLoading && ( -
- - -
- )} -
- {shop.name} setImageLoading(false)} - style={{ - maskImage: 'linear-gradient(to bottom, black 50%, transparent 100%)', - WebkitMaskImage: 'linear-gradient(to bottom, black 50%, transparent 100%)' - }} - /> -
- + + {shop ? ( + // Details View + // Details View +
-
- {/* Content */} - -
-

{shop.name}

- -
-
- - {shop.address} + + {/* Header Image - Now part of scroll area */} +
+ {imageLoading && ( +
+ + +
+ )} +
+ {shop.name} setImageLoading(false)} + style={{ + maskImage: 'linear-gradient(to bottom, black 50%, transparent 100%)', + WebkitMaskImage: 'linear-gradient(to bottom, black 50%, transparent 100%)' + }} + />
- {shop.phone && ( -
- - {shop.phone} -
- )} - {shop.website && ( - - )}
- + {/* Content - Overlaps image slightly or just follows */} +
+

{shop.name}

-
-
-

About

-

- {shop.description} -

+
+
+ + {shop.address} +
+ {shop.phone && ( +
+ + {shop.phone} +
+ )} + {shop.website && ( + + )}
- + + Get Directions + + +
+
+ +
+ ) : ( + // List View +
+
+

Discover Coffee

+
+ + setSearchQuery(e.target.value)} + />
- - - )} + +
+ {filteredShops.map((s) => ( +
onSelect(s)} + className="group flex items-center gap-4 p-3 rounded-lg hover:bg-muted/50 transition-colors cursor-pointer border border-transparent hover:border-border/50" + > +
+ {s.name} +
+
+

{s.name}

+

{s.address}

+
+ +
+ ))} + {filteredShops.length === 0 && ( +
+ +

No shops found matching "{searchQuery}"

+
+ )} +
+
+
+ )} +
); } diff --git a/src/components/LocateControl.tsx b/src/components/LocateControl.tsx new file mode 100644 index 0000000..189b7fc --- /dev/null +++ b/src/components/LocateControl.tsx @@ -0,0 +1,35 @@ +"use client"; + +import { Locate } from "lucide-react"; +import { Button } from "~/components/ui/button"; +import { useMap } from "react-leaflet"; +import { useState } from "react"; + +export function LocateControl() { + const map = useMap(); + const [loading, setLoading] = useState(false); + + const handleLocate = () => { + setLoading(true); + map.locate().on("locationfound", function (e) { + map.flyTo(e.latlng, 16); + setLoading(false); + }).on("locationerror", function () { + setLoading(false); + alert("Could not access your location"); + }); + }; + + return ( + + ); +} diff --git a/src/components/Map.tsx b/src/components/Map.tsx index 9236fc1..4df1535 100644 --- a/src/components/Map.tsx +++ b/src/components/Map.tsx @@ -7,6 +7,7 @@ import { useState, useEffect } from 'react'; import { useTheme } from "next-themes"; import Navbar from "./Navbar"; import { MapStyleControl } from "./MapStyleControl"; +import { LocateControl } from './LocateControl'; import { ZoomControls } from "./ZoomControls"; interface CoffeeShop { @@ -25,6 +26,8 @@ interface MapProps { shops: CoffeeShop[]; onShopSelect: (shop: CoffeeShop) => void; selectedShop: CoffeeShop | null; + isDiscoveryOpen: boolean; + onToggleDiscovery: () => void; } const MapController = ({ selectedShop }: { selectedShop: CoffeeShop | null }) => { @@ -42,7 +45,7 @@ const MapController = ({ selectedShop }: { selectedShop: CoffeeShop | null }) => return null; }; -const Map = ({ shops, onShopSelect, selectedShop }: MapProps) => { +const Map = ({ shops, onShopSelect, selectedShop, isDiscoveryOpen, onToggleDiscovery }: MapProps) => { useEffect(() => { // Fix for Leaflet default icon not found // @ts-expect-error Fix for Leaflet default icon not found @@ -122,9 +125,10 @@ const Map = ({ shops, onShopSelect, selectedShop }: MapProps) => { zoomControl={false} attributionControl={false} > - +
+
diff --git a/src/components/MapLoader.tsx b/src/components/MapLoader.tsx index 3105876..a19bda6 100644 --- a/src/components/MapLoader.tsx +++ b/src/components/MapLoader.tsx @@ -23,8 +23,10 @@ interface MapLoaderProps { shops: CoffeeShop[]; onShopSelect: (shop: CoffeeShop) => void; selectedShop: CoffeeShop | null; + isDiscoveryOpen: boolean; + onToggleDiscovery: () => void; } -export default function MapLoader({ shops, onShopSelect, selectedShop }: MapLoaderProps) { - return ; +export default function MapLoader({ shops, onShopSelect, selectedShop, isDiscoveryOpen, onToggleDiscovery }: MapLoaderProps) { + return ; } diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index e1d3db3..57e8a2e 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -1,23 +1,50 @@ -import { Coffee, X } from "lucide-react"; +import { Coffee, PanelLeft, X } from "lucide-react"; import { Button } from "~/components/ui/button"; import { useState } from "react"; -export default function Navbar() { +interface NavbarProps { + isDiscoveryOpen: boolean; + onToggleDiscovery: () => void; +} + +export default function Navbar({ isDiscoveryOpen, onToggleDiscovery }: NavbarProps) { const [showAbout, setShowAbout] = useState(false); + const handleHeaderClick = () => { + const event = new CustomEvent('show-welcome-modal'); + window.dispatchEvent(event); + }; + return ( <> -
-
-
window.dispatchEvent(new Event("show-welcome-modal"))} - > - -

- Lewisburg Coffee Map -

+
+
+
+
+ +
+
+ +
+
+

Lewisburg Coffee Map

+

Find your perfect brew

+
+
+ +
{/* Spacer to balance the toggle button */}
diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx new file mode 100644 index 0000000..c98ff86 --- /dev/null +++ b/src/components/ui/input.tsx @@ -0,0 +1,21 @@ +import * as React from "react" + +import { cn } from "~/lib/utils" + +function Input({ className, type, ...props }: React.ComponentProps<"input">) { + return ( + + ) +} + +export { Input }