From 8edb717dd20fa2c90187bfefe7be875bb5022e3a Mon Sep 17 00:00:00 2001 From: Logan Date: Sat, 20 Jun 2026 23:34:45 -0400 Subject: [PATCH] Add trips to UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit lib/types.ts — TripRecord and TripEvent types lib/c2api.ts — getTrips, getTrip, createTrip, deleteTrip, createTripEvent, deleteTripEvent lib/useTrips.ts — Firestore realtime hook on the trips collection, ordered by start date app/trips/page.tsx — List page split into Upcoming / Past sections, card click navigates to detail, "+ New Trip" modal for admins with all fields including date range and maps link app/trips/[id]/page.tsx — Detail page fetched via C2 API (gets trip + events in one call), day-by-day itinerary with time, location, maps link, notes, and Discord attendees. Add Event modal (date constrained to trip range). Admin-only delete trip + remove event. components/Nav.tsx — Trips link added to the nav --- drb-frontend/app/trips/[id]/page.tsx | 403 +++++++++++++++++++++++++++ drb-frontend/app/trips/page.tsx | 238 ++++++++++++++++ drb-frontend/components/Nav.tsx | 1 + drb-frontend/lib/c2api.ts | 13 + drb-frontend/lib/types.ts | 26 ++ drb-frontend/lib/useTrips.ts | 38 +++ 6 files changed, 719 insertions(+) create mode 100644 drb-frontend/app/trips/[id]/page.tsx create mode 100644 drb-frontend/app/trips/page.tsx create mode 100644 drb-frontend/lib/useTrips.ts diff --git a/drb-frontend/app/trips/[id]/page.tsx b/drb-frontend/app/trips/[id]/page.tsx new file mode 100644 index 0000000..ca4833a --- /dev/null +++ b/drb-frontend/app/trips/[id]/page.tsx @@ -0,0 +1,403 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useParams, useRouter } from "next/navigation"; +import { useAuth } from "@/components/AuthProvider"; +import { c2api } from "@/lib/c2api"; +import type { TripRecord, TripEvent } from "@/lib/types"; + +// --------------------------------------------------------------------------- +// Formatting helpers +// --------------------------------------------------------------------------- + +function fmtDate(iso: string) { + return new Date(`${iso}T12:00:00`).toLocaleDateString("en-US", { + month: "short", day: "numeric", year: "numeric", + }); +} + +function fmtDayLabel(iso: string) { + return new Date(`${iso}T12:00:00`).toLocaleDateString("en-US", { + weekday: "long", month: "short", day: "numeric", + }); +} + +function fmtTime(t: string | null) { + if (!t) return null; + const [h, m] = t.split(":").map(Number); + const d = new Date(); + d.setHours(h, m, 0, 0); + return d.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" }); +} + +function dateRange(start: string, end: string): string[] { + const dates: string[] = []; + const cur = new Date(`${start}T12:00:00`); + const endDate = new Date(`${end}T12:00:00`); + while (cur <= endDate) { + dates.push(cur.toISOString().slice(0, 10)); + cur.setDate(cur.getDate() + 1); + } + return dates; +} + +// --------------------------------------------------------------------------- +// Add Event modal +// --------------------------------------------------------------------------- + +function AddEventModal({ trip, onClose, onAdd }: { + trip: TripRecord; + onClose: () => void; + onAdd: (body: object) => Promise; +}) { + const [title, setTitle] = useState(""); + const [date, setDate] = useState(trip.start_date); + const [time, setTime] = useState(""); + const [location, setLocation] = useState(""); + const [mapsLink, setMapsLink] = useState(""); + const [notes, setNotes] = useState(""); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(null); + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + setSaving(true); + setError(null); + try { + await onAdd({ + title, + date, + time: time || null, + location: location || null, + maps_link: mapsLink || null, + notes: notes || null, + }); + onClose(); + } catch { + setError("Failed to add event. Make sure the date is within the trip range."); + } finally { + setSaving(false); + } + } + + return ( +
+
+

Add Event

+ +
+ + setTitle(e.target.value)} + placeholder="Dinner on Broadway" + className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-indigo-500" + /> +
+ +
+
+ + setDate(e.target.value)} + className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-indigo-500" + /> +
+
+ + setTime(e.target.value)} + className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-indigo-500" + /> +
+
+ +
+ + setLocation(e.target.value)} + placeholder={trip.location} + className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-indigo-500" + /> +
+ +
+ + setMapsLink(e.target.value)} + placeholder="https://maps.google.com/…" + className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-indigo-500" + /> +
+ +
+ +