feat: Add dynamic theming, map style controls, and core UI components for an interactive map experience.

This commit is contained in:
2025-12-05 01:10:14 -05:00
parent 9f55d8087b
commit 37e522e1e3
15 changed files with 693 additions and 95 deletions

View File

@@ -4,6 +4,7 @@ import { type Metadata } from "next";
import { PT_Serif } from "next/font/google";
import { TRPCReactProvider } from "~/trpc/react";
import { ThemeProvider } from "~/components/ThemeProvider";
export const metadata: Metadata = {
title: "Lewisburg Coffee Map",
@@ -21,9 +22,16 @@ export default function RootLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="en" className={`${ptSerif.variable}`}>
<html lang="en" className={`${ptSerif.variable}`} suppressHydrationWarning>
<body>
<TRPCReactProvider>{children}</TRPCReactProvider>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
<TRPCReactProvider>{children}</TRPCReactProvider>
</ThemeProvider>
</body>
</html>
);

View File

@@ -5,7 +5,9 @@ import MapLoader from "~/components/MapLoader";
import Drawer from "~/components/Drawer";
import { COFFEE_SHOPS } from "~/lib/data";
export default function Home() {
import { WelcomeModal } from "~/components/WelcomeModal";
export default function HomePage() {
const [selectedShop, setSelectedShop] = useState<typeof COFFEE_SHOPS[0] | null>(null);
return (
@@ -23,6 +25,7 @@ export default function Home() {
shop={selectedShop}
onClose={() => setSelectedShop(null)}
/>
<WelcomeModal />
</main>
);
}

View File

@@ -28,24 +28,26 @@ export default function Drawer({ shop, onClose }: DrawerProps) {
}`}
>
{shop && (
<Card className="h-full w-full bg-black/60 backdrop-blur-2xl border-white/10 shadow-2xl overflow-hidden flex flex-col gap-0 pointer-events-auto rounded-xl p-0 border-0">
<Card className="h-full w-full bg-background/60 backdrop-blur-2xl border-border/50 shadow-2xl overflow-hidden flex flex-col gap-0 pointer-events-auto rounded-xl p-0 border-0">
{/* Header Image */}
<div className="h-56 relative flex-shrink-0">
<img
src={shop.image}
alt={shop.name}
className="w-full h-full object-cover"
/>
{/* Top Fade/Shadow */}
<div className="absolute inset-0 bg-gradient-to-b from-black/60 via-transparent to-transparent" />
{/* Bottom Fade */}
<div className="absolute inset-0 bg-gradient-to-t from-black/90 to-transparent" />
<div className="absolute inset-0 z-0">
<img
src={shop.image}
alt={shop.name}
className="w-full h-full object-cover"
style={{
maskImage: 'linear-gradient(to bottom, black 50%, transparent 100%)',
WebkitMaskImage: 'linear-gradient(to bottom, black 50%, transparent 100%)'
}}
/>
</div>
<Button
variant="ghost"
size="icon"
onClick={onClose}
className="absolute top-4 right-4 bg-black/20 hover:bg-black/40 text-white rounded-full h-8 w-8 backdrop-blur-md border border-white/10"
className="absolute top-4 right-4 bg-background/20 hover:bg-background/40 text-foreground rounded-full h-8 w-8 backdrop-blur-md border border-border/50"
>
<X className="w-4 h-4" />
</Button>
@@ -54,35 +56,35 @@ export default function Drawer({ shop, onClose }: DrawerProps) {
{/* Content */}
<ScrollArea className="flex-1 -mt-12 relative z-10 min-h-0">
<div className="p-8">
<h2 className="text-3xl font-bold font-serif mb-4 text-[#D2691E] leading-tight">{shop.name}</h2>
<h2 className="text-3xl font-bold font-serif mb-4 text-primary leading-tight">{shop.name}</h2>
<div className="space-y-4 mb-8">
<div className="flex items-start gap-3 text-gray-300 font-serif text-sm">
<MapPin className="w-5 h-5 text-[#8B4513] flex-shrink-0 mt-0.5" />
<div className="flex items-start gap-3 text-muted-foreground font-serif text-sm">
<MapPin className="w-5 h-5 text-primary flex-shrink-0 mt-0.5" />
<span>{shop.address}</span>
</div>
{shop.phone && (
<div className="flex items-center gap-3 text-gray-300 font-serif text-sm">
<Phone className="w-5 h-5 text-[#8B4513] flex-shrink-0" />
<a href={`tel:${shop.phone}`} className="hover:text-white transition-colors">{shop.phone}</a>
<div className="flex items-center gap-3 text-muted-foreground font-serif text-sm">
<Phone className="w-5 h-5 text-primary flex-shrink-0" />
<a href={`tel:${shop.phone}`} className="hover:text-foreground transition-colors">{shop.phone}</a>
</div>
)}
{shop.website && (
<div className="flex items-center gap-3 text-gray-300 font-serif text-sm">
<Globe className="w-5 h-5 text-[#8B4513] flex-shrink-0" />
<a href={shop.website} target="_blank" rel="noopener noreferrer" className="hover:text-white transition-colors flex items-center gap-1">
<div className="flex items-center gap-3 text-muted-foreground font-serif text-sm">
<Globe className="w-5 h-5 text-primary flex-shrink-0" />
<a href={shop.website} target="_blank" rel="noopener noreferrer" className="hover:text-foreground transition-colors flex items-center gap-1">
Visit Website <ExternalLink className="w-3 h-3" />
</a>
</div>
)}
</div>
<Separator className="bg-white/10 mb-6" />
<Separator className="bg-border/50 mb-6" />
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold mb-2 text-gray-200 font-serif">About</h3>
<p className="text-gray-300 leading-relaxed font-serif text-lg">
<h3 className="text-lg font-semibold mb-2 text-foreground font-serif">About</h3>
<p className="text-muted-foreground leading-relaxed font-serif text-lg">
{shop.description}
</p>
</div>
@@ -90,7 +92,7 @@ export default function Drawer({ shop, onClose }: DrawerProps) {
<Button
asChild
size="sm"
className="w-auto px-6 bg-[#8B4513]/20 hover:bg-[#8B4513]/40 text-white font-semibold rounded-lg shadow-lg transition-all hover:scale-[1.02] border border-[#8B4513]/50 backdrop-blur-md"
className="w-auto px-6 bg-primary/20 hover:bg-primary/40 text-foreground font-semibold rounded-lg shadow-lg transition-all hover:scale-[1.02] border border-primary/50 backdrop-blur-md"
>
<a
href={`https://www.google.com/maps/search/?api=1&query=${shop.lat},${shop.lng}`}

View File

@@ -3,10 +3,13 @@
import { MapContainer, TileLayer, Marker } from 'react-leaflet';
import 'leaflet/dist/leaflet.css';
import L from 'leaflet';
import { useEffect } from 'react';
import { useState, useEffect } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { Coffee } from 'lucide-react';
import { useTheme } from "next-themes";
import Navbar from "./Navbar";
import { MapStyleControl } from "./MapStyleControl";
import { ZoomControls } from "./ZoomControls";
interface CoffeeShop {
id: number;
@@ -51,6 +54,51 @@ const Map = ({ shops, onShopSelect }: MapProps) => {
const customIcon = createCustomIcon();
const { resolvedTheme, setTheme } = useTheme();
const [mapStyle, setMapStyle] = useState(resolvedTheme === 'light' ? 'light' : 'dark');
// Sync map style with theme
useEffect(() => {
if (resolvedTheme === 'dark' && mapStyle === 'light') {
setMapStyle('dark');
} else if (resolvedTheme === 'light' && (mapStyle === 'dark' || mapStyle === 'satellite')) {
setMapStyle('light');
}
}, [resolvedTheme, mapStyle]);
// Handle manual style change
const handleStyleChange = (newStyle: string) => {
setMapStyle(newStyle);
if (newStyle === 'dark') {
setTheme('dark');
} else if (newStyle === 'light') {
setTheme('light');
} else if (newStyle === 'satellite') {
setTheme('dark');
}
};
const getTileLayer = () => {
switch (mapStyle) {
case "light":
return "https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png";
case "satellite":
return "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}";
case "dark":
default:
return "https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png";
}
};
const getAttribution = () => {
switch (mapStyle) {
case "satellite":
return 'Tiles &copy; Esri &mdash; Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community';
default:
return '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <a href="https://carto.com/attributions">CARTO</a>';
}
};
return (
<MapContainer
center={[40.9645, -76.8845]}
@@ -61,9 +109,13 @@ const Map = ({ shops, onShopSelect }: MapProps) => {
attributionControl={false}
>
<Navbar />
<div className="absolute bottom-8 right-4 z-[1000] flex flex-col gap-2 items-end">
<ZoomControls />
<MapStyleControl currentStyle={mapStyle} onStyleChange={handleStyleChange} />
</div>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <a href="https://carto.com/attributions">CARTO</a>'
url="https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png"
attribution={getAttribution()}
url={getTileLayer()}
/>
{shops.map((shop) => (
<Marker

View File

@@ -0,0 +1,43 @@
"use client";
import * as React from "react";
import { Layers, Moon, Sun, Globe } from "lucide-react";
import { Button } from "~/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "~/components/ui/dropdown-menu";
interface MapStyleControlProps {
currentStyle: string;
onStyleChange: (style: string) => void;
}
export function MapStyleControl({ currentStyle, onStyleChange }: MapStyleControlProps) {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon" className="bg-background/60 dark:bg-background/60 backdrop-blur-2xl border-border/50 dark:border-border/50 h-10 w-10 rounded-lg shadow-2xl text-foreground">
<Layers className="h-5 w-5" />
<span className="sr-only">Change map style</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-40 bg-background/90 backdrop-blur-xl">
<DropdownMenuItem onClick={() => onStyleChange("dark")} className={currentStyle === "dark" ? "bg-accent" : ""}>
<Moon className="mr-2 h-4 w-4" />
Dark
</DropdownMenuItem>
<DropdownMenuItem onClick={() => onStyleChange("light")} className={currentStyle === "light" ? "bg-accent" : ""}>
<Sun className="mr-2 h-4 w-4" />
Light
</DropdownMenuItem>
<DropdownMenuItem onClick={() => onStyleChange("satellite")} className={currentStyle === "satellite" ? "bg-accent" : ""}>
<Globe className="mr-2 h-4 w-4" />
Satellite
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@@ -1,65 +1,30 @@
import { Coffee, Plus, Minus, Home, Info, X } from "lucide-react";
import { Card } from "~/components/ui/card";
import { Coffee, X } from "lucide-react";
import { Button } from "~/components/ui/button";
import { useMap } from "react-leaflet";
import { useState } from "react";
export default function Navbar() {
const map = useMap();
const [showAbout, setShowAbout] = useState(false);
return (
<>
<div className="absolute top-0 left-0 right-0 z-[1000] p-4 pointer-events-none">
<Card className="flex flex-row items-center justify-between px-5 py-3 bg-black/60 backdrop-blur-2xl border-white/10 rounded-xl shadow-2xl pointer-events-auto w-full">
<div className="flex items-center gap-3">
<Coffee className="w-6 h-6 text-[#8B4513]" />
<h1 className="text-xl font-bold text-white font-serif tracking-wide leading-none pt-0.5">
<div className="flex flex-row items-center justify-between px-5 py-3 bg-background/60 backdrop-blur-2xl border border-border/50 rounded-lg shadow-2xl pointer-events-auto w-full">
<div
className="flex items-center gap-3 cursor-pointer hover:opacity-80 transition-opacity"
onClick={() => window.dispatchEvent(new Event("show-welcome-modal"))}
>
<Coffee className="w-6 h-6 text-primary" />
<h1 className="text-xl font-bold text-foreground font-serif tracking-wide leading-none pt-0.5">
Lewisburg Coffee
</h1>
</div>
<div className="flex items-center gap-2">
{/* <Button
variant="ghost"
size="icon"
onClick={() => setShowAbout(true)}
className="bg-black/20 hover:bg-black/40 text-white rounded-lg h-8 w-8 backdrop-blur-md border border-white/10 mr-2"
>
<Info className="w-4 h-4" />
</Button> */}
<Button
variant="ghost"
size="icon"
onClick={() => map.setView([40.9645, -76.8845], 15)}
className="bg-black/20 hover:bg-black/40 text-white rounded-lg h-8 w-8 backdrop-blur-md border border-white/10"
>
<Home className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => map.zoomOut()}
className="bg-black/20 hover:bg-black/40 text-white rounded-lg h-8 w-8 backdrop-blur-md border border-white/10"
>
<Minus className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => map.zoomIn()}
className="bg-black/20 hover:bg-black/40 text-white rounded-lg h-8 w-8 backdrop-blur-md border border-white/10"
>
<Plus className="w-4 h-4" />
</Button>
</div>
</Card>
</div>
</div>
{/* About Dialog Overlay */}
{showAbout && (
<div className="absolute inset-0 z-[2000] flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
<Card className="w-full max-w-md bg-black/60 backdrop-blur-2xl border-white/10 text-white p-6 relative shadow-2xl rounded-xl">
<div className="w-full max-w-md bg-black/60 backdrop-blur-2xl border border-white/10 text-white p-6 relative shadow-2xl rounded-xl">
<Button
variant="ghost"
size="icon"
@@ -87,7 +52,7 @@ export default function Navbar() {
<p>Built with Next.js, Tailwind, and Leaflet</p>
</div>
</div>
</Card>
</div>
</div>
)}
</>

View File

@@ -0,0 +1,11 @@
"use client";
import * as React from "react";
import { ThemeProvider as NextThemesProvider } from "next-themes";
export function ThemeProvider({
children,
...props
}: React.ComponentProps<typeof NextThemesProvider>) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}

View File

@@ -0,0 +1,73 @@
"use client";
import { useEffect, useState } from "react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "~/components/ui/dialog";
import { Button } from "~/components/ui/button";
import { Coffee, MapPin } from "lucide-react";
export function WelcomeModal() {
const [open, setOpen] = useState(false);
useEffect(() => {
const hasSeenWelcome = localStorage.getItem("hasSeenWelcome");
if (!hasSeenWelcome) {
setOpen(true);
}
const handleShowWelcome = () => setOpen(true);
window.addEventListener("show-welcome-modal", handleShowWelcome);
return () => window.removeEventListener("show-welcome-modal", handleShowWelcome);
}, []);
const handleClose = () => {
setOpen(false);
localStorage.setItem("hasSeenWelcome", "true");
};
return (
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="sm:max-w-md bg-background/80 backdrop-blur-xl border-border/50">
<DialogHeader>
<div className="mx-auto bg-primary/10 p-3 rounded-full mb-4 w-fit">
<Coffee className="h-8 w-8 text-primary" />
</div>
<DialogTitle className="text-center text-2xl font-serif">Welcome to Lewisburg Coffee</DialogTitle>
<DialogDescription className="text-center text-muted-foreground pt-2">
Discover the best coffee spots in Lewisburg, PA.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="flex items-start gap-4 p-4 rounded-lg bg-muted/50 border border-border/50">
<MapPin className="h-5 w-5 text-primary mt-0.5" />
<div className="space-y-1">
<h4 className="text-sm font-medium leading-none">Explore the Map</h4>
<p className="text-sm text-muted-foreground">
Navigate through the town to find your next caffeine fix.
</p>
</div>
</div>
<div className="flex items-start gap-4 p-4 rounded-lg bg-muted/50 border border-border/50">
<Coffee className="h-5 w-5 text-primary mt-0.5" />
<div className="space-y-1">
<h4 className="text-sm font-medium leading-none">View Details</h4>
<p className="text-sm text-muted-foreground">
Click on any marker to see photos, hours, and get directions.
</p>
</div>
</div>
</div>
<div className="flex justify-center">
<Button onClick={handleClose} className="w-full sm:w-auto min-w-[120px]">
Start Exploring
</Button>
</div>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,41 @@
"use client";
import { Home, Minus, Plus } from "lucide-react";
import { Button } from "~/components/ui/button";
import { useMap } from "react-leaflet";
export function ZoomControls() {
const map = useMap();
return (
<div className="flex flex-col gap-2">
<Button
variant="outline"
size="icon"
onClick={() => map.zoomIn()}
className="bg-background/60 dark:bg-background/60 backdrop-blur-2xl border-border/50 dark:border-border/50 h-10 w-10 rounded-lg shadow-2xl text-foreground"
>
<Plus className="h-5 w-5" />
<span className="sr-only">Zoom in</span>
</Button>
<Button
variant="outline"
size="icon"
onClick={() => map.zoomOut()}
className="bg-background/60 dark:bg-background/60 backdrop-blur-2xl border-border/50 dark:border-border/50 h-10 w-10 rounded-lg shadow-2xl text-foreground"
>
<Minus className="h-5 w-5" />
<span className="sr-only">Zoom out</span>
</Button>
<Button
variant="outline"
size="icon"
onClick={() => map.setView([40.9645, -76.8845], 15)}
className="bg-background/60 dark:bg-background/60 backdrop-blur-2xl border-border/50 dark:border-border/50 h-10 w-10 rounded-lg shadow-2xl text-foreground"
>
<Home className="h-5 w-5" />
<span className="sr-only">Reset view</span>
</Button>
</div>
);
}

View File

@@ -0,0 +1,122 @@
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import { cn } from "~/lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}

View File

@@ -0,0 +1,200 @@
"use client"
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { Check, ChevronRight, Circle } from "lucide-react"
import { cn } from "~/lib/utils"
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
)
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}

View File

@@ -129,7 +129,7 @@ export const COFFEE_SHOPS = [
address: "600 N Derr Dr, Lewisburg, PA 17837",
phone: "(570) 524-4900",
website: "https://www.dunkindonuts.com",
image: "https://lh3.googleusercontent.com/gps-cs-s/AG0ilSzX2AHOPwtciA9yCg3RBi8aiZ3Ra-3693zBfqT3sHFxIHayMAHG2y9C1GkvW4210xhVx4YjiJ5D2U-XeRHJ1-jwHU42Mw_uciGnyGTtr3Ovl_IsVxDIuphqg9twFtbovZoNI7dO=w408-h307-k-no"
image: "https://bloximages.newyork1.vip.townnews.com/northcentralpa.com/content/tncms/assets/v3/editorial/2/87/2872aa54-5fab-11eb-9cb0-53994a2d0d92/600fc9c4b9859.image.jpg?resize=400%2C232"
},
{
id: 13,