From 05d2b35a85a46dde9a1264d3002ba86e02e3d5eb Mon Sep 17 00:00:00 2001 From: yyamashita Date: Sun, 10 May 2026 22:47:46 +0900 Subject: Add calendar export (Google/ICS) and extend scrape window to 65 days MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Event detail page: add Google Calendar link and .ics download button - New route GET /api/events/:id/calendar.ics returns RFC 5545 iCalendar - Scrape window extended from 35 → 65 days (~2 months ahead) Co-Authored-By: Claude Sonnet 4.6 --- app/routes/events.$id.tsx | 81 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) (limited to 'app/routes/events.$id.tsx') diff --git a/app/routes/events.$id.tsx b/app/routes/events.$id.tsx index 423cda5..87c5e2d 100644 --- a/app/routes/events.$id.tsx +++ b/app/routes/events.$id.tsx @@ -10,6 +10,55 @@ export async function loader({ params }: Route.LoaderArgs) { return { event }; } +function buildGoogleCalendarUrl(event: Awaited>["event"]): string { + const dates = (() => { + if (event.start_time && /^\d{1,2}:\d{2}/.test(event.start_time)) { + const [h, m] = event.start_time.split(":").map(Number); + const [y, mo, d] = event.date.split("-").map(Number); + const pad = (n: number) => String(n).padStart(2, "0"); + const startStr = `${event.date.replace(/-/g, "")}T${pad(h)}${pad(m)}00`; + const endH = h + 2; + let endStr: string; + if (endH < 24) { + endStr = `${event.date.replace(/-/g, "")}T${pad(endH)}${pad(m)}00`; + } else { + const next = new Date(y, mo - 1, d + 1); + endStr = + `${next.getFullYear()}${pad(next.getMonth() + 1)}${pad(next.getDate())}` + + `T${pad(endH - 24)}${pad(m)}00`; + } + return `${startStr}/${endStr}`; + } + const [y, mo, d] = event.date.split("-").map(Number); + const next = new Date(y, mo - 1, d + 1); + const pad = (n: number) => String(n).padStart(2, "0"); + const dateStr = event.date.replace(/-/g, ""); + const nextStr = `${next.getFullYear()}${pad(next.getMonth() + 1)}${pad(next.getDate())}`; + return `${dateStr}/${nextStr}`; + })(); + + const details: string[] = []; + if (event.artist) details.push(`出演: ${event.artist}`); + if (event.open_time) details.push(`OPEN: ${event.open_time}`); + if (event.start_time) details.push(`START: ${event.start_time}`); + if (event.price) details.push(`料金: ${event.price}`); + if (event.source_url) details.push(`詳細: ${event.source_url}`); + if (event.ticket_url) details.push(`チケット: ${event.ticket_url}`); + + const location = [event.venue_name, event.venue_area].filter(Boolean).join(" "); + + const params = new URLSearchParams({ + action: "TEMPLATE", + text: event.title, + dates, + location, + ctz: "Asia/Tokyo", + }); + if (details.length > 0) params.set("details", details.join("\n")); + + return `https://calendar.google.com/calendar/render?${params.toString()}`; +} + export default function EventDetail() { const { event } = useLoaderData(); @@ -99,6 +148,27 @@ export default function EventDetail() { )} +
+

カレンダーに追加

+ +
+

最終取得: {event.fetched_at}

@@ -108,6 +178,17 @@ export default function EventDetail() { ); } +function CalendarIcon() { + return ( + + + + + + + ); +} + function Detail({ label, value }: { label: string; value: string }) { return (
-- cgit v1.2.3