mirror of
https://github.com/soconnor0919/lewisburg-coffee.git
synced 2026-02-04 23:56:32 -05:00
feat: Refactor drawer for improved animations and update shadow styles across components
This commit is contained in:
@@ -31,11 +31,14 @@ export default function RootLayout({
|
|||||||
return (
|
return (
|
||||||
<html lang="en" className={`${ptSerif.variable}`} suppressHydrationWarning>
|
<html lang="en" className={`${ptSerif.variable}`} suppressHydrationWarning>
|
||||||
<head>
|
<head>
|
||||||
<script
|
{/* only load analytics on production */}
|
||||||
defer
|
{process.env.NODE_ENV === "production" && (
|
||||||
src="https://umami-iccw808w4wk088o0w4o8c8kg.coolify.soconnor.dev/script.js"
|
<script
|
||||||
data-website-id="415c64e5-98c5-4975-bf49-2c900fe6b1b5"
|
defer
|
||||||
/>
|
src="https://umami-iccw808w4wk088o0w4o8c8kg.coolify.soconnor.dev/script.js"
|
||||||
|
data-website-id="415c64e5-98c5-4975-bf49-2c900fe6b1b5"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<ThemeProvider
|
<ThemeProvider
|
||||||
|
|||||||
@@ -22,23 +22,20 @@ export default function HomePage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="relative h-dvh w-screen overflow-hidden bg-black text-white font-serif">
|
<main className="relative h-dvh w-screen overflow-hidden bg-black text-white font-serif">
|
||||||
{/* Unified shadow container for navbar + drawer */}
|
{/* Navbar - always visible */}
|
||||||
<div className="absolute top-0 left-0 right-0 bottom-0 pointer-events-none z-[1000]" style={{ boxShadow: 'inset 0 0 40px 10px rgb(0 0 0 / 0.3)' }}>
|
<Navbar isDiscoveryOpen={isDiscoveryOpen} onToggleDiscovery={() => setIsDiscoveryOpen(!isDiscoveryOpen)} />
|
||||||
{/* Navbar - always visible */}
|
|
||||||
<Navbar isDiscoveryOpen={isDiscoveryOpen} onToggleDiscovery={() => setIsDiscoveryOpen(!isDiscoveryOpen)} />
|
|
||||||
|
|
||||||
{/* Right Drawer - only render after mount to prevent hydration mismatch */}
|
{/* Right Drawer - only render after mount to prevent hydration mismatch */}
|
||||||
{mounted && (
|
{mounted && (
|
||||||
<Drawer
|
<Drawer
|
||||||
shop={selectedShop}
|
shop={selectedShop}
|
||||||
shops={COFFEE_SHOPS}
|
shops={COFFEE_SHOPS}
|
||||||
onSelect={setSelectedShop}
|
onSelect={setSelectedShop}
|
||||||
onClose={() => setSelectedShop(null)}
|
onClose={() => setSelectedShop(null)}
|
||||||
isOpen={isDiscoveryOpen}
|
isOpen={isDiscoveryOpen}
|
||||||
onToggleOpen={() => setIsDiscoveryOpen(false)}
|
onToggleOpen={() => setIsDiscoveryOpen(false)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Map Background */}
|
{/* Map Background */}
|
||||||
<div className="absolute inset-0 z-0">
|
<div className="absolute inset-0 z-0">
|
||||||
@@ -48,6 +45,7 @@ export default function HomePage() {
|
|||||||
setSelectedShop(shop);
|
setSelectedShop(shop);
|
||||||
}}
|
}}
|
||||||
selectedShop={selectedShop}
|
selectedShop={selectedShop}
|
||||||
|
isDiscoveryOpen={isDiscoveryOpen}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { X, MapPin, Globe, Phone, Coffee, ExternalLink, Search, ChevronRight } from "lucide-react";
|
import { X, MapPin, Globe, Phone, Coffee, ExternalLink, Search, ChevronRight, ChevronLeft } from "lucide-react";
|
||||||
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";
|
||||||
@@ -31,10 +31,13 @@ interface DrawerProps {
|
|||||||
export default function Drawer({ shop, shops, onSelect, onClose, isOpen, onToggleOpen }: DrawerProps) {
|
export default function Drawer({ shop, shops, onSelect, onClose, isOpen, onToggleOpen }: DrawerProps) {
|
||||||
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);
|
||||||
|
|
||||||
// Reset loading state when shop changes
|
// 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);
|
||||||
setImageLoading(true);
|
setImageLoading(true);
|
||||||
}
|
}
|
||||||
}, [shop]);
|
}, [shop]);
|
||||||
@@ -46,154 +49,172 @@ export default function Drawer({ shop, shops, onSelect, onClose, isOpen, onToggl
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`absolute top-20 left-0 h-[calc(100dvh-6rem)] w-full sm:w-[400px] z-30 p-4 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 flex flex-col gap-0 pointer-events-auto rounded-r-xl p-0 border-0 transition-all duration-300">
|
<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">
|
||||||
{shop ? (
|
{/* Details View */}
|
||||||
// Details View
|
<div
|
||||||
// Details View
|
className={`absolute inset-0 z-20 transition-transform duration-300 ease-in-out bg-background/80 backdrop-blur-3xl ${shop ? 'translate-x-0' : 'translate-x-full'}`}
|
||||||
<div className="h-full flex flex-col relative">
|
>
|
||||||
|
{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={onClose}
|
||||||
|
className="bg-background/20 hover:bg-background/40 text-foreground rounded-full h-8 w-8 backdrop-blur-md border border-border/50"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => {
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ScrollArea className="flex-1 min-h-0">
|
||||||
|
{/* Header Image - Now part of scroll area */}
|
||||||
|
<div className="h-64 relative w-full bg-muted/20">
|
||||||
|
{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%)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Skeleton className="h-full w-full absolute inset-0" />
|
||||||
|
<Coffee className="h-12 w-12 text-muted-foreground/50 animate-pulse relative z-20" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="absolute inset-0 z-0">
|
||||||
|
<img
|
||||||
|
src={activeShop.image}
|
||||||
|
alt={activeShop.name}
|
||||||
|
className={`w-full h-full 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%)'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</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>
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href={`https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(`${activeShop.name}, ${activeShop.address}`)}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
Get Directions
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 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'}`}
|
||||||
|
>
|
||||||
|
<div className="p-4 border-b border-border/50 relative">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={onClose}
|
onClick={onToggleOpen}
|
||||||
className="absolute top-4 right-4 z-50 bg-background/20 hover:bg-background/40 text-foreground rounded-full h-8 w-8 backdrop-blur-md border border-border/50"
|
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" />
|
<X className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
<h2 className="text-xl font-bold font-serif mb-4 text-primary">Discover Coffee</h2>
|
||||||
<ScrollArea className="flex-1 min-h-0">
|
<div className="relative">
|
||||||
{/* Header Image - Now part of scroll area */}
|
<Search className="absolute left-3 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||||
<div className="h-64 relative w-full bg-muted/20">
|
<Input
|
||||||
{imageLoading && (
|
placeholder="Search shops..."
|
||||||
<div
|
className="pl-9 bg-background/20 border-border/50 focus:bg-background/40 transition-colors"
|
||||||
className="absolute inset-0 z-10 flex items-center justify-center"
|
value={searchQuery}
|
||||||
style={{
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
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" />
|
|
||||||
<Coffee className="h-12 w-12 text-muted-foreground/50 animate-pulse relative z-20" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="absolute inset-0 z-0">
|
|
||||||
<img
|
|
||||||
src={shop.image}
|
|
||||||
alt={shop.name}
|
|
||||||
className={`w-full h-full 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%)'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</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">{shop.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>{shop.address}</span>
|
|
||||||
</div>
|
|
||||||
{shop.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:${shop.phone}`} className="hover:text-foreground transition-colors">{shop.phone}</a>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{shop.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={shop.website} target="_blank" rel="noopener noreferrer" className="hover:text-foreground transition-colors flex items-center gap-1">
|
|
||||||
Visit Website <ExternalLink className="w-3 h-3" />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Separator className="bg-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">
|
|
||||||
{shop.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"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
href={`https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(`${shop.name}, ${shop.address}`)}`}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
Get Directions
|
|
||||||
</a>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ScrollArea>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
// List View
|
|
||||||
<div className="flex flex-col h-full">
|
|
||||||
<div className="p-4 border-b border-border/50 bg-background/40 backdrop-blur-md 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" />
|
|
||||||
<Input
|
|
||||||
placeholder="Search shops..."
|
|
||||||
className="pl-9 bg-background/50 border-border/50 focus:bg-background transition-colors"
|
|
||||||
value={searchQuery}
|
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</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">
|
|
||||||
<Coffee className="h-12 w-12 mx-auto mb-3 opacity-20" />
|
|
||||||
<p>No shops found matching "{searchQuery}"</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</ScrollArea>
|
|
||||||
</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">
|
||||||
|
<Coffee className="h-12 w-12 mx-auto mb-3 opacity-20" />
|
||||||
|
<p>No shops found matching "{searchQuery}"</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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-2xl text-foreground"
|
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"
|
||||||
>
|
>
|
||||||
<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>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { MapContainer, TileLayer, Marker, Tooltip, useMap } from 'react-leaflet';
|
import { MapContainer, TileLayer, Marker, Tooltip, useMap } from 'react-leaflet';
|
||||||
import 'leaflet/dist/leaflet.css';
|
import 'leaflet/dist/leaflet.css';
|
||||||
import L from 'leaflet';
|
import L from 'leaflet';
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
import { MapStyleControl } from "./MapStyleControl";
|
import { MapStyleControl } from "./MapStyleControl";
|
||||||
import { LocateControl } from './LocateControl';
|
import { LocateControl } from './LocateControl';
|
||||||
@@ -25,24 +25,91 @@ interface MapProps {
|
|||||||
shops: CoffeeShop[];
|
shops: CoffeeShop[];
|
||||||
onShopSelect: (shop: CoffeeShop) => void;
|
onShopSelect: (shop: CoffeeShop) => void;
|
||||||
selectedShop: CoffeeShop | null;
|
selectedShop: CoffeeShop | null;
|
||||||
|
isDiscoveryOpen: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MapController = ({ selectedShop }: { selectedShop: CoffeeShop | null }) => {
|
const MapController = ({ selectedShop, isDiscoveryOpen }: { selectedShop: CoffeeShop | null, isDiscoveryOpen: boolean }) => {
|
||||||
const map = useMap();
|
const map = useMap();
|
||||||
|
const prevOpenRef = useRef(isDiscoveryOpen);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Handle general panning when drawer toggles (and no shop is selected)
|
||||||
|
const isDesktop = window.innerWidth >= 640;
|
||||||
|
|
||||||
|
if (prevOpenRef.current !== isDiscoveryOpen) {
|
||||||
|
if (!selectedShop && isDesktop) {
|
||||||
|
// If opening, we want to shift the view to the right (so panning map left? wait)
|
||||||
|
// If drawer opens on LEFT, the center of the remaining view is to the RIGHT.
|
||||||
|
// We want the content to move into that new center.
|
||||||
|
// So we pan the map in the POSITIVE X direction?
|
||||||
|
// Test: map.panBy([200, 0]) moves the map image 200px to the RIGHT relative to the viewport.
|
||||||
|
// This means a point at x=0 moves to x=200.
|
||||||
|
// This puts it into the open area. Correct.
|
||||||
|
|
||||||
|
// Inverting based on user feedback that it shifted wrong way.
|
||||||
|
// Opening -> Shift Map Image Left (View Right?) -> offset [-200, 0]
|
||||||
|
// Closing -> Shift Map Image Right (View Left?) -> offset [200, 0]
|
||||||
|
const offset = isDiscoveryOpen ? [-200, 0] : [200, 0];
|
||||||
|
map.panBy(offset as [number, number], {
|
||||||
|
animate: true,
|
||||||
|
duration: 0.5
|
||||||
|
});
|
||||||
|
}
|
||||||
|
prevOpenRef.current = isDiscoveryOpen;
|
||||||
|
}
|
||||||
|
}, [isDiscoveryOpen, selectedShop, map]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedShop) {
|
if (selectedShop) {
|
||||||
map.flyTo([selectedShop.lat, selectedShop.lng], 16, {
|
const targetLat = selectedShop.lat;
|
||||||
|
const targetLng = selectedShop.lng;
|
||||||
|
|
||||||
|
// Calculate offset if discovery panel is open and we're on desktop
|
||||||
|
let flyToOption = {
|
||||||
duration: 1.5,
|
duration: 1.5,
|
||||||
easeLinearity: 0.25,
|
easeLinearity: 0.25,
|
||||||
});
|
};
|
||||||
|
|
||||||
|
const isDesktop = window.innerWidth >= 640;
|
||||||
|
|
||||||
|
if (isDiscoveryOpen && isDesktop) {
|
||||||
|
// We need to offset the center so the point appears to the right of the drawer
|
||||||
|
// The drawer is 400px wide. We want to shift the center left by 200px (half drawer width)
|
||||||
|
// so that the target point appears 200px to the right of current center
|
||||||
|
|
||||||
|
// Get current zoom
|
||||||
|
const zoom = 16;
|
||||||
|
|
||||||
|
// Project the lat/lng to point
|
||||||
|
const point = map.project([targetLat, targetLng], zoom);
|
||||||
|
|
||||||
|
// Subtract offset (shift 'center' to the left, which moves 'view' to the right? Wait.)
|
||||||
|
// If we want the point to be at x + 200 (screen coords relative to center),
|
||||||
|
// we need the map center to be at x - 200 relative to point.
|
||||||
|
// Actually simpler: We want the point (targetLat, targetLng) to be at screen coordinates (width/2 + 200, height/2).
|
||||||
|
// Or simply: shift the target point by -200px in x before passing to flyTo? No, flyTo takes strict LatLng.
|
||||||
|
|
||||||
|
// Correct approach:
|
||||||
|
// Find the LatLng that, when centered, puts our target LatLng at the desired pixels.
|
||||||
|
// Center + Offset = Target -> Center = Target - Offset
|
||||||
|
|
||||||
|
const targetPoint = map.project([targetLat, targetLng], zoom);
|
||||||
|
// We want the target to appear 200px (half drawer width) to the right of the map center.
|
||||||
|
// So the new center should be 200px LEFT of the target.
|
||||||
|
const newCenterPoint = targetPoint.subtract([200, 0]);
|
||||||
|
const newCenter = map.unproject(newCenterPoint, zoom);
|
||||||
|
|
||||||
|
map.flyTo(newCenter, zoom, flyToOption);
|
||||||
|
} else {
|
||||||
|
map.flyTo([targetLat, targetLng], 16, flyToOption);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [selectedShop, map]);
|
}, [selectedShop, map, isDiscoveryOpen]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Map = ({ shops, onShopSelect, selectedShop }: MapProps) => {
|
const Map = ({ shops, onShopSelect, selectedShop, isDiscoveryOpen }: MapProps) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Fix for Leaflet default icon not found
|
// Fix for Leaflet default icon not found
|
||||||
// @ts-expect-error Fix for Leaflet default icon not found
|
// @ts-expect-error Fix for Leaflet default icon not found
|
||||||
@@ -122,7 +189,7 @@ const Map = ({ shops, onShopSelect, selectedShop }: MapProps) => {
|
|||||||
zoomControl={false}
|
zoomControl={false}
|
||||||
attributionControl={false}
|
attributionControl={false}
|
||||||
>
|
>
|
||||||
<MapController selectedShop={selectedShop} />
|
<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-8 right-4 z-[1000] flex flex-col gap-2 items-end">
|
||||||
<LocateControl />
|
<LocateControl />
|
||||||
<ZoomControls />
|
<ZoomControls />
|
||||||
@@ -141,7 +208,14 @@ const Map = ({ shops, onShopSelect, selectedShop }: MapProps) => {
|
|||||||
click: () => onShopSelect(shop),
|
click: () => onShopSelect(shop),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Tooltip direction="top" offset={[0, -20]} opacity={0.9}>
|
<Tooltip
|
||||||
|
key={`${shop.id}-${selectedShop?.id === shop.id}`}
|
||||||
|
direction="top"
|
||||||
|
offset={[0, -20]}
|
||||||
|
opacity={0.9}
|
||||||
|
permanent={selectedShop?.id === shop.id}
|
||||||
|
className={selectedShop?.id === shop.id ? 'force-show' : ''}
|
||||||
|
>
|
||||||
<div className="font-serif font-semibold text-sm">
|
<div className="font-serif font-semibold text-sm">
|
||||||
{shop.name}
|
{shop.name}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ interface MapLoaderProps {
|
|||||||
shops: CoffeeShop[];
|
shops: CoffeeShop[];
|
||||||
onShopSelect: (shop: CoffeeShop) => void;
|
onShopSelect: (shop: CoffeeShop) => void;
|
||||||
selectedShop: CoffeeShop | null;
|
selectedShop: CoffeeShop | null;
|
||||||
|
isDiscoveryOpen: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move dynamic import outside component to prevent re-imports
|
// Move dynamic import outside component to prevent re-imports
|
||||||
@@ -36,6 +37,6 @@ const Map = dynamic(() => import("./Map"), {
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function MapLoader({ shops, onShopSelect, selectedShop }: MapLoaderProps) {
|
export default function MapLoader({ shops, onShopSelect, selectedShop, isDiscoveryOpen }: MapLoaderProps) {
|
||||||
return <Map shops={shops} onShopSelect={onShopSelect} selectedShop={selectedShop} />;
|
return <Map shops={shops} onShopSelect={onShopSelect} selectedShop={selectedShop} isDiscoveryOpen={isDiscoveryOpen} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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-2xl text-foreground">
|
<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">
|
||||||
<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>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Coffee, PanelLeft, X } from "lucide-react";
|
import { Coffee, Search, X } from "lucide-react";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "~/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "~/components/ui/tooltip";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
@@ -41,7 +41,7 @@ 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-[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">
|
<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="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 && (
|
||||||
@@ -61,11 +61,11 @@ export default function Navbar({ isDiscoveryOpen, onToggleDiscovery }: NavbarPro
|
|||||||
}}
|
}}
|
||||||
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 hover:bg-background/40 transition-colors ${isDiscoveryOpen ? 'bg-background/40 text-primary' : 'text-muted-foreground'}`}
|
||||||
>
|
>
|
||||||
<PanelLeft 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="bottom" className="bg-background/80 backdrop-blur-xl border-border/50 text-foreground font-semibold shadow-2xl">
|
<TooltipContent side="right" className="bg-background/80 backdrop-blur-xl border-border/50 text-foreground font-semibold font-serif shadow-2xl">
|
||||||
<p>Discover Coffee Shops</p>
|
<p>Discover Coffee Shops</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ 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">
|
<DialogContent className="sm:max-w-md bg-background/80 backdrop-blur-xl border-border/50 font-serif">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<div className="mx-auto bg-primary/10 p-3 rounded-full mb-4 w-fit">
|
<div className="mx-auto bg-primary/10 p-3 rounded-full mb-4 w-fit">
|
||||||
<Coffee className="h-8 w-8 text-primary" />
|
<Coffee className="h-8 w-8 text-primary" />
|
||||||
|
|||||||
@@ -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-2xl text-foreground hover:bg-background/70 dark:hover:bg-background/75"
|
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"
|
||||||
>
|
>
|
||||||
<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-2xl text-foreground hover:bg-background/70 dark:hover:bg-background/75"
|
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"
|
||||||
>
|
>
|
||||||
<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-2xl text-foreground hover:bg-background/70 dark:hover:bg-background/75"
|
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"
|
||||||
>
|
>
|
||||||
<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>
|
||||||
|
|||||||
Reference in New Issue
Block a user