diff options
Diffstat (limited to 'app/routes/events.$id.tsx')
| -rw-r--r-- | app/routes/events.$id.tsx | 124 |
1 files changed, 124 insertions, 0 deletions
diff --git a/app/routes/events.$id.tsx b/app/routes/events.$id.tsx new file mode 100644 index 0000000..cecb282 --- /dev/null +++ b/app/routes/events.$id.tsx @@ -0,0 +1,124 @@ +import { useLoaderData, Link } from "react-router"; +import type { Route } from "./+types/events.$id"; +import { getEvent } from "~/lib/db.server"; + +export async function loader({ params }: Route.LoaderArgs) { + const id = parseInt(params.id, 10); + if (isNaN(id)) throw new Response("Not Found", { status: 404 }); + const event = getEvent(id); + if (!event) throw new Response("Not Found", { status: 404 }); + return { event }; +} + +export default function EventDetail() { + const { event } = useLoaderData<typeof loader>(); + + return ( + <div className="min-h-screen bg-gray-950 text-gray-100"> + <header className="border-b border-gray-800 px-6 py-4 flex items-center justify-between"> + <Link to="/" className="text-xl font-bold tracking-tight text-white"> + 🎸 東京ライブハウス + </Link> + <nav className="flex gap-6 text-sm text-gray-400"> + <Link to="/events" className="hover:text-white transition-colors">イベント</Link> + <Link to="/venues" className="hover:text-white transition-colors">会場一覧</Link> + </nav> + </header> + + <main className="max-w-3xl mx-auto px-4 py-10"> + <Link to="/events" className="text-sm text-indigo-400 hover:underline"> + ← イベント一覧に戻る + </Link> + + <div className="mt-6"> + {event.image_url && ( + <img + src={event.image_url} + alt={event.title} + className="w-full max-h-72 object-cover rounded-xl mb-6" + /> + )} + + <div className="flex items-start justify-between gap-4 flex-wrap"> + <div> + <h1 className="text-3xl font-bold leading-tight">{event.title}</h1> + {event.artist && ( + <p className="mt-1 text-lg text-gray-300">{event.artist}</p> + )} + </div> + <span className="rounded-full bg-indigo-700/60 px-3 py-1 text-sm font-medium whitespace-nowrap"> + {event.venue_name} + </span> + </div> + + <dl className="mt-8 grid grid-cols-2 gap-4 text-sm"> + <Detail label="日付" value={formatDate(event.date)} /> + {event.open_time && <Detail label="OPEN" value={event.open_time} />} + {event.start_time && <Detail label="START" value={event.start_time} />} + {event.price && <Detail label="料金" value={event.price} />} + {event.venue_area && <Detail label="エリア" value={event.venue_area} />} + </dl> + + {event.description && ( + <p className="mt-8 text-gray-300 leading-relaxed whitespace-pre-line"> + {event.description} + </p> + )} + + <div className="mt-8 flex gap-4 flex-wrap"> + {event.ticket_url && ( + <a + href={event.ticket_url} + target="_blank" + rel="noopener noreferrer" + className="rounded-md bg-indigo-600 px-5 py-2 text-sm font-medium hover:bg-indigo-500 transition-colors" + > + チケット購入 + </a> + )} + {event.source_url && ( + <a + href={event.source_url} + target="_blank" + rel="noopener noreferrer" + className="rounded-md bg-gray-700 px-5 py-2 text-sm font-medium hover:bg-gray-600 transition-colors" + > + 詳細ページ + </a> + )} + {event.venue_url && ( + <a + href={event.venue_url} + target="_blank" + rel="noopener noreferrer" + className="rounded-md bg-gray-700 px-5 py-2 text-sm font-medium hover:bg-gray-600 transition-colors" + > + 会場サイト + </a> + )} + </div> + + <p className="mt-10 text-xs text-gray-600"> + 最終取得: {event.fetched_at} + </p> + </div> + </main> + </div> + ); +} + +function Detail({ label, value }: { label: string; value: string }) { + return ( + <div className="rounded-lg bg-gray-800/60 p-3"> + <dt className="text-xs text-gray-500 mb-1">{label}</dt> + <dd className="font-medium">{value}</dd> + </div> + ); +} + +function formatDate(iso: string): string { + const [y, m, d] = iso.split("-"); + const days = ["日", "月", "火", "水", "木", "金", "土"]; + const day = days[new Date(iso).getDay()]; + return `${y}年${m}月${d}日(${day})`; +} |
