add event editing

This commit is contained in:
Logan
2026-06-21 15:35:57 -04:00
parent fce189d8c9
commit 4dd3343026
4 changed files with 111 additions and 14 deletions
+12
View File
@@ -161,6 +161,18 @@ class TripEventCreate(BaseModel):
tags: List[str] = [] # tag labels applied to this event
class TripEventUpdate(BaseModel):
title: Optional[str] = None
date: Optional[str] = None
start_time: Optional[str] = None
end_time: Optional[str] = None
location: Optional[str] = None
maps_link: Optional[str] = None
place_id: Optional[str] = None
notes: Optional[str] = None
tags: Optional[List[str]] = None
class AttendeeAction(BaseModel):
discord_user_id: str
discord_username: Optional[str] = None
+40 -1
View File
@@ -5,7 +5,7 @@ from datetime import datetime, timezone
from typing import Optional
from fastapi import APIRouter, HTTPException, Depends
from pydantic import BaseModel
from app.models import TripCreate, TripEventCreate, AttendeeAction
from app.models import TripCreate, TripEventCreate, TripEventUpdate, AttendeeAction
from app.internal import firestore as fstore
from app.config import settings
from app.internal.logger import logger
@@ -319,6 +319,45 @@ async def create_event(trip_id: str, body: TripEventCreate):
return doc
@router.patch("/{trip_id}/events/{event_id}")
async def update_event(trip_id: str, event_id: str, body: TripEventUpdate):
event = await fstore.doc_get("trip_events", event_id)
if not event or event.get("trip_id") != trip_id:
raise HTTPException(404, f"Event '{event_id}' not found in trip '{trip_id}'.")
trip = await fstore.doc_get("trips", trip_id)
if not trip:
raise HTTPException(404, f"Trip '{trip_id}' not found.")
updates: dict = {}
if body.title is not None:
updates["title"] = body.title
if body.date is not None:
if not (trip["start_date"] <= body.date <= trip["end_date"]):
raise HTTPException(400, f"Event date {body.date} is outside the trip range.")
updates["date"] = body.date
if body.start_time is not None:
updates["start_time"] = body.start_time or None
if body.end_time is not None:
updates["end_time"] = body.end_time or None
if body.location is not None:
updates["location"] = body.location
updates["location_inherited"] = False
if body.maps_link is not None:
updates["maps_link"] = body.maps_link or None
if body.place_id is not None:
updates["place_id"] = body.place_id or None
if body.notes is not None:
updates["notes"] = body.notes or None
if body.tags is not None:
updates["tags"] = body.tags
if updates:
await fstore.doc_update("trip_events", event_id, updates)
return {**event, **updates}
@router.delete("/{trip_id}/events/{event_id}")
async def delete_event(
trip_id: str,
+57 -13
View File
@@ -180,12 +180,15 @@ function AddEventModal({
onClose,
onAdd,
prefill,
editEventId,
}: {
trip: TripRecord;
onClose: () => void;
onAdd: (body: object) => Promise<void>;
prefill?: Partial<TripEvent>;
editEventId?: string;
}) {
const isEdit = !!editEventId;
const availableTags = trip.available_tags ?? [];
const [title, setTitle] = useState(prefill?.title ?? "");
const [date, setDate] = useState(prefill?.date ?? trip.start_date);
@@ -224,7 +227,7 @@ function AddEventModal({
});
onClose();
} catch {
setError("Failed to add event. Check the date is within the trip range.");
setError(isEdit ? "Failed to save changes." : "Failed to add event. Check the date is within the trip range.");
} finally {
setSaving(false);
}
@@ -236,7 +239,7 @@ function AddEventModal({
onSubmit={handleSubmit}
className="bg-gray-900 border border-gray-700 rounded-xl p-6 w-full max-w-lg space-y-4 max-h-[90vh] overflow-y-auto"
>
<h2 className="text-white font-bold text-lg">Add Event</h2>
<h2 className="text-white font-bold text-lg">{isEdit ? "Edit Event" : "Add Event"}</h2>
<div>
<label className="text-xs text-gray-400 block mb-1">Title</label>
@@ -341,7 +344,7 @@ function AddEventModal({
type="submit" disabled={saving}
className="bg-indigo-600 hover:bg-indigo-500 disabled:opacity-50 text-white text-sm rounded-lg px-4 py-2"
>
{saving ? "Adding…" : "Add Event"}
{saving ? (isEdit ? "Saving…" : "Adding…") : (isEdit ? "Save Changes" : "Add Event")}
</button>
</div>
</form>
@@ -359,12 +362,14 @@ function DayTimeline({
events,
isAdmin,
onDelete,
onEdit,
driveSegments,
availableTags,
}: {
events: TripEvent[];
isAdmin: boolean;
onDelete: (id: string) => void;
onEdit: (event: TripEvent) => void;
driveSegments: { fromId: string; toId: string; text: string }[];
availableTags: string[];
}) {
@@ -472,12 +477,20 @@ function DayTimeline({
</a>
)}
{isAdmin && (
<button
onClick={() => onDelete(e.event_id)}
className="text-gray-600 hover:text-red-400 text-base leading-none opacity-0 group-hover:opacity-100 transition-opacity"
>
×
</button>
<>
<button
onClick={() => onEdit(e)}
className="text-gray-600 hover:text-indigo-400 text-xs opacity-0 group-hover:opacity-100 transition-opacity"
>
Edit
</button>
<button
onClick={() => onDelete(e.event_id)}
className="text-gray-600 hover:text-red-400 text-base leading-none opacity-0 group-hover:opacity-100 transition-opacity"
>
×
</button>
</>
)}
</div>
</div>
@@ -526,10 +539,16 @@ function DayTimeline({
</a>
)}
{isAdmin && (
<button onClick={() => onDelete(e.event_id)}
className="text-gray-600 hover:text-red-400 text-sm opacity-0 group-hover:opacity-100 transition-opacity">
×
</button>
<>
<button onClick={() => onEdit(e)}
className="text-xs text-gray-600 hover:text-indigo-400 opacity-0 group-hover:opacity-100 transition-opacity">
Edit
</button>
<button onClick={() => onDelete(e.event_id)}
className="text-gray-600 hover:text-red-400 text-sm opacity-0 group-hover:opacity-100 transition-opacity">
×
</button>
</>
)}
</div>
</div>
@@ -842,6 +861,7 @@ export default function TripDetailPage() {
const [loading, setLoading] = useState(true);
const [selectedDay, setSelectedDay] = useState<string>("");
const [showAdd, setShowAdd] = useState(false);
const [editEvent, setEditEvent] = useState<TripEvent | null>(null);
const [driveTimes, setDriveTimes] = useState<Record<string, { fromId: string; toId: string; text: string }[]>>({});
const [tagInput, setTagInput] = useState("");
@@ -901,6 +921,18 @@ export default function TripDetailPage() {
} catch (e) { console.error(e); }
}
async function handleUpdateEvent(body: object) {
if (!trip || !editEvent) return;
const updated = await c2api.updateTripEvent(trip.trip_id, editEvent.event_id, body);
setTrip((prev) => {
if (!prev) return prev;
const events = prev.events.map((e) => e.event_id === updated.event_id ? updated : e)
.sort((a, b) => a.date.localeCompare(b.date) || (a.start_time ?? "").localeCompare(b.start_time ?? ""));
return { ...prev, events };
});
setEditEvent(null);
}
async function handleAddEvent(body: object) {
if (!trip) return;
const event = await c2api.createTripEvent(trip.trip_id, body);
@@ -1079,6 +1111,7 @@ export default function TripDetailPage() {
events={dayEvents}
isAdmin={isAdmin}
onDelete={handleDeleteEvent}
onEdit={setEditEvent}
driveSegments={driveTimes[selectedDay] ?? []}
availableTags={trip.available_tags ?? []}
/>
@@ -1100,6 +1133,17 @@ export default function TripDetailPage() {
prefill={selectedDay ? { date: selectedDay } : undefined}
/>
)}
{/* Edit event modal */}
{editEvent && (
<AddEventModal
trip={trip}
onClose={() => setEditEvent(null)}
onAdd={handleUpdateEvent}
prefill={editEvent}
editEventId={editEvent.event_id}
/>
)}
</div>
);
}
+2
View File
@@ -146,6 +146,8 @@ export const c2api = {
request<{ available_tags: string[] }>(`/trips/${id}/tags`, { method: "PUT", body: JSON.stringify({ available_tags }) }),
createTripEvent: (tripId: string, body: object) =>
request<import("@/lib/types").TripEvent>(`/trips/${tripId}/events`, { method: "POST", body: JSON.stringify(body) }),
updateTripEvent: (tripId: string, eventId: string, body: object) =>
request<import("@/lib/types").TripEvent>(`/trips/${tripId}/events/${eventId}`, { method: "PATCH", body: JSON.stringify(body) }),
deleteTripEvent: (tripId: string, eventId: string) =>
request(`/trips/${tripId}/events/${eventId}`, { method: "DELETE" }),
tripChat: (tripId: string, message: string, history: { role: string; content: string }[]) =>