feat: Implement glassmorphism UI with amber accents, add About and Copyright components, and update license to GPLv3.

This commit is contained in:
Sean O'Connor
2026-02-03 18:30:32 -05:00
parent e89a6946cb
commit b7fad1e691
16 changed files with 339 additions and 179 deletions

View File

@@ -68,4 +68,4 @@ bun start
## License
MIT
GPLv3

View File

@@ -7,6 +7,10 @@ import "./src/env.js";
/** @type {import("next").NextConfig} */
const config = {
output: "export",
basePath: process.env.NODE_ENV === "production" ? "/lewisburg-coffee" : undefined,
images: {
unoptimized: true,
},
};
export default config;

View File

@@ -4,6 +4,7 @@ import { type Metadata, type Viewport } from "next";
import { PT_Serif } from "next/font/google";
import { ThemeProvider } from "~/components/ThemeProvider";
import { CopyrightFooter } from "~/components/CopyrightFooter";
import { env } from "~/env";
export const metadata: Metadata = {
@@ -30,7 +31,7 @@ export default function RootLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="en" className={`${ptSerif.variable}`} suppressHydrationWarning>
<html lang="en" className={`${ptSerif.variable} `} suppressHydrationWarning>
<head>
{/* only load analytics on production */}
{process.env.NODE_ENV === "production" && (
@@ -49,13 +50,10 @@ export default function RootLayout({
disableTransitionOnChange
>
{children}
<div className="pointer-events-none fixed bottom-0 left-0 right-0 z-50 flex justify-end p-4">
<div className="pointer-events-auto rounded-xl border border-white/20 bg-black/20 px-4 py-2 text-xs text-white/80 shadow-lg backdrop-blur-md transition-opacity hover:opacity-100">
© 2026 Sean O'Connor. All Rights Reserved.
</div>
</div>
<CopyrightFooter />
</ThemeProvider>
</body>
</html>
);
}

View File

@@ -4,12 +4,12 @@ import { useState, useEffect } from "react";
import MapLoader from "~/components/MapLoader";
import Drawer from "~/components/Drawer";
import Navbar from "~/components/Navbar";
import { COFFEE_SHOPS } from "~/lib/data";
import { COFFEE_SHOPS, type CoffeeShop } from "~/lib/data";
import { WelcomeModal } from "~/components/WelcomeModal";
export default function HomePage() {
const [selectedShop, setSelectedShop] = useState<typeof COFFEE_SHOPS[0] | null>(null);
const [selectedShop, setSelectedShop] = useState<CoffeeShop | null>(null);
const [isDiscoveryOpen, setIsDiscoveryOpen] = useState(true); // Default to true for SSR
const [mounted, setMounted] = useState(false);

View File

@@ -0,0 +1,69 @@
"use client";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "~/components/ui/dialog";
import { Coffee, Github, Heart } from "lucide-react";
import { Button } from "./ui/button";
interface AboutModalProps {
isOpen: boolean;
onClose: () => void;
}
export function AboutModal({ isOpen, onClose }: AboutModalProps) {
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="sm:max-w-md bg-glass-background backdrop-blur-xl border-glass-border text-glass-text-primary">
<DialogHeader>
<DialogTitle className="flex items-center gap-2 text-2xl font-serif">
<Coffee className="h-6 w-6 text-amber-500" />
Lewisburg Coffee Map
</DialogTitle>
<DialogDescription className="text-glass-text-secondary">
Discover the best coffee spots in Lewisburg, PA.
</DialogDescription>
</DialogHeader>
<div className="space-y-6 py-4">
<div className="space-y-2">
<h3 className="font-semibold text-amber-500">About the Project</h3>
<p className="text-sm text-glass-text-primary leading-relaxed">
A modern, interactive map application built to help locals and visitors find their perfect cup of coffee.
Featuring real-time location services, dark/light mode, and a premium seamless experience.
</p>
</div>
<div className="space-y-2">
<h3 className="font-semibold text-amber-500">Attribution</h3>
<ul className="text-sm text-glass-text-secondary space-y-1 list-disc pl-4">
<li>Created by <span className="text-glass-text-primary font-medium">Sean O'Connor</span></li>
<li>Built with <span className="text-glass-text-primary font-medium">Next.js 15</span> & <span className="text-glass-text-primary font-medium">React 19</span></li>
<li>Styled with <span className="text-glass-text-primary font-medium">Tailwind CSS v4</span></li>
<li>Map data provided by <span className="text-glass-text-primary font-medium">OpenStreetMap</span> & <span className="text-glass-text-primary font-medium">Leaflet</span></li>
<li>Icons by <span className="text-glass-text-primary font-medium">Lucide React</span></li>
</ul>
</div>
<div className="pt-4 border-t border-glass-border flex flex-col gap-2">
<p className="text-xs text-center text-glass-text-secondary flex items-center justify-center gap-1">
© 2026 Sean O'Connor. All Rights Reserved.
</p>
<p className="text-xs text-center text-glass-text-secondary flex items-center justify-center gap-1">
Licensed under GPLv3. <Heart className="h-3 w-3 text-red-500 fill-red-500" />
</p>
</div>
</div>
<div className="flex justify-center">
<Button
variant="outline"
className="gap-2 border-glass-border bg-glass-border/20 hover:bg-glass-border text-glass-text-primary hover:text-white"
asChild
>
<a href="https://github.com/soconnor0919/lewisburg-coffee" target="_blank" rel="noopener noreferrer">
<Github className="h-4 w-4" />
View on GitHub
</a>
</Button>
</div>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,22 @@
"use client";
import { useState } from "react";
import { AboutModal } from "./AboutModal";
export function CopyrightFooter() {
const [isAboutOpen, setIsAboutOpen] = useState(false);
return (
<>
<div className="pointer-events-none fixed bottom-0 left-0 right-0 z-50 flex justify-end p-4">
<button
onClick={() => setIsAboutOpen(true)}
className="pointer-events-auto rounded-xl border border-glass-border bg-glass-background px-4 py-2 text-xs text-glass-text-secondary shadow-lg backdrop-blur-md transition-all hover:bg-glass-border hover:text-glass-text-primary hover:scale-105 active:scale-95 cursor-pointer"
>
© 2026 Sean O'Connor. All Rights Reserved.
</button>
</div>
<AboutModal isOpen={isAboutOpen} onClose={() => setIsAboutOpen(false)} />
</>
);
}

View File

@@ -1,4 +1,5 @@
import { X, MapPin, Globe, Phone, Coffee, ExternalLink, Search, ChevronRight, ChevronLeft } from "lucide-react";
import { X, MapPin, Globe, Phone, Coffee, ExternalLink, Search, ChevronRight, ChevronLeft, Navigation, Heart } from "lucide-react";
import Image from "next/image";
import { Card } from "~/components/ui/card";
import { Button } from "~/components/ui/button";
import { ScrollArea } from "~/components/ui/scroll-area";
@@ -6,18 +7,7 @@ import { Separator } from "~/components/ui/separator";
import { Skeleton } from "~/components/ui/skeleton";
import { Input } from "~/components/ui/input";
import { useState, useEffect } from "react";
interface CoffeeShop {
id: number;
name: string;
description: string;
image: string;
address: string;
phone: string;
website: string;
lat: number;
lng: number;
}
import { type CoffeeShop } from "~/lib/data";
interface DrawerProps {
shop: CoffeeShop | null;
@@ -32,9 +22,34 @@ export default function Drawer({ shop, shops, onSelect, onClose, isOpen, onToggl
const [searchQuery, setSearchQuery] = useState("");
const [imageLoading, setImageLoading] = useState(true);
const [activeShop, setActiveShop] = useState<CoffeeShop | null>(shop);
const [favorites, setFavorites] = useState<Set<number>>(new Set());
// Load favorites from local storage on mount
useEffect(() => {
const saved = localStorage.getItem('lewisburg-coffee-favorites');
if (saved) {
try {
setFavorites(new Set(JSON.parse(saved)));
} catch (e) {
console.error("Failed to parse favorites", e);
}
}
}, []);
// Save favorites to local storage whenever they change
const toggleFavorite = (id: number, e?: React.MouseEvent) => {
e?.stopPropagation();
const newFavorites = new Set(favorites);
if (newFavorites.has(id)) {
newFavorites.delete(id);
} else {
newFavorites.add(id);
}
setFavorites(newFavorites);
localStorage.setItem('lewisburg-coffee-favorites', JSON.stringify(Array.from(newFavorites)));
};
// Update activeShop when shop changes, but only if it's not null
// This allows us to keep displaying the shop details while animating out
useEffect(() => {
if (shop) {
setActiveShop(shop);
@@ -47,23 +62,39 @@ export default function Drawer({ shop, shops, onSelect, onClose, isOpen, onToggl
s.description.toLowerCase().includes(searchQuery.toLowerCase())
);
const favoriteShops = filteredShops.filter(s => favorites.has(s.id));
const otherShops = filteredShops.filter(s => !favorites.has(s.id));
const FADE_BOTTOM_STYLE = {
maskImage: 'linear-gradient(to bottom, black 50%, transparent 100%)',
WebkitMaskImage: 'linear-gradient(to bottom, black 50%, transparent 100%)'
};
return (
<div
className={`absolute top-20 left-0 h-[calc(100dvh-6rem)] w-full sm:w-[400px] z-30 px-4 pt-3 pointer-events-none transition-transform duration-300 ease-in-out ${isOpen || shop ? 'translate-x-0' : '-translate-x-full'}`}
>
<Card className="h-full w-full bg-background/60 dark:bg-background/65 backdrop-blur-2xl border-border/50 overflow-hidden relative shadow-xl rounded-r-xl border-0 pointer-events-auto">
<Card className="h-full w-full bg-glass-background backdrop-blur-xl border-glass-border overflow-hidden relative shadow-xl rounded-r-xl border-0 pointer-events-auto flex flex-col">
{/* Details View */}
<div
className={`absolute inset-0 z-20 transition-transform duration-300 ease-in-out bg-background/80 backdrop-blur-3xl ${shop ? 'translate-x-0 pointer-events-auto' : 'translate-x-full pointer-events-none'}`}
className={`absolute inset-0 z-20 transition-transform duration-300 ease-in-out bg-glass-background backdrop-blur-xl ${shop ? 'translate-x-0 pointer-events-auto' : 'translate-x-full pointer-events-none'}`}
>
{activeShop && (
<div className="h-full flex flex-col relative">
<div className="absolute top-4 right-4 z-50 flex gap-2">
<Button
variant="ghost"
size="icon"
onClick={(e) => toggleFavorite(activeShop.id, e)}
className="bg-black/40 hover:bg-black/60 text-white rounded-full h-8 w-8 backdrop-blur-md border border-glass-border"
>
<Heart className={`w-4 h-4 ${favorites.has(activeShop.id) ? 'fill-red-500 text-red-500' : 'text-white'}`} />
</Button>
<Button
variant="ghost"
size="icon"
onClick={onClose}
className="bg-background/20 hover:bg-background/40 text-foreground rounded-full h-8 w-8 backdrop-blur-md border border-border/50"
className="bg-black/40 hover:bg-black/60 text-white rounded-full h-8 w-8 backdrop-blur-md border border-glass-border"
>
<ChevronLeft className="w-4 h-4" />
</Button>
@@ -74,7 +105,7 @@ export default function Drawer({ shop, shops, onSelect, onClose, isOpen, onToggl
onClose();
if (onToggleOpen) onToggleOpen();
}}
className="bg-background/20 hover:bg-background/40 text-foreground rounded-full h-8 w-8 backdrop-blur-md border border-border/50"
className="bg-black/40 hover:bg-black/60 text-white rounded-full h-8 w-8 backdrop-blur-md border border-glass-border"
>
<X className="w-4 h-4" />
</Button>
@@ -86,77 +117,67 @@ export default function Drawer({ shop, shops, onSelect, onClose, isOpen, onToggl
{imageLoading && (
<div
className="absolute inset-0 z-10 flex items-center justify-center"
style={{
maskImage: 'linear-gradient(to bottom, black 50%, transparent 100%)',
WebkitMaskImage: 'linear-gradient(to bottom, black 50%, transparent 100%)'
}}
style={FADE_BOTTOM_STYLE}
>
<Skeleton className="h-full w-full absolute inset-0" />
<Coffee className="h-12 w-12 text-muted-foreground/50 animate-pulse relative z-20" />
<Skeleton className="h-full w-full absolute inset-0 bg-white/5" />
<Coffee className="h-12 w-12 text-white/20 animate-pulse relative z-20" />
</div>
)}
<div className="absolute inset-0 z-0">
<img
<Image
src={activeShop.image}
alt={activeShop.name}
className={`w-full h-full object-cover transition-opacity duration-500 ${imageLoading ? 'opacity-0' : 'opacity-100'}`}
fill
className={`object-cover transition-opacity duration-500 ${imageLoading ? 'opacity-0' : 'opacity-100'}`}
onLoad={() => setImageLoading(false)}
style={{
maskImage: 'linear-gradient(to bottom, black 50%, transparent 100%)',
WebkitMaskImage: 'linear-gradient(to bottom, black 50%, transparent 100%)'
}}
style={FADE_BOTTOM_STYLE}
sizes="(max-width: 640px) 100vw, 400px"
/>
</div>
</div>
{/* Content - Overlaps image slightly or just follows */}
<div className="p-8 -mt-12 relative z-10">
<h2 className="text-3xl font-bold font-serif mb-4 text-primary leading-tight drop-shadow-md">{activeShop.name}</h2>
<div className="space-y-4 mb-8">
<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>{activeShop.address}</span>
</div>
{activeShop.phone && (
<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:${activeShop.phone}`} className="hover:text-foreground transition-colors">{activeShop.phone}</a>
</div>
)}
{activeShop.website && (
<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={activeShop.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-border/50 mb-6" />
<div className="space-y-6">
<div className="px-6 pb-20 space-y-6 pt-6">
<div>
<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">
<h2 className="text-3xl font-bold font-serif mb-2 text-glass-text-primary leading-tight">{activeShop.name}</h2>
<div className="flex items-start gap-2 text-glass-text-secondary mb-4">
<MapPin className="w-4 h-4 mt-1 flex-shrink-0 text-amber-500" />
<p className="text-sm leading-relaxed">{activeShop.address}</p>
</div>
<p className="text-glass-text-primary leading-relaxed text-sm font-sans">
{activeShop.description}
</p>
</div>
<Button
asChild
size="sm"
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"
>
<div className="space-y-3">
<Button className="w-full bg-white text-black hover:bg-white/90 gap-2 font-medium" asChild>
<a
href={`https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(`${activeShop.name}, ${activeShop.address}`)}`}
href={`https://www.google.com/maps/dir/?api=1&destination=${activeShop.lat},${activeShop.lng}`}
target="_blank"
rel="noopener noreferrer"
>
<Navigation className="w-4 h-4" />
Get Directions
</a>
</Button>
<div className="grid grid-cols-2 gap-3">
{activeShop.website && (
<Button variant="outline" className="w-full border-glass-border text-glass-text-primary hover:bg-glass-border hover:text-white gap-2 text-xs" asChild>
<a href={activeShop.website} target="_blank" rel="noopener noreferrer">
<Globe className="w-3.5 h-3.5" />
Website
</a>
</Button>
)}
{activeShop.phone && (
<Button variant="outline" className="w-full border-glass-border text-glass-text-primary hover:bg-glass-border hover:text-white gap-2 text-xs" asChild>
<a href={`tel:${activeShop.phone}`}>
<Phone className="w-3.5 h-3.5" />
Call
</a>
</Button>
)}
</div>
</div>
</div>
</ScrollArea>
@@ -166,51 +187,56 @@ export default function Drawer({ shop, shops, onSelect, onClose, isOpen, onToggl
{/* List View */}
<div
className={`absolute inset-0 z-10 transition-all duration-300 ease-in-out flex flex-col h-full bg-background/0 ${shop ? '-translate-x-1/4 opacity-0 pointer-events-none' : 'translate-x-0 opacity-100 pointer-events-auto'}`}
className={`absolute inset-0 z-10 transition-all duration-300 ease-in-out flex flex-col h-full bg-transparent ${shop ? '-translate-x-1/4 opacity-0 pointer-events-none' : 'translate-x-0 opacity-100 pointer-events-auto'}`}
>
<div className="p-4 border-b border-border/50 relative">
<Button
variant="ghost"
size="icon"
onClick={onToggleOpen}
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>
<h2 className="text-xl font-bold font-serif mb-4 text-primary">Discover Coffee</h2>
<div className="relative">
<Search className="absolute left-3 top-2.5 h-4 w-4 text-muted-foreground" />
<div className="px-6 pt-6 pb-4 bg-transparent shrink-0">
<h2 className="text-2xl font-bold font-serif mb-4 flex items-center gap-2 text-glass-text-primary">
<Coffee className="h-6 w-6 text-amber-500" />
Coffee Shops
</h2>
<div className="relative group">
<Search className="absolute left-3 top-2.5 h-4 w-4 text-glass-text-secondary group-focus-within:text-amber-500 transition-colors" />
<Input
placeholder="Search shops..."
className="pl-9 bg-background/20 border-border/50 focus:bg-background/40 transition-colors"
placeholder="Search coffee shops..."
className="pl-9 bg-glass-border border-glass-border text-glass-text-primary placeholder:text-glass-text-secondary focus-visible:ring-amber-500/50 hover:bg-glass-border/80 transition-colors"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
</div>
<ScrollArea className="flex-1 min-h-0">
<div className="p-4 space-y-3">
{filteredShops.map((s) => (
<div
key={s.id}
onClick={() => 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"
>
<div className="h-16 w-16 rounded-md overflow-hidden flex-shrink-0 bg-muted relative">
<img src={s.image} alt={s.name} className="h-full w-full object-cover" />
</div>
<div className="flex-1 min-w-0">
<h3 className="font-semibold font-serif text-foreground truncate group-hover:text-primary transition-colors">{s.name}</h3>
<p className="text-sm text-muted-foreground truncate">{s.address}</p>
</div>
<ChevronRight className="h-5 w-5 text-muted-foreground group-hover:text-primary transition-colors" />
</div>
))}
{filteredShops.length === 0 && (
<div className="text-center py-8 text-muted-foreground">
<div className="p-4 space-y-6">
{filteredShops.length === 0 ? (
<div className="text-center py-8 text-white/40">
<Coffee className="h-12 w-12 mx-auto mb-3 opacity-20" />
<p>No shops found matching "{searchQuery}"</p>
</div>
) : (
<>
{favoriteShops.length > 0 && (
<div className="space-y-3">
<div className="flex items-center gap-2 px-2 text-xs font-medium text-amber-500 uppercase tracking-wider">
<Heart className="w-3 h-3 fill-amber-500" />
Favorites
</div>
{favoriteShops.map((s) => (
<ShopListItem key={s.id} shop={s} onSelect={onSelect} isFavorite={true} onToggleFavorite={toggleFavorite} />
))}
<Separator className="bg-glass-border my-4" />
</div>
)}
<div className="space-y-3">
{favoriteShops.length > 0 && (
<div className="flex items-center gap-2 px-2 text-xs font-medium text-glass-text-secondary uppercase tracking-wider">
All Shops
</div>
)}
{otherShops.map((s) => (
<ShopListItem key={s.id} shop={s} onSelect={onSelect} isFavorite={false} onToggleFavorite={toggleFavorite} />
))}
</div>
</>
)}
</div>
</ScrollArea>
@@ -219,3 +245,41 @@ export default function Drawer({ shop, shops, onSelect, onClose, isOpen, onToggl
</div>
);
}
function ShopListItem({ shop, onSelect, isFavorite, onToggleFavorite }: { shop: CoffeeShop, onSelect: (s: CoffeeShop) => void, isFavorite: boolean, onToggleFavorite: (id: number, e: React.MouseEvent) => void }) {
return (
<div
onClick={() => onSelect(shop)}
className="group flex items-start gap-3 p-3 rounded-lg hover:bg-glass-border transition-all cursor-pointer border border-transparent hover:border-amber-500/20 active:scale-[0.99]"
>
<div className="h-16 w-16 rounded-md overflow-hidden flex-shrink-0 bg-glass-border relative shadow-sm">
<Image
src={shop.image}
alt={shop.name}
fill
className="object-cover transition-transform duration-500 group-hover:scale-110"
sizes="64px"
/>
{isFavorite && (
<div className="absolute top-1 right-1 bg-black/60 backdrop-blur-sm p-1 rounded-full">
<Heart className="w-3 h-3 text-red-500 fill-red-500" />
</div>
)}
</div>
<div className="flex-1 min-w-0 flex flex-col gap-1">
<div className="flex items-start justify-between gap-2">
<h3 className="font-semibold font-serif text-glass-text-primary text-base leading-tight group-hover:text-amber-500 transition-colors relative top-[-2px]">{shop.name}</h3>
<Button
variant="ghost"
size="icon"
className="h-6 w-6 -mr-1 -mt-1 hover:bg-white/10 text-glass-text-secondary hover:text-red-500"
onClick={(e) => onToggleFavorite(shop.id, e)}
>
<Heart className={`w-4 h-4 ${isFavorite ? 'fill-red-500 text-red-500' : ''}`} />
</Button>
</div>
<p className="text-sm text-glass-text-secondary leading-snug group-hover:text-white/80 transition-colors line-clamp-2">{shop.address}</p>
</div>
</div>
);
}

View File

@@ -72,7 +72,7 @@ export function LocateControl() {
size="icon"
onClick={handleLocate}
disabled={loading}
className="bg-background/60 dark:bg-background/65 backdrop-blur-2xl border-border/50 dark:border-border/50 h-10 w-10 rounded-lg shadow-xl text-foreground"
className="rounded-xl border border-glass-border bg-glass-background text-glass-text-primary shadow-lg backdrop-blur-md transition-all hover:bg-glass-border hover:text-white"
>
<Locate className={`h-5 w-5 ${loading ? 'animate-pulse' : ''}`} />
<span className="sr-only">Locate me</span>

View File

@@ -8,18 +8,7 @@ import { useTheme } from "next-themes";
import { MapStyleControl } from "./MapStyleControl";
import { LocateControl } from './LocateControl';
import { ZoomControls } from "./ZoomControls";
interface CoffeeShop {
id: number;
name: string;
description: string;
lat: number;
lng: number;
address: string;
phone: string;
website: string;
image: string;
}
import { type CoffeeShop } from "~/lib/data";
interface MapProps {
shops: CoffeeShop[];
@@ -185,7 +174,7 @@ const Map = ({ shops, onShopSelect, selectedShop, isDiscoveryOpen }: MapProps) =
attributionControl={false}
>
<MapController selectedShop={selectedShop} isDiscoveryOpen={isDiscoveryOpen} />
<div className="absolute bottom-8 right-4 z-[1000] flex flex-col gap-2 items-end">
<div className="absolute bottom-20 right-4 z-[1000] flex flex-col gap-2 items-end">
<LocateControl />
<ZoomControls />
<MapStyleControl currentStyle={mapStyle} onStyleChange={handleStyleChange} />

View File

@@ -3,18 +3,7 @@
import dynamic from "next/dynamic";
import { Skeleton } from "~/components/ui/skeleton";
import { Coffee, Loader2 } from "lucide-react";
interface CoffeeShop {
id: number;
name: string;
description: string;
lat: number;
lng: number;
address: string;
phone: string;
website: string;
image: string;
}
import { type CoffeeShop } from "~/lib/data";
interface MapLoaderProps {
shops: CoffeeShop[];

View File

@@ -19,7 +19,7 @@ export function MapStyleControl({ currentStyle, onStyleChange }: MapStyleControl
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon" className="bg-background/60 dark:bg-background/65 backdrop-blur-2xl border-border/50 dark:border-border/50 h-10 w-10 rounded-lg shadow-xl text-foreground">
<Button variant="outline" size="icon" className="rounded-xl border border-glass-border bg-glass-background text-glass-text-primary shadow-lg backdrop-blur-md transition-all hover:bg-glass-border hover:text-white">
<Layers className="h-5 w-5" />
<span className="sr-only">Change map style</span>
</Button>

View File

@@ -40,12 +40,12 @@ export default function Navbar({ isDiscoveryOpen, onToggleDiscovery }: NavbarPro
return (
<>
<div className="absolute top-4 left-4 right-4 z-[1000] flex justify-center pointer-events-none">
<div className="bg-background/60 dark:bg-background/65 backdrop-blur-2xl border border-border/50 rounded-xl p-2 flex items-center justify-between w-full pointer-events-auto shadow-xl">
<div className="absolute top-4 left-4 right-4 z-40 flex justify-center pointer-events-none">
<div className="bg-glass-background backdrop-blur-xl border border-glass-border rounded-xl p-2 flex items-center justify-between w-full pointer-events-auto shadow-lg">
<div className="flex items-center gap-2 relative">
{/* Pulsing indicator ring - only during onboarding */}
{isOnboarding && showTooltip && (
<div className="absolute inset-0 rounded-lg animate-ping bg-primary/30 pointer-events-none" />
<div className="absolute inset-0 rounded-lg animate-ping bg-amber-500/30 pointer-events-none" />
)}
<TooltipProvider>
<Tooltip open={showTooltip} onOpenChange={setShowTooltip}>
@@ -59,13 +59,13 @@ export default function Navbar({ isDiscoveryOpen, onToggleDiscovery }: NavbarPro
setIsOnboarding(false);
localStorage.setItem('discovery-panel-hint-seen', 'true');
}}
className={`h-10 w-10 rounded-lg hover:bg-background/40 transition-colors ${isDiscoveryOpen ? 'bg-background/40 text-primary' : 'text-muted-foreground'}`}
className={`h-10 w-10 rounded-lg transition-colors ${isDiscoveryOpen ? 'bg-glass-border text-amber-500' : 'text-glass-text-primary hover:bg-glass-border hover:text-white'}`}
>
<Search className="h-5 w-5" />
<span className="sr-only">Toggle Panel</span>
</Button>
</TooltipTrigger>
<TooltipContent side="right" className="bg-background/80 backdrop-blur-xl border-border/50 text-foreground font-semibold font-serif shadow-2xl">
<TooltipContent side="right" className="bg-glass-background backdrop-blur-xl border-glass-border text-glass-text-primary font-semibold font-serif shadow-2xl">
<p>Discover Coffee Shops</p>
</TooltipContent>
</Tooltip>
@@ -76,12 +76,12 @@ export default function Navbar({ isDiscoveryOpen, onToggleDiscovery }: NavbarPro
className="flex items-center gap-3 px-2 cursor-pointer group absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2"
onClick={handleHeaderClick}
>
<div className="p-2 rounded-lg group-hover:bg-primary/10 transition-colors">
<Coffee className="h-6 w-6 text-primary" />
<div className="p-2 rounded-lg group-hover:bg-glass-border transition-colors">
<Coffee className="h-6 w-6 text-amber-500" />
</div>
<div>
<h1 className="text-lg font-bold font-serif text-foreground leading-none">Lewisburg&nbsp;Coffee&nbsp;Map</h1>
<p className="text-xs text-muted-foreground font-serif mt-0.5">Find&nbsp;your&nbsp;perfect&nbsp;brew</p>
<h1 className="text-lg font-bold font-serif text-glass-text-primary leading-none">Lewisburg&nbsp;Coffee&nbsp;Map</h1>
<p className="text-xs text-glass-text-secondary font-serif mt-0.5">Find&nbsp;your&nbsp;perfect&nbsp;brew</p>
</div>
</div>
@@ -92,30 +92,30 @@ export default function Navbar({ isDiscoveryOpen, onToggleDiscovery }: NavbarPro
{/* About Dialog Overlay */}
{showAbout && (
<div className="absolute inset-0 z-[2000] flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
<div className="w-full max-w-md bg-background/80 backdrop-blur-2xl border border-border/50 text-foreground p-6 relative shadow-2xl rounded-xl">
<div className="w-full max-w-md bg-glass-background backdrop-blur-xl border border-glass-border text-glass-text-primary p-6 relative shadow-2xl rounded-xl">
<Button
variant="ghost"
size="icon"
onClick={() => setShowAbout(false)}
className="absolute top-4 right-4 text-muted-foreground hover:text-foreground"
className="absolute top-4 right-4 text-glass-text-secondary hover:text-white hover:bg-glass-border"
>
<X className="w-5 h-5" />
</Button>
<div className="flex flex-col items-center text-center space-y-4">
<div className="p-3 bg-primary/20 rounded-full">
<Coffee className="w-8 h-8 text-primary" />
<div className="p-3 bg-white/5 rounded-full">
<Coffee className="w-8 h-8 text-amber-500" />
</div>
<h2 className="text-2xl font-bold font-serif">Lewisburg Coffee Map</h2>
<p className="text-muted-foreground font-serif leading-relaxed">
<p className="text-white/60 font-serif leading-relaxed">
Discover the best coffee spots in Lewisburg, PA. Click on any marker to learn more about each location,
or use the discovery panel to browse and search all available shops.
</p>
<div className="w-full h-px bg-border/50 my-4" />
<div className="w-full h-px bg-white/10 my-4" />
<div className="text-xs text-muted-foreground space-y-2 font-sans w-full text-left">
<p><strong>Features:</strong></p>
<div className="text-xs text-white/60 space-y-2 font-sans w-full text-left">
<p><strong className="text-white/80">Features:</strong></p>
<ul className="list-disc list-inside space-y-1 ml-2">
<li>Interactive map with coffee shop locations</li>
<li>Search and filter coffee shops</li>
@@ -123,9 +123,9 @@ export default function Navbar({ isDiscoveryOpen, onToggleDiscovery }: NavbarPro
<li>Get directions to any shop</li>
<li>Light/Dark theme support</li>
</ul>
<div className="pt-4 border-t border-border/50 mt-4">
<p>Map Data © <a href="https://www.openstreetmap.org/copyright" className="underline hover:text-foreground transition-colors">OpenStreetMap</a> contributors</p>
<p>Tiles © <a href="https://carto.com/attributions" className="underline hover:text-foreground transition-colors">CARTO</a></p>
<div className="pt-4 border-t border-white/10 mt-4">
<p>Map Data © <a href="https://www.openstreetmap.org/copyright" className="underline hover:text-white transition-colors">OpenStreetMap</a> contributors</p>
<p>Tiles © <a href="https://carto.com/attributions" className="underline hover:text-white transition-colors">CARTO</a></p>
</div>
</div>
</div>

View File

@@ -46,47 +46,49 @@ export function WelcomeModal() {
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className="sm:max-w-md bg-background/80 backdrop-blur-xl border-border/50 font-serif">
<DialogContent className="sm:max-w-md bg-glass-background backdrop-blur-xl border-glass-border font-serif text-glass-text-primary">
<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 className="mx-auto bg-glass-border p-3 rounded-full mb-4 w-fit">
<Coffee className="h-8 w-8 text-amber-500" />
</div>
<DialogTitle className="text-center text-2xl font-serif">Welcome to the Lewisburg&nbsp;Coffee&nbsp;Map</DialogTitle>
<DialogDescription className="text-center text-muted-foreground pt-2">
Discover the best coffee spots in Lewisburg, PA. Click on markers, search shops, and find your perfect brew.
<DialogDescription className="text-center text-glass-text-secondary pt-2">
Discover the best coffee spots in Lewisburg, PA.
<br />
<span className="text-xs text-amber-500/80 font-medium mt-1 block">Created by Sean O'Connor</span>
</DialogDescription>
</DialogHeader>
<div className="grid gap-3 py-4">
<div className="flex items-start gap-4 p-3 rounded-lg bg-muted/50 border border-border/50">
<MapPin className="h-5 w-5 text-primary mt-0.5 flex-shrink-0" />
<div className="flex items-start gap-4 p-3 rounded-lg bg-glass-border border border-glass-border">
<MapPin className="h-5 w-5 text-amber-500 mt-0.5 flex-shrink-0" />
<div className="space-y-1">
<h4 className="text-sm font-medium leading-none">Explore the Map</h4>
<p className="text-sm text-muted-foreground">
<h4 className="text-sm font-medium leading-none text-glass-text-primary">Explore the Map</h4>
<p className="text-sm text-glass-text-secondary">
Click any marker to see details, photos, and get directions.
</p>
</div>
</div>
<div className="flex items-start gap-4 p-3 rounded-lg bg-muted/50 border border-border/50">
<Search className="h-5 w-5 text-primary mt-0.5 flex-shrink-0" />
<div className="flex items-start gap-4 p-3 rounded-lg bg-glass-border border border-glass-border">
<Search className="h-5 w-5 text-amber-500 mt-0.5 flex-shrink-0" />
<div className="space-y-1">
<h4 className="text-sm font-medium leading-none">Search & Filter</h4>
<p className="text-sm text-muted-foreground">
<h4 className="text-sm font-medium leading-none text-glass-text-primary">Search & Filter</h4>
<p className="text-sm text-glass-text-secondary">
Use the discovery panel to browse and search all coffee shops.
</p>
</div>
</div>
<div className="flex items-start gap-4 p-3 rounded-lg bg-muted/50 border border-border/50">
<Navigation className="h-5 w-5 text-primary mt-0.5 flex-shrink-0" />
<div className="flex items-start gap-4 p-3 rounded-lg bg-glass-border border border-glass-border">
<Navigation className="h-5 w-5 text-amber-500 mt-0.5 flex-shrink-0" />
<div className="space-y-1">
<h4 className="text-sm font-medium leading-none">Find Your Location</h4>
<p className="text-sm text-muted-foreground">
<h4 className="text-sm font-medium leading-none text-glass-text-primary">Find Your Location</h4>
<p className="text-sm text-glass-text-secondary">
Use the locate button to center the map on your current position.
</p>
</div>
</div>
</div>
<div className="flex justify-center">
<Button onClick={handleClose} className="w-full sm:w-auto min-w-[120px]">
<Button onClick={handleClose} className="w-full sm:w-auto min-w-[120px] bg-white text-black hover:bg-white/90">
Start Exploring
</Button>
</div>

View File

@@ -13,7 +13,7 @@ export function ZoomControls() {
variant="outline"
size="icon"
onClick={() => map.zoomIn()}
className="bg-background/60 dark:bg-background/65 backdrop-blur-2xl border-border/50 dark:border-border/50 h-10 w-10 rounded-lg shadow-xl text-foreground hover:bg-background/70 dark:hover:bg-background/75"
className="rounded-xl border border-glass-border bg-glass-background text-glass-text-primary shadow-lg backdrop-blur-md transition-all hover:bg-glass-border hover:text-white"
>
<Plus className="h-5 w-5" />
<span className="sr-only">Zoom in</span>
@@ -22,7 +22,7 @@ export function ZoomControls() {
variant="outline"
size="icon"
onClick={() => map.zoomOut()}
className="bg-background/60 dark:bg-background/65 backdrop-blur-2xl border-border/50 dark:border-border/50 h-10 w-10 rounded-lg shadow-xl text-foreground hover:bg-background/70 dark:hover:bg-background/75"
className="rounded-xl border border-glass-border bg-glass-background text-glass-text-primary shadow-lg backdrop-blur-md transition-all hover:bg-glass-border hover:text-white"
>
<Minus className="h-5 w-5" />
<span className="sr-only">Zoom out</span>
@@ -31,7 +31,7 @@ export function ZoomControls() {
variant="outline"
size="icon"
onClick={() => map.setView([40.9645, -76.8845], 15)}
className="bg-background/60 dark:bg-background/65 backdrop-blur-2xl border-border/50 dark:border-border/50 h-10 w-10 rounded-lg shadow-xl text-foreground hover:bg-background/70 dark:hover:bg-background/75"
className="rounded-xl border border-glass-border bg-glass-background text-glass-text-primary shadow-lg backdrop-blur-md transition-all hover:bg-glass-border hover:text-white"
>
<Home className="h-5 w-5" />
<span className="sr-only">Reset view</span>

View File

@@ -1,4 +1,16 @@
export const COFFEE_SHOPS = [
export interface CoffeeShop {
id: number;
name: string;
description: string;
lat: number;
lng: number;
address: string;
phone: string;
website: string;
image: string;
}
export const COFFEE_SHOPS: CoffeeShop[] = [
{
id: 1,
name: "Amami Kitchen",

View File

@@ -44,11 +44,22 @@
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
--color-glass-background: var(--glass-background);
--color-glass-border: var(--glass-border);
--color-glass-text-primary: var(--glass-text-primary);
--color-glass-text-secondary: var(--glass-text-secondary);
}
:root {
--radius: 0.625rem;
/* ... existing vars ... */
--glass-background: rgba(0, 0, 0, 0.7);
--glass-border: rgba(255, 255, 255, 0.15);
--glass-text-primary: rgba(255, 255, 255, 0.95);
--glass-text-secondary: rgba(255, 255, 255, 0.70);
--background: oklch(1 0 0);
/* ... */
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);