From 3a961122bdf64cf72cf1b3770e9d71865a3aac52 Mon Sep 17 00:00:00 2001
From: yyamashita <yyamashita@hetzner.yyamashita.com>
Date: Fri, 22 May 2026 10:31:14 +0900
Subject: Add filters to date view (keyword, venue, area, capacity)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---
 app/routes/events.by-date.tsx | 114 ++++++++++++++++++++++++++++++++++++++----
 1 file changed, 105 insertions(+), 9 deletions(-)

(limited to 'app/routes/events.by-date.tsx')

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">
-- 
cgit v1.2.3

