From be55729482296663da8c96723bfd22080e6762c1 Mon Sep 17 00:00:00 2001
From: yyamashita <yyamashita@mosquit.one>
Date: Wed, 6 May 2026 22:07:53 +0900
Subject: Add Tokyo livehouse event aggregator service

Full-stack React Router v7 app that scrapes event listings from major
Tokyo live venues (Liquid Room, WWW/WWW X, Shibuya O-EAST, Shinjuku LOFT,
Club Quattro) and stores them in SQLite for browsing and search.

- Modular scraper architecture: add a new venue by dropping a file in
  app/scrapers/ and registering it in index.ts
- Routes: /events (filter by keyword/venue/date), /events/:id, /venues,
  GET /api/scrape
- EventCard shows artist, date/time, venue, ticket URL, and fee
- Post-scrape per-venue Markdown files generated to events/ (dev reference)
- /add-livehouse Claude Code skill defined in .claude/commands/

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---
 app/components/FilterBar.tsx | 85 ++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 85 insertions(+)
 create mode 100644 app/components/FilterBar.tsx

(limited to 'app/components/FilterBar.tsx')

diff --git a/app/components/FilterBar.tsx b/app/components/FilterBar.tsx
new file mode 100644
index 0000000..97a3c02
--- /dev/null
+++ b/app/components/FilterBar.tsx
@@ -0,0 +1,85 @@
+import { Form, useSearchParams } from "react-router";
+import type { Venue } from "~/lib/db.server";
+
+interface Props {
+  venues: Venue[];
+}
+
+export default function FilterBar({ venues }: Props) {
+  const [searchParams] = useSearchParams();
+
+  return (
+    <Form method="get" className="flex flex-wrap gap-3 items-end">
+      {/* Keyword */}
+      <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-52"
+        />
+      </div>
+
+      {/* Venue */}
+      <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>
+
+      {/* Date from */}
+      <div className="flex flex-col gap-1">
+        <label className="text-xs text-gray-400">開始日</label>
+        <input
+          name="date_from"
+          type="date"
+          defaultValue={searchParams.get("date_from") ?? ""}
+          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"
+        />
+      </div>
+
+      {/* Date to */}
+      <div className="flex flex-col gap-1">
+        <label className="text-xs text-gray-400">終了日</label>
+        <input
+          name="date_to"
+          type="date"
+          defaultValue={searchParams.get("date_to") ?? ""}
+          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"
+        />
+      </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(searchParams) && (
+        <a
+          href="/events"
+          className="rounded-md border border-gray-700 px-4 py-1.5 text-sm text-gray-400 hover:text-white transition-colors"
+        >
+          クリア
+        </a>
+      )}
+    </Form>
+  );
+}
+
+function hasFilters(params: URLSearchParams): boolean {
+  return ["keyword", "venue_id", "date_from", "date_to"].some((k) => params.get(k));
+}
-- 
cgit v1.2.3

