summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoryyamashita <yyamashita@hetzner.yyamashita.com>2026-05-22 10:31:14 +0900
committeryyamashita <yyamashita@hetzner.yyamashita.com>2026-05-22 10:31:14 +0900
commit3a961122bdf64cf72cf1b3770e9d71865a3aac52 (patch)
tree8e95d3575a70bd67e8154c6fce0e92712a1a5990
parent7c5860ce57a334b7ab4a501de78f1a7f41c8cdcc (diff)
Add filters to date view (keyword, venue, area, capacity)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
-rw-r--r--app/routes/events.by-date.tsx114
1 files changed, 105 insertions, 9 deletions
diff --git a/app/routes/events.by-date.tsx b/app/routes/events.by-date.tsx
index 9a59073..ee4a873 100644
--- a/app/routes/events.by-date.tsx
+++ b/app/routes/events.by-date.tsx
@@ -1,6 +1,7 @@
-import { useLoaderData, useSearchParams, Link } from "react-router";
-import { queryEvents, getVenues } from "~/lib/db.server";
+import { useLoaderData, useSearchParams, Link, Form } from "react-router";
+import { queryEvents, getVenues, type CapacityRange } from "~/lib/db.server";
import type { Event } from "~/lib/db.server";
+import { useVenueFilter } from "~/lib/venue-filter";
function todayJst(): string {
// JST = UTC+9
@@ -32,9 +33,14 @@ function buildTimeLabel(open: string | null, start: string | null): string {
export async function loader({ request }: { request: Request }) {
const url = new URL(request.url);
const date = url.searchParams.get("date") ?? todayJst();
+ const keyword = url.searchParams.get("keyword") ?? undefined;
+ const venue_id = url.searchParams.get("venue_id") ?? undefined;
+ const capacity_range = (url.searchParams.get("capacity_range") ?? undefined) as CapacityRange | undefined;
+ const area = url.searchParams.get("area") ?? undefined;
- const events = queryEvents({ date_from: date, date_to: date, limit: 500 });
+ const events = queryEvents({ date_from: date, date_to: date, keyword, venue_id, capacity_range, area, limit: 500 });
const venues = getVenues();
+ const areas = [...new Set(venues.filter((v) => v.area).map((v) => v.area as string))].sort();
const byVenue = new Map<string, Event[]>();
for (const event of events) {
@@ -46,12 +52,19 @@ export async function loader({ request }: { request: Request }) {
.filter((v) => byVenue.has(v.id))
.map((v) => ({ venue: v, events: byVenue.get(v.id)! }));
- return { date, groups, totalEvents: events.length };
+ return { date, groups, totalEvents: events.length, venues, areas };
}
export default function EventsByDate() {
- const { date, groups, totalEvents } = useLoaderData<typeof loader>();
+ const { date, groups, totalEvents, venues, areas } = useLoaderData<typeof loader>();
const [searchParams] = useSearchParams();
+ const { hiddenIds } = useVenueFilter();
+
+ const hasVenueFilter = !!searchParams.get("venue_id");
+ const visibleGroups = hasVenueFilter
+ ? groups
+ : groups.filter((g) => !hiddenIds.has(g.venue.id));
+ const hiddenCount = groups.length - visibleGroups.length;
function dateUrl(d: string) {
const p = new URLSearchParams(searchParams);
@@ -59,6 +72,10 @@ export default function EventsByDate() {
return `?${p.toString()}`;
}
+ const hasFilters = ["keyword", "venue_id", "capacity_range", "area"].some(
+ (k) => searchParams.get(k)
+ );
+
const prevDay = addDays(date, -1);
const nextDay = addDays(date, 1);
@@ -77,7 +94,7 @@ export default function EventsByDate() {
<main className="max-w-4xl mx-auto px-4 py-8">
{/* Date navigation */}
- <div className="mb-8 flex items-center justify-between">
+ <div className="mb-6 flex items-center justify-between">
<Link
to={dateUrl(prevDay)}
className="rounded-lg bg-gray-800 px-4 py-2 text-sm hover:bg-gray-700 transition-colors"
@@ -107,15 +124,94 @@ export default function EventsByDate() {
</Link>
</div>
+ {/* Filter bar */}
+ <Form method="get" className="mb-6 flex flex-wrap gap-3 items-end">
+ <input type="hidden" name="date" value={date} />
+
+ <div className="flex flex-col gap-1">
+ <label className="text-xs text-gray-400">キーワード</label>
+ <input
+ name="keyword"
+ type="text"
+ defaultValue={searchParams.get("keyword") ?? ""}
+ placeholder="アーティスト名、イベント名..."
+ className="rounded-md bg-gray-800 border border-gray-700 px-3 py-1.5 text-sm focus:outline-none focus:ring-1 focus:ring-indigo-500 w-full sm:w-44"
+ />
+ </div>
+
+ <div className="flex flex-col gap-1">
+ <label className="text-xs text-gray-400">会場</label>
+ <select
+ name="venue_id"
+ defaultValue={searchParams.get("venue_id") ?? ""}
+ className="rounded-md bg-gray-800 border border-gray-700 px-3 py-1.5 text-sm focus:outline-none focus:ring-1 focus:ring-indigo-500"
+ >
+ <option value="">すべて</option>
+ {venues.map((v) => (
+ <option key={v.id} value={v.id}>{v.name}</option>
+ ))}
+ </select>
+ </div>
+
+ {areas.length > 0 && (
+ <div className="flex flex-col gap-1">
+ <label className="text-xs text-gray-400">エリア</label>
+ <select
+ name="area"
+ defaultValue={searchParams.get("area") ?? ""}
+ className="rounded-md bg-gray-800 border border-gray-700 px-3 py-1.5 text-sm focus:outline-none focus:ring-1 focus:ring-indigo-500"
+ >
+ <option value="">すべて</option>
+ {areas.map((a) => (
+ <option key={a} value={a}>{a}</option>
+ ))}
+ </select>
+ </div>
+ )}
+
+ <div className="flex flex-col gap-1">
+ <label className="text-xs text-gray-400">キャパシティ</label>
+ <select
+ name="capacity_range"
+ defaultValue={searchParams.get("capacity_range") ?? ""}
+ className="rounded-md bg-gray-800 border border-gray-700 px-3 py-1.5 text-sm focus:outline-none focus:ring-1 focus:ring-indigo-500"
+ >
+ <option value="">すべて</option>
+ <option value="small">〜100人(小箱)</option>
+ <option value="medium">100〜300人(中箱)</option>
+ <option value="large">300人〜(大箱)</option>
+ </select>
+ </div>
+
+ <button
+ type="submit"
+ className="rounded-md bg-gray-700 px-4 py-1.5 text-sm font-medium hover:bg-gray-600 transition-colors"
+ >
+ 絞り込む
+ </button>
+
+ {hasFilters && (
+ <a
+ href={`/events/by-date?date=${date}`}
+ className="rounded-md border border-gray-700 px-4 py-1.5 text-sm text-gray-400 hover:text-white transition-colors"
+ >
+ クリア
+ </a>
+ )}
+ </Form>
+
{/* Summary */}
{totalEvents > 0 && (
<p className="mb-6 text-sm text-gray-500">
- {groups.length} 会場 / {totalEvents} イベント
+ {visibleGroups.length} 会場 / {totalEvents} イベント
+ {hiddenCount > 0 && (
+ <> — <Link to="/venues" className="text-indigo-400 hover:text-indigo-300 underline">{hiddenCount} 会場非表示</Link></>
+ )}
</p>
)}
{/* Venue groups */}
- {groups.length === 0 ? (
+ {visibleGroups.length === 0 ? (
<div className="mt-16 text-center text-gray-500">
<p className="text-lg">この日のイベントはありません</p>
<p className="mt-2 text-sm">
@@ -125,7 +221,7 @@ export default function EventsByDate() {
</div>
) : (
<div className="flex flex-col gap-8">
- {groups.map(({ venue, events }) => (
+ {visibleGroups.map(({ venue, events }) => (
<section key={venue.id}>
<div className="mb-3 flex items-baseline gap-2">
<h2 className="text-lg font-semibold text-white">