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 ## License
MIT GPLv3

View File

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

View File

@@ -4,6 +4,7 @@ import { type Metadata, type Viewport } from "next";
import { PT_Serif } from "next/font/google"; import { PT_Serif } from "next/font/google";
import { ThemeProvider } from "~/components/ThemeProvider"; import { ThemeProvider } from "~/components/ThemeProvider";
import { CopyrightFooter } from "~/components/CopyrightFooter";
import { env } from "~/env"; import { env } from "~/env";
export const metadata: Metadata = { export const metadata: Metadata = {
@@ -49,13 +50,10 @@ export default function RootLayout({
disableTransitionOnChange disableTransitionOnChange
> >
{children} {children}
<div className="pointer-events-none fixed bottom-0 left-0 right-0 z-50 flex justify-end p-4"> <CopyrightFooter />
<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>
</ThemeProvider> </ThemeProvider>
</body> </body>
</html> </html>
); );
} }

View File

@@ -4,12 +4,12 @@ import { useState, useEffect } from "react";
import MapLoader from "~/components/MapLoader"; import MapLoader from "~/components/MapLoader";
import Drawer from "~/components/Drawer"; import Drawer from "~/components/Drawer";
import Navbar from "~/components/Navbar"; import Navbar from "~/components/Navbar";
import { COFFEE_SHOPS } from "~/lib/data"; import { COFFEE_SHOPS, type CoffeeShop } from "~/lib/data";
import { WelcomeModal } from "~/components/WelcomeModal"; import { WelcomeModal } from "~/components/WelcomeModal";
export default function HomePage() { 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 [isDiscoveryOpen, setIsDiscoveryOpen] = useState(true); // Default to true for SSR
const [mounted, setMounted] = useState(false); 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 { Card } from "~/components/ui/card";
import { Button } from "~/components/ui/button"; import { Button } from "~/components/ui/button";
import { ScrollArea } from "~/components/ui/scroll-area"; import { ScrollArea } from "~/components/ui/scroll-area";
@@ -6,18 +7,7 @@ import { Separator } from "~/components/ui/separator";
import { Skeleton } from "~/components/ui/skeleton"; import { Skeleton } from "~/components/ui/skeleton";
import { Input } from "~/components/ui/input"; import { Input } from "~/components/ui/input";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { type CoffeeShop } from "~/lib/data";
interface CoffeeShop {
id: number;
name: string;
description: string;
image: string;
address: string;
phone: string;
website: string;
lat: number;
lng: number;
}
interface DrawerProps { interface DrawerProps {
shop: CoffeeShop | null; shop: CoffeeShop | null;
@@ -32,9 +22,34 @@ export default function Drawer({ shop, shops, onSelect, onClose, isOpen, onToggl
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
const [imageLoading, setImageLoading] = useState(true); const [imageLoading, setImageLoading] = useState(true);
const [activeShop, setActiveShop] = useState<CoffeeShop | null>(shop); 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 // 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(() => { useEffect(() => {
if (shop) { if (shop) {
setActiveShop(shop); setActiveShop(shop);
@@ -47,23 +62,39 @@ export default function Drawer({ shop, shops, onSelect, onClose, isOpen, onToggl
s.description.toLowerCase().includes(searchQuery.toLowerCase()) 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 ( return (
<div <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'}`} 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 */} {/* Details View */}
<div <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 && ( {activeShop && (
<div className="h-full flex flex-col relative"> <div className="h-full flex flex-col relative">
<div className="absolute top-4 right-4 z-50 flex gap-2"> <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 <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
onClick={onClose} 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" /> <ChevronLeft className="w-4 h-4" />
</Button> </Button>
@@ -74,7 +105,7 @@ export default function Drawer({ shop, shops, onSelect, onClose, isOpen, onToggl
onClose(); onClose();
if (onToggleOpen) onToggleOpen(); 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" /> <X className="w-4 h-4" />
</Button> </Button>
@@ -86,77 +117,67 @@ export default function Drawer({ shop, shops, onSelect, onClose, isOpen, onToggl
{imageLoading && ( {imageLoading && (
<div <div
className="absolute inset-0 z-10 flex items-center justify-center" className="absolute inset-0 z-10 flex items-center justify-center"
style={{ style={FADE_BOTTOM_STYLE}
maskImage: 'linear-gradient(to bottom, black 50%, transparent 100%)',
WebkitMaskImage: 'linear-gradient(to bottom, black 50%, transparent 100%)'
}}
> >
<Skeleton className="h-full w-full absolute inset-0" /> <Skeleton className="h-full w-full absolute inset-0 bg-white/5" />
<Coffee className="h-12 w-12 text-muted-foreground/50 animate-pulse relative z-20" /> <Coffee className="h-12 w-12 text-white/20 animate-pulse relative z-20" />
</div> </div>
)} )}
<div className="absolute inset-0 z-0"> <div className="absolute inset-0 z-0">
<img <Image
src={activeShop.image} src={activeShop.image}
alt={activeShop.name} 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)} onLoad={() => setImageLoading(false)}
style={{ style={FADE_BOTTOM_STYLE}
maskImage: 'linear-gradient(to bottom, black 50%, transparent 100%)', sizes="(max-width: 640px) 100vw, 400px"
WebkitMaskImage: 'linear-gradient(to bottom, black 50%, transparent 100%)'
}}
/> />
</div> </div>
</div> </div>
{/* Content - Overlaps image slightly or just follows */} {/* Content - Overlaps image slightly or just follows */}
<div className="p-8 -mt-12 relative z-10"> <div className="px-6 pb-20 space-y-6 pt-6">
<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> <div>
<h3 className="text-lg font-semibold mb-2 text-foreground font-serif">About</h3> <h2 className="text-3xl font-bold font-serif mb-2 text-glass-text-primary leading-tight">{activeShop.name}</h2>
<p className="text-muted-foreground leading-relaxed font-serif text-lg"> <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} {activeShop.description}
</p> </p>
</div> </div>
<Button <div className="space-y-3">
asChild <Button className="w-full bg-white text-black hover:bg-white/90 gap-2 font-medium" 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"
>
<a <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" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
<Navigation className="w-4 h-4" />
Get Directions Get Directions
</a> </a>
</Button> </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>
</div> </div>
</ScrollArea> </ScrollArea>
@@ -166,51 +187,56 @@ export default function Drawer({ shop, shops, onSelect, onClose, isOpen, onToggl
{/* List View */} {/* List View */}
<div <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"> <div className="px-6 pt-6 pb-4 bg-transparent shrink-0">
<Button <h2 className="text-2xl font-bold font-serif mb-4 flex items-center gap-2 text-glass-text-primary">
variant="ghost" <Coffee className="h-6 w-6 text-amber-500" />
size="icon" Coffee Shops
onClick={onToggleOpen} </h2>
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" <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" />
<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" />
<Input <Input
placeholder="Search shops..." placeholder="Search coffee shops..."
className="pl-9 bg-background/20 border-border/50 focus:bg-background/40 transition-colors" 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} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
/> />
</div> </div>
</div> </div>
<ScrollArea className="flex-1 min-h-0"> <ScrollArea className="flex-1 min-h-0">
<div className="p-4 space-y-3"> <div className="p-4 space-y-6">
{filteredShops.map((s) => ( {filteredShops.length === 0 ? (
<div <div className="text-center py-8 text-white/40">
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">
<Coffee className="h-12 w-12 mx-auto mb-3 opacity-20" /> <Coffee className="h-12 w-12 mx-auto mb-3 opacity-20" />
<p>No shops found matching "{searchQuery}"</p> <p>No shops found matching "{searchQuery}"</p>
</div> </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> </div>
</ScrollArea> </ScrollArea>
@@ -219,3 +245,41 @@ export default function Drawer({ shop, shops, onSelect, onClose, isOpen, onToggl
</div> </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" size="icon"
onClick={handleLocate} onClick={handleLocate}
disabled={loading} 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' : ''}`} /> <Locate className={`h-5 w-5 ${loading ? 'animate-pulse' : ''}`} />
<span className="sr-only">Locate me</span> <span className="sr-only">Locate me</span>

View File

@@ -8,18 +8,7 @@ import { useTheme } from "next-themes";
import { MapStyleControl } from "./MapStyleControl"; import { MapStyleControl } from "./MapStyleControl";
import { LocateControl } from './LocateControl'; import { LocateControl } from './LocateControl';
import { ZoomControls } from "./ZoomControls"; import { ZoomControls } from "./ZoomControls";
import { type CoffeeShop } from "~/lib/data";
interface CoffeeShop {
id: number;
name: string;
description: string;
lat: number;
lng: number;
address: string;
phone: string;
website: string;
image: string;
}
interface MapProps { interface MapProps {
shops: CoffeeShop[]; shops: CoffeeShop[];
@@ -185,7 +174,7 @@ const Map = ({ shops, onShopSelect, selectedShop, isDiscoveryOpen }: MapProps) =
attributionControl={false} attributionControl={false}
> >
<MapController selectedShop={selectedShop} isDiscoveryOpen={isDiscoveryOpen} /> <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 /> <LocateControl />
<ZoomControls /> <ZoomControls />
<MapStyleControl currentStyle={mapStyle} onStyleChange={handleStyleChange} /> <MapStyleControl currentStyle={mapStyle} onStyleChange={handleStyleChange} />

View File

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

View File

@@ -19,7 +19,7 @@ export function MapStyleControl({ currentStyle, onStyleChange }: MapStyleControl
return ( return (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <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" /> <Layers className="h-5 w-5" />
<span className="sr-only">Change map style</span> <span className="sr-only">Change map style</span>
</Button> </Button>

View File

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

View File

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

View File

@@ -13,7 +13,7 @@ export function ZoomControls() {
variant="outline" variant="outline"
size="icon" size="icon"
onClick={() => map.zoomIn()} 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" /> <Plus className="h-5 w-5" />
<span className="sr-only">Zoom in</span> <span className="sr-only">Zoom in</span>
@@ -22,7 +22,7 @@ export function ZoomControls() {
variant="outline" variant="outline"
size="icon" size="icon"
onClick={() => map.zoomOut()} 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" /> <Minus className="h-5 w-5" />
<span className="sr-only">Zoom out</span> <span className="sr-only">Zoom out</span>
@@ -31,7 +31,7 @@ export function ZoomControls() {
variant="outline" variant="outline"
size="icon" size="icon"
onClick={() => map.setView([40.9645, -76.8845], 15)} 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" /> <Home className="h-5 w-5" />
<span className="sr-only">Reset view</span> <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, id: 1,
name: "Amami Kitchen", name: "Amami Kitchen",

View File

@@ -44,11 +44,22 @@
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground); --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border); --color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring); --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 { :root {
--radius: 0.625rem; --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); --background: oklch(1 0 0);
/* ... */
--foreground: oklch(0.145 0 0); --foreground: oklch(0.145 0 0);
--card: oklch(1 0 0); --card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0); --card-foreground: oklch(0.145 0 0);