diff options
| author | yyamashita <yyamashita@mosquit.one> | 2026-05-06 22:07:53 +0900 |
|---|---|---|
| committer | yyamashita <yyamashita@mosquit.one> | 2026-05-06 22:07:53 +0900 |
| commit | be55729482296663da8c96723bfd22080e6762c1 (patch) | |
| tree | fcd94b1dc5c55f3a80796c90a555863d13fc9a95 /app/components/FilterBar.tsx | |
| parent | 014b29bc22b1c207a03dd560051ecdd5df63f0b1 (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.tsx | 85 |
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)); +} |
