Initial food blog app
This commit is contained in:
127
app/map/page.tsx
Normal file
127
app/map/page.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
"use client"
|
||||
|
||||
import dynamic from "next/dynamic"
|
||||
import { useState, useEffect } from "react"
|
||||
import { motion } from "framer-motion"
|
||||
import Link from "next/link"
|
||||
import { Restaurant } from "@/app/types"
|
||||
|
||||
const MapView = dynamic(() => import("@/app/components/MapView"), { ssr: false })
|
||||
|
||||
function avgRating(restaurant: Restaurant) {
|
||||
if (restaurant.visits.length === 0) return 0
|
||||
return (
|
||||
restaurant.visits.reduce((s, v) => s + v.rating, 0) / restaurant.visits.length
|
||||
)
|
||||
}
|
||||
|
||||
export default function MapPage() {
|
||||
const [restaurants, setRestaurants] = useState<Restaurant[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [selected, setSelected] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/restaurants")
|
||||
.then((r) => r.json())
|
||||
.then((data) => {
|
||||
setRestaurants(data)
|
||||
setLoading(false)
|
||||
})
|
||||
.catch(() => setLoading(false))
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="flex flex-col md:flex-row h-[calc(100vh-65px)]">
|
||||
{/* Map */}
|
||||
<div className="flex-1 relative">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center h-full bg-[#0d0d0d]">
|
||||
<motion.div
|
||||
animate={{ rotate: 360 }}
|
||||
transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
|
||||
className="w-6 h-6 border-2 border-[#f59e0b] border-t-transparent rounded-full"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<MapView
|
||||
restaurants={restaurants}
|
||||
selectedId={selected}
|
||||
onSelect={setSelected}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Desktop sidebar */}
|
||||
<aside className="hidden md:flex w-80 flex-col border-l border-white/[0.06] bg-[#0d0d0d] overflow-y-auto">
|
||||
<div className="p-5 border-b border-white/[0.06]">
|
||||
<h2 className="text-lg font-black tracking-tight">
|
||||
{restaurants.length} places
|
||||
</h2>
|
||||
</div>
|
||||
<div className="flex-1 p-3 space-y-2">
|
||||
{restaurants.map((r) => (
|
||||
<button
|
||||
key={r.id}
|
||||
onClick={() => setSelected(r.id === selected ? null : r.id)}
|
||||
className={`w-full text-left rounded-xl p-3 transition-all border ${
|
||||
selected === r.id
|
||||
? "bg-[#f59e0b]/10 border-[#f59e0b]/30"
|
||||
: "bg-white/[0.02] border-white/[0.06] hover:bg-white/[0.05]"
|
||||
}`}
|
||||
>
|
||||
<p className="font-semibold text-sm text-[#f5f5f5]">{r.name}</p>
|
||||
<p className="text-xs text-white/40 mt-0.5">
|
||||
{r.city} · {r.cuisine[0]}
|
||||
</p>
|
||||
<div className="flex items-center gap-2 mt-1.5">
|
||||
<span className="text-[#f59e0b] text-xs font-bold">
|
||||
{avgRating(r).toFixed(1)}
|
||||
</span>
|
||||
<span className="text-white/25 text-xs">
|
||||
{r.visits.length} {r.visits.length === 1 ? "visit" : "visits"}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Mobile bottom sheet */}
|
||||
<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"
|
||||
initial={{ y: 200 }}
|
||||
animate={{ y: 0 }}
|
||||
transition={{ delay: 0.3, duration: 0.4 }}
|
||||
>
|
||||
<div className="p-3 grid grid-cols-2 gap-2">
|
||||
{restaurants.map((r) => (
|
||||
<button
|
||||
key={r.id}
|
||||
onClick={() => setSelected(r.id === selected ? null : r.id)}
|
||||
className={`text-left rounded-xl p-3 transition-all border ${
|
||||
selected === r.id
|
||||
? "bg-[#f59e0b]/10 border-[#f59e0b]/30"
|
||||
: "bg-white/[0.03] border-white/[0.06]"
|
||||
}`}
|
||||
>
|
||||
<p className="font-semibold text-xs text-[#f5f5f5] truncate">{r.name}</p>
|
||||
<p className="text-[10px] text-white/40">{r.city}</p>
|
||||
<span className="text-[#f59e0b] text-xs font-bold">
|
||||
{avgRating(r).toFixed(1)}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{selected && (
|
||||
<Link
|
||||
href={`/restaurant/${selected}`}
|
||||
className="hidden md:block absolute bottom-6 right-6 bg-[#f59e0b] text-black font-bold text-sm px-4 py-2 rounded-xl hover:bg-[#d97706] transition-colors z-50"
|
||||
>
|
||||
View details
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user