summaryrefslogtreecommitdiff
path: root/app/components/FilterBar.tsx
diff options
context:
space:
mode:
authoryyamashita <yyamashita@mosquit.one>2026-05-06 22:07:53 +0900
committeryyamashita <yyamashita@mosquit.one>2026-05-06 22:07:53 +0900
commitbe55729482296663da8c96723bfd22080e6762c1 (patch)
treefcd94b1dc5c55f3a80796c90a555863d13fc9a95 /app/components/FilterBar.tsx
parent014b29bc22b1c207a03dd560051ecdd5df63f0b1 (diff)
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>
Diffstat (limited to 'app/components/FilterBar.tsx')
-rw-r--r--app/components/FilterBar.tsx85
1 files changed, 85 insertions, 0 deletions
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));
+}