mirror of
https://github.com/soconnor0919/lewisburg-coffee.git
synced 2026-02-05 08:06:32 -05:00
feat: Implement glassmorphism UI with amber accents, add About and Copyright components, and update license to GPLv3.
This commit is contained in:
69
src/components/AboutModal.tsx
Normal file
69
src/components/AboutModal.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
22
src/components/CopyrightFooter.tsx
Normal file
22
src/components/CopyrightFooter.tsx
Normal 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)} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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 className="px-6 pb-20 space-y-6 pt-6">
|
||||
<div>
|
||||
<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>
|
||||
{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>
|
||||
)}
|
||||
<p className="text-glass-text-primary leading-relaxed text-sm font-sans">
|
||||
{activeShop.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Separator className="bg-border/50 mb-6" />
|
||||
|
||||
<div className="space-y-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">
|
||||
{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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 Coffee Map</h1>
|
||||
<p className="text-xs text-muted-foreground font-serif mt-0.5">Find your perfect brew</p>
|
||||
<h1 className="text-lg font-bold font-serif text-glass-text-primary leading-none">Lewisburg Coffee Map</h1>
|
||||
<p className="text-xs text-glass-text-secondary font-serif mt-0.5">Find your perfect 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>
|
||||
|
||||
@@ -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 Coffee 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user