Add light/dark mode with toggle, fix all theme-aware text colors
This commit is contained in:
@@ -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='© <a href="https://carto.com/">CARTO</a>'
|
attribution='© <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>
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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">
|
||||||
“{latestVisit.dishes[0]}”
|
“{latestVisit.dishes[0]}”
|
||||||
</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>
|
||||||
|
|||||||
55
app/components/ThemeProvider.tsx
Normal file
55
app/components/ThemeProvider.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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">
|
||||||
<Nav />
|
<ThemeProvider>
|
||||||
<main className="flex-1 pb-20 md:pb-0">
|
<Nav />
|
||||||
{children}
|
<main className="flex-1 pb-20 md:pb-0">
|
||||||
</main>
|
{children}
|
||||||
|
</main>
|
||||||
|
</ThemeProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
12
app/page.tsx
12
app/page.tsx
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
“{visit.notes}”
|
“{visit.notes}”
|
||||||
</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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user