import { useLoaderData, useSearchParams, Link } from "react-router"; import { queryEvents, getVenues } from "~/lib/db.server"; import type { Event } from "~/lib/db.server"; function todayJst(): string { // JST = UTC+9 const now = new Date(); const jst = new Date(now.getTime() + 9 * 60 * 60 * 1000); return jst.toISOString().slice(0, 10); } function addDays(iso: string, delta: number): string { const d = new Date(`${iso}T00:00:00Z`); d.setUTCDate(d.getUTCDate() + delta); return d.toISOString().slice(0, 10); } function formatDateJa(iso: string): string { const [y, m, d] = iso.split("-"); const days = ["日", "月", "火", "水", "木", "金", "土"]; const dayIdx = new Date(`${iso}T00:00:00`).getDay(); return `${y}/${m}/${d}(${days[dayIdx]})`; } function buildTimeLabel(open: string | null, start: string | null): string { const parts: string[] = []; if (open) parts.push(`OPEN ${open}`); if (start) parts.push(`START ${start}`); return parts.join(" / "); } export async function loader({ request }: { request: Request }) { const url = new URL(request.url); const date = url.searchParams.get("date") ?? todayJst(); const events = queryEvents({ date_from: date, date_to: date, limit: 500 }); const venues = getVenues(); const byVenue = new Map(); for (const event of events) { if (!byVenue.has(event.venue_id)) byVenue.set(event.venue_id, []); byVenue.get(event.venue_id)!.push(event); } const groups = venues .filter((v) => byVenue.has(v.id)) .map((v) => ({ venue: v, events: byVenue.get(v.id)! })); return { date, groups, totalEvents: events.length }; } export default function EventsByDate() { const { date, groups, totalEvents } = useLoaderData(); const [searchParams] = useSearchParams(); function dateUrl(d: string) { const p = new URLSearchParams(searchParams); p.set("date", d); return `?${p.toString()}`; } const prevDay = addDays(date, -1); const nextDay = addDays(date, 1); return (
🎸 ライブに行くしかない
{/* Date navigation */}
← 前日

{formatDateJa(date)}

{ if (e.target.value) window.location.href = dateUrl(e.target.value); }} className="rounded bg-gray-800 border border-gray-700 px-2 py-1 text-sm text-gray-200 focus:outline-none focus:border-indigo-500" />
翌日 →
{/* Summary */} {totalEvents > 0 && (

{groups.length} 会場 / {totalEvents} イベント

)} {/* Venue groups */} {groups.length === 0 ? (

この日のイベントはありません

スクレイパーを実行してデータを取得してください:{" "} npm run scrape

) : (
{groups.map(({ venue, events }) => (

{venue.name}

{venue.area && ( {venue.area} )} {events.length} 件
{events.map((event) => ( ))}
))}
)}
); } function VenueEventRow({ event }: { event: Event }) { const timeLabel = buildTimeLabel(event.open_time, event.start_time); return ( {/* Time */} {timeLabel || 時間未定} {/* Title + artist */} {event.title} {event.artist && ( {event.artist} )} {/* Price */} {event.price ? ( ¥{event.price} ) : ( )} ); }