1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
|
import { useLoaderData, useSearchParams, Link } from "react-router";
import type { Route } from "./+types/events._index";
import { queryEvents, getVenues, type CapacityRange } from "~/lib/db.server";
import EventCard from "~/components/EventCard";
import EventListRow from "~/components/EventListRow";
import FilterBar from "~/components/FilterBar";
function defaultWindow() {
const today = new Date();
today.setHours(0, 0, 0, 0);
const end = new Date(today);
end.setDate(end.getDate() + 35);
return {
from: today.toISOString().slice(0, 10),
to: end.toISOString().slice(0, 10),
};
}
export async function loader({ request }: Route.LoaderArgs) {
const url = new URL(request.url);
const { from: defaultFrom, to: defaultTo } = defaultWindow();
const date_from = url.searchParams.get("date_from") ?? defaultFrom;
const date_to = url.searchParams.get("date_to") ?? defaultTo;
const venue_id = url.searchParams.get("venue_id") ?? undefined;
const keyword = url.searchParams.get("keyword") ?? undefined;
const capacity_range = (url.searchParams.get("capacity_range") ?? undefined) as CapacityRange | 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, capacity_range, limit, offset });
const venues = getVenues();
return { events, venues, page, hasMore: events.length === limit, date_from, date_to };
}
export default function EventsIndex() {
const { events, venues, page, hasMore, date_from, date_to } = useLoaderData<typeof loader>();
const [searchParams, setSearchParams] = useSearchParams();
const view = (searchParams.get("view") ?? "card") as "card" | "list";
function switchView(next: "card" | "list") {
const next_params = new URLSearchParams(searchParams);
next_params.set("view", next);
next_params.delete("page");
setSearchParams(next_params, { preventScrollReset: true });
}
return (
<div className="min-h-screen bg-gray-950 text-gray-100">
<header className="border-b border-gray-800 px-4 sm:px-6 py-3 sm:py-4 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 sm:gap-0">
<Link to="/" className="text-xl font-bold tracking-tight text-white">
🎸 ライブに行くしかない
</Link>
<nav className="flex gap-4 sm:gap-6 text-sm text-gray-400">
<Link to="/events" className="hover:text-white transition-colors">イベント</Link>
<Link to="/events/by-date" 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>
<div className="flex rounded-lg overflow-hidden border border-gray-700 text-sm">
<button
onClick={() => switchView("card")}
className={`px-3 py-1.5 transition-colors ${view === "card" ? "bg-indigo-600 text-white" : "bg-gray-800 text-gray-400 hover:text-white"}`}
title="カード表示"
>
▦
</button>
<button
onClick={() => switchView("list")}
className={`px-3 py-1.5 transition-colors ${view === "list" ? "bg-indigo-600 text-white" : "bg-gray-800 text-gray-400 hover:text-white"}`}
title="リスト表示"
>
☰
</button>
</div>
</div>
<FilterBar venues={venues} defaultDateFrom={date_from} defaultDateTo={date_to} />
{events.length === 0 ? (
<div className="mt-16 text-center text-gray-500">
<p className="text-lg">イベントが見つかりません</p>
<p className="mt-2 text-sm">スクレイパーを実行してデータを取得してください: <code>npm run scrape</code></p>
</div>
) : view === "list" ? (
<div className="mt-6 flex flex-col gap-1.5">
{events.map((event) => (
<EventListRow key={event.id} event={event} />
))}
</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();
}
|