summaryrefslogtreecommitdiff
path: root/app/routes/events._index.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'app/routes/events._index.tsx')
-rw-r--r--app/routes/events._index.tsx94
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();
+}