diff options
Diffstat (limited to 'app/routes/events._index.tsx')
| -rw-r--r-- | app/routes/events._index.tsx | 94 |
1 files changed, 94 insertions, 0 deletions
diff --git a/app/routes/events._index.tsx b/app/routes/events._index.tsx new file mode 100644 index 0000000..3883d37 --- /dev/null +++ b/app/routes/events._index.tsx @@ -0,0 +1,94 @@ +import { useLoaderData, useSearchParams, Form, Link } from "react-router"; +import type { Route } from "./+types/events._index"; +import { queryEvents, getVenues } from "~/lib/db.server"; +import EventCard from "~/components/EventCard"; +import FilterBar from "~/components/FilterBar"; + +export async function loader({ request }: Route.LoaderArgs) { + const url = new URL(request.url); + const date_from = url.searchParams.get("date_from") ?? undefined; + const date_to = url.searchParams.get("date_to") ?? undefined; + const venue_id = url.searchParams.get("venue_id") ?? undefined; + const keyword = url.searchParams.get("keyword") ?? undefined; + const page = Math.max(1, parseInt(url.searchParams.get("page") ?? "1", 10)); + const limit = 30; + const offset = (page - 1) * limit; + + const events = queryEvents({ date_from, date_to, venue_id, keyword, limit, offset }); + const venues = getVenues(); + + return { events, venues, page, hasMore: events.length === limit }; +} + +export default function EventsIndex() { + const { events, venues, page, hasMore } = useLoaderData<typeof loader>(); + const [searchParams] = useSearchParams(); + + 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-6xl mx-auto px-4 py-8"> + <div className="mb-6 flex items-center justify-between"> + <h1 className="text-2xl font-bold">イベント一覧</h1> + <Form method="post" action="/api/scrape"> + <button + type="submit" + className="rounded-md bg-indigo-600 px-4 py-2 text-sm font-medium hover:bg-indigo-500 transition-colors" + > + 情報を更新 + </button> + </Form> + </div> + + <FilterBar venues={venues} /> + + {events.length === 0 ? ( + <div className="mt-16 text-center text-gray-500"> + <p className="text-lg">イベントが見つかりません</p> + <p className="mt-2 text-sm">「情報を更新」ボタンでデータを取得してください。</p> + </div> + ) : ( + <div className="mt-6 grid gap-4 sm:grid-cols-2 lg:grid-cols-3"> + {events.map((event) => ( + <EventCard key={event.id} event={event} /> + ))} + </div> + )} + + <div className="mt-8 flex justify-center gap-4"> + {page > 1 && ( + <Link + to={`?${buildPageParams(searchParams, page - 1)}`} + className="rounded bg-gray-800 px-4 py-2 text-sm hover:bg-gray-700" + > + ← 前のページ + </Link> + )} + {hasMore && ( + <Link + to={`?${buildPageParams(searchParams, page + 1)}`} + className="rounded bg-gray-800 px-4 py-2 text-sm hover:bg-gray-700" + > + 次のページ → + </Link> + )} + </div> + </main> + </div> + ); +} + +function buildPageParams(params: URLSearchParams, page: number): string { + const next = new URLSearchParams(params); + next.set("page", String(page)); + return next.toString(); +} |
