diff options
| author | yyamashita <yyamashita@hetzner.yyamashita.com> | 2026-05-22 10:31:14 +0900 |
|---|---|---|
| committer | yyamashita <yyamashita@hetzner.yyamashita.com> | 2026-05-22 10:31:14 +0900 |
| commit | 3a961122bdf64cf72cf1b3770e9d71865a3aac52 (patch) | |
| tree | 8e95d3575a70bd67e8154c6fce0e92712a1a5990 /app/routes/events.by-date.tsx | |
| parent | 7c5860ce57a334b7ab4a501de78f1a7f41c8cdcc (diff) | |
Add filters to date view (keyword, venue, area, capacity)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'app/routes/events.by-date.tsx')
| -rw-r--r-- | app/routes/events.by-date.tsx | 114 |
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"> |
