Add light/dark mode with toggle, fix all theme-aware text colors

This commit is contained in:
Andy
2026-03-21 12:18:14 +00:00
parent 4741546adf
commit 75e1d0083f
9 changed files with 211 additions and 91 deletions

View File

@@ -1,6 +1,6 @@
"use client" "use client"
import { useEffect, useRef } from "react" import { useEffect } from "react"
import { MapContainer, TileLayer, Marker, Popup, useMap } from "react-leaflet" import { MapContainer, TileLayer, Marker, Popup, useMap } from "react-leaflet"
import L from "leaflet" import L from "leaflet"
import "leaflet/dist/leaflet.css" import "leaflet/dist/leaflet.css"
@@ -14,7 +14,7 @@ function createAmberIcon(isSelected: boolean) {
<svg width="${size}" height="${size}" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="${size}" height="${size}" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="16" cy="14" r="10" fill="${isSelected ? "#f59e0b" : "#d97706"}" stroke="${isSelected ? "#fff" : "#f59e0b"}" stroke-width="2"/> <circle cx="16" cy="14" r="10" fill="${isSelected ? "#f59e0b" : "#d97706"}" stroke="${isSelected ? "#fff" : "#f59e0b"}" stroke-width="2"/>
<path d="M16 28 L10 20 Q16 22 22 20 Z" fill="${isSelected ? "#f59e0b" : "#d97706"}"/> <path d="M16 28 L10 20 Q16 22 22 20 Z" fill="${isSelected ? "#f59e0b" : "#d97706"}"/>
<circle cx="16" cy="14" r="4" fill="${isSelected ? "#000" : "#0d0d0d"}"/> <circle cx="16" cy="14" r="4" fill="${isSelected ? "#000" : "#333"}"/>
</svg> </svg>
` `
return L.divIcon({ return L.divIcon({
@@ -53,24 +53,30 @@ interface Props {
restaurants: Restaurant[] restaurants: Restaurant[]
selectedId: string | null selectedId: string | null
onSelect: (id: string | null) => void onSelect: (id: string | null) => void
theme?: "light" | "dark"
} }
export default function MapView({ restaurants, selectedId, onSelect }: Props) { export default function MapView({ restaurants, selectedId, onSelect, theme = "dark" }: Props) {
const center: [number, number] = const center: [number, number] =
restaurants.length > 0 restaurants.length > 0
? [restaurants[0].lat, restaurants[0].lng] ? [restaurants[0].lat, restaurants[0].lng]
: [51.5, 10] : [51.5, 10]
const tileUrl = theme === "dark"
? "https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png"
: "https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png"
return ( return (
<MapContainer <MapContainer
center={center} center={center}
zoom={5} zoom={5}
style={{ height: "100%", width: "100%", background: "#0d0d0d" }} style={{ height: "100%", width: "100%", background: theme === "dark" ? "#0d0d0d" : "#f0f0f0" }}
zoomControl={false} zoomControl={false}
> >
<TileLayer <TileLayer
key={tileUrl}
attribution='&copy; <a href="https://carto.com/">CARTO</a>' attribution='&copy; <a href="https://carto.com/">CARTO</a>'
url="https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png" url={tileUrl}
subdomains="abcd" subdomains="abcd"
maxZoom={20} maxZoom={20}
/> />
@@ -88,8 +94,8 @@ export default function MapView({ restaurants, selectedId, onSelect }: Props) {
> >
<Popup> <Popup>
<div className="min-w-[180px]"> <div className="min-w-[180px]">
<p className="font-bold text-[#f5f5f5] text-base leading-tight">{r.name}</p> <p className="font-bold text-base leading-tight">{r.name}</p>
<p className="text-white/50 text-xs mt-1"> <p className="text-xs mt-1 opacity-50">
{r.city}, {r.country} {r.city}, {r.country}
</p> </p>
<div className="flex flex-wrap gap-1 mt-2"> <div className="flex flex-wrap gap-1 mt-2">
@@ -106,7 +112,7 @@ export default function MapView({ restaurants, selectedId, onSelect }: Props) {
<span className="text-[#f59e0b] font-black text-lg"> <span className="text-[#f59e0b] font-black text-lg">
{avgRating(r).toFixed(1)} {avgRating(r).toFixed(1)}
</span> </span>
<span className="text-white/40 text-xs"> <span className="text-xs opacity-40">
{r.visits.length} {r.visits.length === 1 ? "visit" : "visits"} {r.visits.length} {r.visits.length === 1 ? "visit" : "visits"}
</span> </span>
</div> </div>

View File

@@ -3,20 +3,46 @@
import Link from "next/link" import Link from "next/link"
import { usePathname } from "next/navigation" import { usePathname } from "next/navigation"
import { motion } from "framer-motion" import { motion } from "framer-motion"
import { useTheme } from "@/app/components/ThemeProvider"
const navItems = [ const navItems = [
{ href: "/", label: "Home", icon: "◈" }, { href: "/", label: "Home", icon: "◈" },
{ href: "/map", label: "Map", icon: "◉" }, { href: "/map", label: "Map", icon: "◉" },
] ]
function SunIcon() {
return (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="5"/>
<line x1="12" y1="1" x2="12" y2="3"/>
<line x1="12" y1="21" x2="12" y2="23"/>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/>
<line x1="1" y1="12" x2="3" y2="12"/>
<line x1="21" y1="12" x2="23" y2="12"/>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
</svg>
)
}
function MoonIcon() {
return (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
</svg>
)
}
export default function Nav() { export default function Nav() {
const pathname = usePathname() const pathname = usePathname()
const { theme, toggle } = useTheme()
return ( return (
<> <>
{/* Desktop top nav */} {/* Desktop top nav */}
<nav className="hidden md:flex items-center justify-between px-8 py-5 border-b border-white/5 sticky top-0 z-50 bg-[#0d0d0d]/80 backdrop-blur-xl"> <nav className="hidden md:flex items-center justify-between px-8 py-5 border-b border-black/[0.06] dark:border-white/[0.05] sticky top-0 z-50 bg-[#fafafa]/80 dark:bg-[#0d0d0d]/80 backdrop-blur-xl">
<Link href="/" className="text-2xl font-black tracking-tighter text-[#f5f5f5] hover:text-[#f59e0b] transition-colors"> <Link href="/" className="text-2xl font-black tracking-tighter text-[#111] dark:text-[#f5f5f5] hover:text-[#f59e0b] dark:hover:text-[#f59e0b] transition-colors">
food. food.
</Link> </Link>
<div className="flex items-center gap-8"> <div className="flex items-center gap-8">
@@ -25,17 +51,26 @@ export default function Nav() {
key={item.href} key={item.href}
href={item.href} href={item.href}
className={`text-sm font-medium transition-colors hover:text-[#f59e0b] ${ className={`text-sm font-medium transition-colors hover:text-[#f59e0b] ${
pathname === item.href ? "text-[#f59e0b]" : "text-white/60" pathname === item.href
? "text-[#f59e0b]"
: "text-black/50 dark:text-white/60"
}`} }`}
> >
{item.label} {item.label}
</Link> </Link>
))} ))}
<button
onClick={toggle}
aria-label="Toggle theme"
className="text-black/40 dark:text-white/40 hover:text-[#f59e0b] dark:hover:text-[#f59e0b] transition-colors p-1"
>
{theme === "dark" ? <SunIcon /> : <MoonIcon />}
</button>
</div> </div>
</nav> </nav>
{/* Mobile bottom nav */} {/* Mobile bottom nav */}
<nav className="md:hidden fixed bottom-0 left-0 right-0 z-50 border-t border-white/5 bg-[#0d0d0d]/90 backdrop-blur-xl"> <nav className="md:hidden fixed bottom-0 left-0 right-0 z-50 border-t border-black/[0.06] dark:border-white/[0.05] bg-[#fafafa]/90 dark:bg-[#0d0d0d]/90 backdrop-blur-xl">
<div className="flex items-center justify-around py-3"> <div className="flex items-center justify-around py-3">
{navItems.map((item) => { {navItems.map((item) => {
const isActive = pathname === item.href const isActive = pathname === item.href
@@ -48,14 +83,14 @@ export default function Nav() {
<motion.div <motion.div
whileTap={{ scale: 0.85 }} whileTap={{ scale: 0.85 }}
className={`text-xl transition-colors ${ className={`text-xl transition-colors ${
isActive ? "text-[#f59e0b]" : "text-white/40" isActive ? "text-[#f59e0b]" : "text-black/30 dark:text-white/40"
}`} }`}
> >
{item.icon} {item.icon}
</motion.div> </motion.div>
<span <span
className={`text-[10px] font-medium tracking-wider uppercase transition-colors ${ className={`text-[10px] font-medium tracking-wider uppercase transition-colors ${
isActive ? "text-[#f59e0b]" : "text-white/30" isActive ? "text-[#f59e0b]" : "text-black/25 dark:text-white/30"
}`} }`}
> >
{item.label} {item.label}
@@ -63,6 +98,22 @@ export default function Nav() {
</Link> </Link>
) )
})} })}
{/* Theme toggle in mobile nav */}
<button
onClick={toggle}
aria-label="Toggle theme"
className="flex flex-col items-center gap-1 px-6 py-1"
>
<motion.div
whileTap={{ scale: 0.85 }}
className="text-xl text-black/30 dark:text-white/40 hover:text-[#f59e0b] transition-colors"
>
{theme === "dark" ? <SunIcon /> : <MoonIcon />}
</motion.div>
<span className="text-[10px] font-medium tracking-wider uppercase text-black/25 dark:text-white/30">
{theme === "dark" ? "Light" : "Dark"}
</span>
</button>
</div> </div>
</nav> </nav>
</> </>

View File

@@ -30,18 +30,19 @@ export default function RestaurantCard({ restaurant, latestVisit, index }: Props
transition={{ duration: 0.4, delay: index * 0.08, ease: [0.25, 0.1, 0.25, 1] }} transition={{ duration: 0.4, delay: index * 0.08, ease: [0.25, 0.1, 0.25, 1] }}
> >
<Link href={`/restaurant/${restaurant.id}`}> <Link href={`/restaurant/${restaurant.id}`}>
<div className="group relative rounded-2xl border border-white/[0.06] bg-white/[0.03] hover:bg-white/[0.06] hover:border-white/[0.12] transition-all duration-300 p-5 cursor-pointer overflow-hidden"> <div className="group relative rounded-2xl border border-black/[0.07] dark:border-white/[0.06] bg-white dark:bg-white/[0.03] hover:bg-stone-50 dark:hover:bg-white/[0.06] hover:border-black/[0.12] dark:hover:border-white/[0.12] transition-all duration-300 p-5 cursor-pointer overflow-hidden shadow-sm dark:shadow-none">
{/* Amber glow on hover */} {/* Amber glow on hover */}
<div className="absolute inset-0 rounded-2xl opacity-0 group-hover:opacity-100 transition-opacity duration-500 pointer-events-none" <div
className="absolute inset-0 rounded-2xl opacity-0 group-hover:opacity-100 transition-opacity duration-500 pointer-events-none"
style={{ background: "radial-gradient(circle at 50% 0%, rgba(245,158,11,0.06) 0%, transparent 70%)" }} style={{ background: "radial-gradient(circle at 50% 0%, rgba(245,158,11,0.06) 0%, transparent 70%)" }}
/> />
<div className="flex items-start justify-between gap-4"> <div className="flex items-start justify-between gap-4">
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<h3 className="text-lg font-bold text-[#f5f5f5] group-hover:text-[#f59e0b] transition-colors truncate"> <h3 className="text-lg font-bold text-[#111] dark:text-[#f5f5f5] group-hover:text-[#f59e0b] transition-colors truncate">
{restaurant.name} {restaurant.name}
</h3> </h3>
<p className="text-sm text-white/40 mt-0.5"> <p className="text-sm text-black/40 dark:text-white/40 mt-0.5">
{restaurant.city}, {restaurant.country} {restaurant.city}, {restaurant.country}
</p> </p>
</div> </div>
@@ -51,9 +52,9 @@ export default function RestaurantCard({ restaurant, latestVisit, index }: Props
<span className="text-[#f59e0b] font-black text-lg leading-none"> <span className="text-[#f59e0b] font-black text-lg leading-none">
{latestVisit.rating} {latestVisit.rating}
</span> </span>
<span className="text-white/30 text-xs">/10</span> <span className="text-black/25 dark:text-white/30 text-xs">/10</span>
</div> </div>
<span className="text-white/30 text-xs font-medium tracking-wide"> <span className="text-black/30 dark:text-white/30 text-xs font-medium tracking-wide">
{priceLabel(restaurant.price_range)} {priceLabel(restaurant.price_range)}
</span> </span>
</div> </div>
@@ -61,7 +62,7 @@ export default function RestaurantCard({ restaurant, latestVisit, index }: Props
<div className="mt-4"> <div className="mt-4">
{latestVisit.dishes[0] && ( {latestVisit.dishes[0] && (
<p className="text-sm text-white/60 italic truncate"> <p className="text-sm text-black/50 dark:text-white/60 italic truncate">
&ldquo;{latestVisit.dishes[0]}&rdquo; &ldquo;{latestVisit.dishes[0]}&rdquo;
</p> </p>
)} )}
@@ -72,20 +73,20 @@ export default function RestaurantCard({ restaurant, latestVisit, index }: Props
{restaurant.cuisine.slice(0, 3).map((tag) => ( {restaurant.cuisine.slice(0, 3).map((tag) => (
<span <span
key={tag} key={tag}
className="text-[10px] font-semibold uppercase tracking-wider text-[#f59e0b]/70 bg-[#f59e0b]/10 rounded-full px-2.5 py-0.5" className="text-[10px] font-semibold uppercase tracking-wider text-[#f59e0b] bg-[#f59e0b]/10 rounded-full px-2.5 py-0.5"
> >
{tag} {tag}
</span> </span>
))} ))}
</div> </div>
<span className="text-xs text-white/25 shrink-0"> <span className="text-xs text-black/25 dark:text-white/25 shrink-0">
{formatDate(latestVisit.date)} {formatDate(latestVisit.date)}
</span> </span>
</div> </div>
{restaurant.visits.length > 1 && ( {restaurant.visits.length > 1 && (
<div className="mt-3 pt-3 border-t border-white/[0.05]"> <div className="mt-3 pt-3 border-t border-black/[0.05] dark:border-white/[0.05]">
<p className="text-xs text-white/30"> <p className="text-xs text-black/30 dark:text-white/30">
{restaurant.visits.length} visits {restaurant.visits.length} visits
</p> </p>
</div> </div>

View File

@@ -0,0 +1,55 @@
"use client"
import { createContext, useContext, useEffect, useState } from "react"
type Theme = "light" | "dark"
interface ThemeContextValue {
theme: Theme
toggle: () => void
}
const ThemeContext = createContext<ThemeContextValue>({
theme: "dark",
toggle: () => {},
})
export function useTheme() {
return useContext(ThemeContext)
}
export default function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<Theme>("dark")
const [mounted, setMounted] = useState(false)
useEffect(() => {
// Read from localStorage, fall back to system preference
const stored = localStorage.getItem("food-theme") as Theme | null
if (stored === "light" || stored === "dark") {
setTheme(stored)
} else {
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches
setTheme(prefersDark ? "dark" : "light")
}
setMounted(true)
}, [])
useEffect(() => {
if (!mounted) return
const root = document.documentElement
if (theme === "dark") {
root.classList.add("dark")
} else {
root.classList.remove("dark")
}
localStorage.setItem("food-theme", theme)
}, [theme, mounted])
const toggle = () => setTheme((t) => (t === "dark" ? "light" : "dark"))
return (
<ThemeContext.Provider value={{ theme, toggle }}>
{children}
</ThemeContext.Provider>
)
}

View File

@@ -1,17 +1,9 @@
@import "tailwindcss"; @import "tailwindcss";
:root { /* Dark mode via .dark class on <html> */
--background: #0d0d0d; @custom-variant dark (&:where(.dark, .dark *));
--foreground: #f5f5f5;
--accent: #f59e0b;
--accent-dim: #d97706;
--surface: rgba(255, 255, 255, 0.04);
--border: rgba(255, 255, 255, 0.08);
}
@theme inline { @theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans); --font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono); --font-mono: var(--font-geist-mono);
} }
@@ -25,8 +17,6 @@ html {
} }
body { body {
background-color: #0d0d0d;
color: #f5f5f5;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
@@ -38,7 +28,7 @@ body {
} }
::-webkit-scrollbar-track { ::-webkit-scrollbar-track {
background: #0d0d0d; background: transparent;
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
@@ -50,23 +40,35 @@ body {
background: rgba(245, 158, 11, 0.6); background: rgba(245, 158, 11, 0.6);
} }
/* Leaflet dark overrides */ /* Leaflet popup — light mode */
.leaflet-container {
background: #0d0d0d;
}
.leaflet-popup-content-wrapper { .leaflet-popup-content-wrapper {
background: #1a1a1a; background: #ffffff;
color: #f5f5f5; color: #111111;
border: 1px solid rgba(255, 255, 255, 0.08); border: 1px solid rgba(0, 0, 0, 0.08);
border-radius: 12px; border-radius: 12px;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.6); box-shadow: 0 4px 24px rgba(0, 0, 0, 0.12);
} }
.leaflet-popup-tip { .leaflet-popup-tip {
background: #1a1a1a; background: #ffffff;
} }
.leaflet-popup-close-button { .leaflet-popup-close-button {
color: #111111 !important;
}
/* Leaflet popup — dark mode */
.dark .leaflet-popup-content-wrapper {
background: #1a1a1a;
color: #f5f5f5;
border: 1px solid rgba(255, 255, 255, 0.08);
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.6);
}
.dark .leaflet-popup-tip {
background: #1a1a1a;
}
.dark .leaflet-popup-close-button {
color: #f5f5f5 !important; color: #f5f5f5 !important;
} }

View File

@@ -2,6 +2,7 @@ import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google"; import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css"; import "./globals.css";
import Nav from "@/app/components/Nav"; import Nav from "@/app/components/Nav";
import ThemeProvider from "@/app/components/ThemeProvider";
const geistSans = Geist({ const geistSans = Geist({
variable: "--font-geist-sans", variable: "--font-geist-sans",
@@ -28,11 +29,13 @@ export default function RootLayout({
lang="en" lang="en"
className={`${geistSans.variable} ${geistMono.variable} h-full`} className={`${geistSans.variable} ${geistMono.variable} h-full`}
> >
<body className="min-h-full flex flex-col bg-[#0d0d0d] text-[#f5f5f5]"> <body className="min-h-full flex flex-col bg-[#fafafa] dark:bg-[#0d0d0d] text-[#111111] dark:text-[#f5f5f5] transition-colors duration-200">
<ThemeProvider>
<Nav /> <Nav />
<main className="flex-1 pb-20 md:pb-0"> <main className="flex-1 pb-20 md:pb-0">
{children} {children}
</main> </main>
</ThemeProvider>
</body> </body>
</html> </html>
); );

View File

@@ -5,6 +5,7 @@ import { useState, useEffect } from "react"
import { motion } from "framer-motion" import { motion } from "framer-motion"
import Link from "next/link" import Link from "next/link"
import { Restaurant } from "@/app/types" import { Restaurant } from "@/app/types"
import { useTheme } from "@/app/components/ThemeProvider"
const MapView = dynamic(() => import("@/app/components/MapView"), { ssr: false }) const MapView = dynamic(() => import("@/app/components/MapView"), { ssr: false })
@@ -19,6 +20,7 @@ export default function MapPage() {
const [restaurants, setRestaurants] = useState<Restaurant[]>([]) const [restaurants, setRestaurants] = useState<Restaurant[]>([])
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [selected, setSelected] = useState<string | null>(null) const [selected, setSelected] = useState<string | null>(null)
const { theme } = useTheme()
useEffect(() => { useEffect(() => {
fetch("/api/restaurants") fetch("/api/restaurants")
@@ -35,7 +37,7 @@ export default function MapPage() {
{/* Map */} {/* Map */}
<div className="flex-1 relative"> <div className="flex-1 relative">
{loading ? ( {loading ? (
<div className="flex items-center justify-center h-full bg-[#0d0d0d]"> <div className="flex items-center justify-center h-full bg-[#fafafa] dark:bg-[#0d0d0d]">
<motion.div <motion.div
animate={{ rotate: 360 }} animate={{ rotate: 360 }}
transition={{ duration: 1, repeat: Infinity, ease: "linear" }} transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
@@ -47,14 +49,15 @@ export default function MapPage() {
restaurants={restaurants} restaurants={restaurants}
selectedId={selected} selectedId={selected}
onSelect={setSelected} onSelect={setSelected}
theme={theme}
/> />
)} )}
</div> </div>
{/* Desktop sidebar */} {/* Desktop sidebar */}
<aside className="hidden md:flex w-80 flex-col border-l border-white/[0.06] bg-[#0d0d0d] overflow-y-auto"> <aside className="hidden md:flex w-80 flex-col border-l border-black/[0.06] dark:border-white/[0.06] bg-[#fafafa] dark:bg-[#0d0d0d] overflow-y-auto">
<div className="p-5 border-b border-white/[0.06]"> <div className="p-5 border-b border-black/[0.06] dark:border-white/[0.06]">
<h2 className="text-lg font-black tracking-tight"> <h2 className="text-lg font-black tracking-tight text-[#111] dark:text-[#f5f5f5]">
{restaurants.length} places {restaurants.length} places
</h2> </h2>
</div> </div>
@@ -66,18 +69,18 @@ export default function MapPage() {
className={`w-full text-left rounded-xl p-3 transition-all border ${ className={`w-full text-left rounded-xl p-3 transition-all border ${
selected === r.id selected === r.id
? "bg-[#f59e0b]/10 border-[#f59e0b]/30" ? "bg-[#f59e0b]/10 border-[#f59e0b]/30"
: "bg-white/[0.02] border-white/[0.06] hover:bg-white/[0.05]" : "bg-black/[0.02] dark:bg-white/[0.02] border-black/[0.06] dark:border-white/[0.06] hover:bg-black/[0.04] dark:hover:bg-white/[0.05]"
}`} }`}
> >
<p className="font-semibold text-sm text-[#f5f5f5]">{r.name}</p> <p className="font-semibold text-sm text-[#111] dark:text-[#f5f5f5]">{r.name}</p>
<p className="text-xs text-white/40 mt-0.5"> <p className="text-xs text-black/40 dark:text-white/40 mt-0.5">
{r.city} · {r.cuisine[0]} {r.city} · {r.cuisine[0]}
</p> </p>
<div className="flex items-center gap-2 mt-1.5"> <div className="flex items-center gap-2 mt-1.5">
<span className="text-[#f59e0b] text-xs font-bold"> <span className="text-[#f59e0b] text-xs font-bold">
{avgRating(r).toFixed(1)} {avgRating(r).toFixed(1)}
</span> </span>
<span className="text-white/25 text-xs"> <span className="text-black/25 dark:text-white/25 text-xs">
{r.visits.length} {r.visits.length === 1 ? "visit" : "visits"} {r.visits.length} {r.visits.length === 1 ? "visit" : "visits"}
</span> </span>
</div> </div>
@@ -88,7 +91,7 @@ export default function MapPage() {
{/* Mobile bottom sheet */} {/* Mobile bottom sheet */}
<motion.div <motion.div
className="md:hidden fixed bottom-16 left-0 right-0 bg-[#111]/95 backdrop-blur-xl border-t border-white/[0.06] max-h-64 overflow-y-auto z-40" className="md:hidden fixed bottom-16 left-0 right-0 bg-white/95 dark:bg-[#111]/95 backdrop-blur-xl border-t border-black/[0.06] dark:border-white/[0.06] max-h-64 overflow-y-auto z-40"
initial={{ y: 200 }} initial={{ y: 200 }}
animate={{ y: 0 }} animate={{ y: 0 }}
transition={{ delay: 0.3, duration: 0.4 }} transition={{ delay: 0.3, duration: 0.4 }}
@@ -101,11 +104,11 @@ export default function MapPage() {
className={`text-left rounded-xl p-3 transition-all border ${ className={`text-left rounded-xl p-3 transition-all border ${
selected === r.id selected === r.id
? "bg-[#f59e0b]/10 border-[#f59e0b]/30" ? "bg-[#f59e0b]/10 border-[#f59e0b]/30"
: "bg-white/[0.03] border-white/[0.06]" : "bg-black/[0.03] dark:bg-white/[0.03] border-black/[0.06] dark:border-white/[0.06]"
}`} }`}
> >
<p className="font-semibold text-xs text-[#f5f5f5] truncate">{r.name}</p> <p className="font-semibold text-xs text-[#111] dark:text-[#f5f5f5] truncate">{r.name}</p>
<p className="text-[10px] text-white/40">{r.city}</p> <p className="text-[10px] text-black/40 dark:text-white/40">{r.city}</p>
<span className="text-[#f59e0b] text-xs font-bold"> <span className="text-[#f59e0b] text-xs font-bold">
{avgRating(r).toFixed(1)} {avgRating(r).toFixed(1)}
</span> </span>

View File

@@ -99,10 +99,10 @@ export default function HomePage() {
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, ease: [0.25, 0.1, 0.25, 1] }} transition={{ duration: 0.6, ease: [0.25, 0.1, 0.25, 1] }}
> >
<h1 className="text-[clamp(4rem,16vw,10rem)] font-black leading-none tracking-tighter text-[#f5f5f5]"> <h1 className="text-[clamp(4rem,16vw,10rem)] font-black leading-none tracking-tighter text-[#111] dark:text-[#f5f5f5]">
food<span className="text-[#f59e0b]">.</span> food<span className="text-[#f59e0b]">.</span>
</h1> </h1>
<p className="text-white/40 text-base md:text-lg mt-3 font-light tracking-wide"> <p className="text-black/40 dark:text-white/40 text-base md:text-lg mt-3 font-light tracking-wide">
a personal food journal a personal food journal
</p> </p>
</motion.div> </motion.div>
@@ -121,7 +121,7 @@ export default function HomePage() {
placeholder="Search restaurants, cities..." placeholder="Search restaurants, cities..."
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
className="w-full bg-white/[0.04] border border-white/[0.08] rounded-xl px-4 py-3 text-sm text-[#f5f5f5] placeholder-white/25 outline-none focus:border-[#f59e0b]/40 focus:bg-white/[0.06] transition-all" className="w-full bg-white dark:bg-white/[0.04] border border-black/[0.10] dark:border-white/[0.08] rounded-xl px-4 py-3 text-sm text-[#111] dark:text-[#f5f5f5] placeholder-black/30 dark:placeholder-white/25 outline-none focus:border-[#f59e0b]/60 dark:focus:border-[#f59e0b]/40 focus:bg-white dark:focus:bg-white/[0.06] transition-all shadow-sm dark:shadow-none"
/> />
{allCuisines.length > 0 && ( {allCuisines.length > 0 && (
@@ -133,7 +133,7 @@ export default function HomePage() {
className={`text-xs font-semibold uppercase tracking-wider rounded-full px-3 py-1.5 transition-all border ${ className={`text-xs font-semibold uppercase tracking-wider rounded-full px-3 py-1.5 transition-all border ${
selectedCuisines.includes(c) selectedCuisines.includes(c)
? "bg-[#f59e0b] text-black border-[#f59e0b]" ? "bg-[#f59e0b] text-black border-[#f59e0b]"
: "bg-white/[0.04] text-white/50 border-white/[0.08] hover:border-white/20" : "bg-white dark:bg-white/[0.04] text-black/50 dark:text-white/50 border-black/[0.10] dark:border-white/[0.08] hover:border-black/20 dark:hover:border-white/20"
}`} }`}
> >
{c} {c}
@@ -150,7 +150,7 @@ export default function HomePage() {
className={`text-xs font-semibold rounded-full px-3 py-1.5 transition-all border ${ className={`text-xs font-semibold rounded-full px-3 py-1.5 transition-all border ${
selectedPrices.includes(p) selectedPrices.includes(p)
? "bg-[#f59e0b] text-black border-[#f59e0b]" ? "bg-[#f59e0b] text-black border-[#f59e0b]"
: "bg-white/[0.04] text-white/50 border-white/[0.08] hover:border-white/20" : "bg-white dark:bg-white/[0.04] text-black/50 dark:text-white/50 border-black/[0.10] dark:border-white/[0.08] hover:border-black/20 dark:hover:border-white/20"
}`} }`}
> >
{PRICE_LABELS[p - 1]} {PRICE_LABELS[p - 1]}
@@ -174,7 +174,7 @@ export default function HomePage() {
<motion.p <motion.p
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
className="text-white/30 text-center py-24 text-sm" className="text-black/30 dark:text-white/30 text-center py-24 text-sm"
> >
No restaurants found. No restaurants found.
</motion.p> </motion.p>

View File

@@ -22,7 +22,7 @@ function formatDate(iso: string) {
function RatingBar({ rating }: { rating: number }) { function RatingBar({ rating }: { rating: number }) {
return ( return (
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="flex-1 h-1.5 bg-white/[0.08] rounded-full overflow-hidden"> <div className="flex-1 h-1.5 bg-black/[0.08] dark:bg-white/[0.08] rounded-full overflow-hidden">
<motion.div <motion.div
className="h-full bg-[#f59e0b] rounded-full" className="h-full bg-[#f59e0b] rounded-full"
initial={{ width: 0 }} initial={{ width: 0 }}
@@ -46,15 +46,15 @@ function VisitCard({ visit, index }: { visit: Visit; index: number }) {
className="relative pl-6" className="relative pl-6"
> >
{/* Timeline line */} {/* Timeline line */}
<div className="absolute left-0 top-0 bottom-0 w-px bg-white/[0.08]" /> <div className="absolute left-0 top-0 bottom-0 w-px bg-black/[0.08] dark:bg-white/[0.08]" />
{/* Timeline dot */} {/* Timeline dot */}
<div className="absolute left-[-4px] top-3 w-2 h-2 rounded-full bg-[#f59e0b]" /> <div className="absolute left-[-4px] top-3 w-2 h-2 rounded-full bg-[#f59e0b]" />
<div className="rounded-2xl border border-white/[0.06] bg-white/[0.03] p-5 mb-4"> <div className="rounded-2xl border border-black/[0.07] dark:border-white/[0.06] bg-white dark:bg-white/[0.03] p-5 mb-4 shadow-sm dark:shadow-none">
<div className="flex items-start justify-between gap-4 mb-4"> <div className="flex items-start justify-between gap-4 mb-4">
<p className="text-sm text-white/40">{formatDate(visit.date)}</p> <p className="text-sm text-black/40 dark:text-white/40">{formatDate(visit.date)}</p>
{visit.price_paid && ( {visit.price_paid && (
<span className="text-xs text-white/30 shrink-0"> <span className="text-xs text-black/30 dark:text-white/30 shrink-0">
{visit.price_paid} {visit.price_paid}
</span> </span>
)} )}
@@ -63,12 +63,12 @@ function VisitCard({ visit, index }: { visit: Visit; index: number }) {
<RatingBar rating={visit.rating} /> <RatingBar rating={visit.rating} />
<div className="mt-4 space-y-1.5"> <div className="mt-4 space-y-1.5">
<p className="text-xs font-semibold uppercase tracking-wider text-white/30"> <p className="text-xs font-semibold uppercase tracking-wider text-black/30 dark:text-white/30">
Dishes Dishes
</p> </p>
<ul className="space-y-1"> <ul className="space-y-1">
{visit.dishes.map((dish, i) => ( {visit.dishes.map((dish, i) => (
<li key={i} className="text-sm text-[#f5f5f5] flex items-start gap-2"> <li key={i} className="text-sm text-[#111] dark:text-[#f5f5f5] flex items-start gap-2">
<span className="text-[#f59e0b] mt-1 shrink-0">·</span> <span className="text-[#f59e0b] mt-1 shrink-0">·</span>
{dish} {dish}
</li> </li>
@@ -77,8 +77,8 @@ function VisitCard({ visit, index }: { visit: Visit; index: number }) {
</div> </div>
{visit.notes && ( {visit.notes && (
<div className="mt-4 pt-4 border-t border-white/[0.06]"> <div className="mt-4 pt-4 border-t border-black/[0.06] dark:border-white/[0.06]">
<p className="text-sm text-white/60 leading-relaxed italic"> <p className="text-sm text-black/50 dark:text-white/60 leading-relaxed italic">
&ldquo;{visit.notes}&rdquo; &ldquo;{visit.notes}&rdquo;
</p> </p>
</div> </div>
@@ -134,7 +134,7 @@ export default function RestaurantPage() {
if (notFound || !restaurant) { if (notFound || !restaurant) {
return ( return (
<div className="flex flex-col items-center justify-center min-h-[60vh] gap-4"> <div className="flex flex-col items-center justify-center min-h-[60vh] gap-4">
<p className="text-white/40">Restaurant not found</p> <p className="text-black/40 dark:text-white/40">Restaurant not found</p>
<Link href="/" className="text-[#f59e0b] text-sm font-medium hover:underline"> <Link href="/" className="text-[#f59e0b] text-sm font-medium hover:underline">
Back home Back home
</Link> </Link>
@@ -161,7 +161,7 @@ export default function RestaurantPage() {
> >
<button <button
onClick={() => router.back()} onClick={() => router.back()}
className="text-sm text-white/40 hover:text-white/70 transition-colors mb-8 flex items-center gap-2" className="text-sm text-black/40 dark:text-white/40 hover:text-black/70 dark:hover:text-white/70 transition-colors mb-8 flex items-center gap-2"
> >
back back
</button> </button>
@@ -174,18 +174,18 @@ export default function RestaurantPage() {
transition={{ duration: 0.5 }} transition={{ duration: 0.5 }}
> >
<div className="flex items-start justify-between gap-4 mb-2"> <div className="flex items-start justify-between gap-4 mb-2">
<h1 className="text-4xl md:text-5xl font-black tracking-tight text-[#f5f5f5] leading-none"> <h1 className="text-4xl md:text-5xl font-black tracking-tight text-[#111] dark:text-[#f5f5f5] leading-none">
{restaurant.name} {restaurant.name}
</h1> </h1>
<div className="shrink-0 text-right"> <div className="shrink-0 text-right">
<div className="text-[#f59e0b] font-black text-3xl leading-none"> <div className="text-[#f59e0b] font-black text-3xl leading-none">
{avgRating.toFixed(1)} {avgRating.toFixed(1)}
</div> </div>
<div className="text-white/25 text-xs mt-1">avg</div> <div className="text-black/25 dark:text-white/25 text-xs mt-1">avg</div>
</div> </div>
</div> </div>
<p className="text-white/40 text-base mt-2"> <p className="text-black/40 dark:text-white/40 text-base mt-2">
{restaurant.address} · {restaurant.city}, {restaurant.country} {restaurant.address} · {restaurant.city}, {restaurant.country}
</p> </p>
@@ -193,33 +193,32 @@ export default function RestaurantPage() {
{restaurant.cuisine.map((tag) => ( {restaurant.cuisine.map((tag) => (
<span <span
key={tag} key={tag}
className="text-xs font-semibold uppercase tracking-wider text-[#f59e0b]/70 bg-[#f59e0b]/10 rounded-full px-3 py-1" className="text-xs font-semibold uppercase tracking-wider text-[#f59e0b] bg-[#f59e0b]/10 rounded-full px-3 py-1"
> >
{tag} {tag}
</span> </span>
))} ))}
<span className="text-white/30 text-sm font-medium"> <span className="text-black/30 dark:text-white/30 text-sm font-medium">
{priceLabel(restaurant.price_range)} {priceLabel(restaurant.price_range)}
</span> </span>
<span className="text-white/25 text-sm"> <span className="text-black/25 dark:text-white/25 text-sm">
{restaurant.visits.length} {restaurant.visits.length === 1 ? "visit" : "visits"} {restaurant.visits.length} {restaurant.visits.length === 1 ? "visit" : "visits"}
</span> </span>
</div> </div>
</motion.div> </motion.div>
{/* Divider */} {/* Divider */}
<div className="my-8 border-t border-white/[0.06]" /> <div className="my-8 border-t border-black/[0.06] dark:border-white/[0.06]" />
{/* Visits timeline */} {/* Visits timeline */}
<div> <div>
<h2 className="text-lg font-bold mb-6 text-white/70">Visits</h2> <h2 className="text-lg font-bold mb-6 text-black/60 dark:text-white/70">Visits</h2>
<div className="space-y-0"> <div className="space-y-0">
{sortedVisits.map((visit, i) => ( {sortedVisits.map((visit, i) => (
<VisitCard key={visit.id} visit={visit} index={i} /> <VisitCard key={visit.id} visit={visit} index={i} />
))} ))}
</div> </div>
</div> </div>
</div> </div>
) )
} }