From 76c137fc67fd6b1b09d1cb05bb676733326ed9d5 Mon Sep 17 00:00:00 2001 From: Ethan Burdett Date: Mon, 6 Oct 2025 15:18:31 -0400 Subject: [PATCH 1/6] added campus christmas toggle --- components/EventFlow.tsx | 2 +- components/EventSettings.tsx | 29 +++++++++- components/Legend.tsx | 103 ++++++++++++++++++++--------------- lib/api/update/Event.ts | 12 ++++ prisma/schema.prisma | 1 + 5 files changed, 102 insertions(+), 45 deletions(-) diff --git a/components/EventFlow.tsx b/components/EventFlow.tsx index 609f055..103607f 100644 --- a/components/EventFlow.tsx +++ b/components/EventFlow.tsx @@ -524,7 +524,7 @@ function Flow({ {isEditable && ( - + )} diff --git a/components/EventSettings.tsx b/components/EventSettings.tsx index b7afc32..b848a7c 100644 --- a/components/EventSettings.tsx +++ b/components/EventSettings.tsx @@ -25,6 +25,7 @@ import { import { SyncLocations, UpdateGettingStarted, + UpdateCampusChristmas, UpdateName, } from "@/lib/api/update/Event"; import { useParams, useRouter } from "next/navigation"; @@ -38,6 +39,7 @@ const formSchema = z.object({ eventName: z.string().min(1, "Event name is required"), eventLocations: z.array(z.string()).optional(), isGettingStarted: z.boolean(), + isCampusChristmas: z.boolean(), }); export default function EventSettings({ @@ -71,6 +73,7 @@ export default function EventSettings({ await SyncLocations(event, data.eventLocations ?? []); await UpdateName(event.id, data.eventName); await UpdateGettingStarted(event.id, data.isGettingStarted); + await UpdateCampusChristmas(event.id, data.isCampusChristmas); setIsOpen(false); setLocationAdderOpen(false); if (!data.eventLocations?.find((v) => v === locationId)) { @@ -84,10 +87,11 @@ export default function EventSettings({ currentLocations.map((location) => location.id) ); form.setValue("isGettingStarted", event.isGS); + form.setValue("isCampusChristmas", event.isCC); setCurrentLocations( currentLocations.sort((a, b) => a.name.localeCompare(b.name)) ); - }, [currentLocations, form, event.isGS]); + }, [currentLocations, form, event.isGS, event.isCC]); return ( @@ -193,6 +197,29 @@ export default function EventSettings({ )} /> + {/* Campus Christmas switch */} + ( + +
+ + Campus Christmas + + + Is this event related to Campus Christmas? + +
+ + + +
+ )} + />
); @@ -143,27 +152,35 @@ const Legend: React.FC = ({ isGettingStarted, onDrop }) => { - {categories.map( - (category) => - (category.value !== "getting-started" || - (category.value === "getting-started" && isGettingStarted)) && ( - - {category.title} - -
- {category.items.map((item, index) => ( - - ))} -
-
-
- ) - )} + {categories.map((category) => { + // Skip getting-started if not enabled + if (category.value === "getting-started" && !isGettingStarted) { + return null; + } + + // Skip campus-christmas if not enabled + if (category.value === "campus-christmas" && !isCampusChristmas) { + return null; + } + + return ( + + {category.title} + +
+ {category.items.map((item, index) => ( + + ))} +
+
+
+ ); + })}
diff --git a/lib/api/update/Event.ts b/lib/api/update/Event.ts index e273c4d..312d0e3 100644 --- a/lib/api/update/Event.ts +++ b/lib/api/update/Event.ts @@ -62,3 +62,15 @@ export async function UpdateGettingStarted(eventId: string, isGS: boolean) { }, }); } + +export async function UpdateCampusChristmas(eventId: string, isCC: boolean) { + revalidatePath("event/"); + return await prisma.event.update({ + where: { + id: eventId, + }, + data: { + isCC, + }, + }); +} diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 7f15d45..9431b66 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -11,6 +11,7 @@ model Event { id String @id @default(cuid()) name String isGS Boolean @default(false) + isCC Boolean @default(false) locations EventToLocation[] Recents Recents[] } From fd59ca67fa3a66efe40636eccba994d63248e783 Mon Sep 17 00:00:00 2001 From: Ethan Burdett Date: Tue, 7 Oct 2025 15:42:39 -0400 Subject: [PATCH 2/6] added event duplication --- app/home/page.tsx | 2 + components/EventSelectForm.tsx | 95 ++++++++++++++++++++++++++++++---- types/Event.ts | 1 + 3 files changed, 87 insertions(+), 11 deletions(-) diff --git a/app/home/page.tsx b/app/home/page.tsx index 2716d19..347cce5 100644 --- a/app/home/page.tsx +++ b/app/home/page.tsx @@ -25,9 +25,11 @@ export default async function EventSelect() { id: true, name: true, isGS: true, + isCC: true, locations: { select: { id: true, + locationId: true, }, }, }, diff --git a/components/EventSelectForm.tsx b/components/EventSelectForm.tsx index 1c849c3..991998f 100644 --- a/components/EventSelectForm.tsx +++ b/components/EventSelectForm.tsx @@ -35,7 +35,15 @@ import CreateEvent from "@/lib/api/create/createEvent"; import DeleteEntity from "@/lib/api/delete/DeleteEntity"; import AddIcon from "@mui/icons-material/Add"; import RemoveIcon from "@mui/icons-material/Remove"; +import { CopyPlus } from "lucide-react"; import { EventWithLocationIds } from "@/types/Event"; +import { + SyncLocations, + UpdateGettingStarted, + UpdateCampusChristmas, +} from "@/lib/api/update/Event"; +import { GetEventLocationInfo } from "@/lib/api/read/GetEventLocationInfo"; +import SaveState from "@/lib/api/update/ReactFlowSave"; export default function EventSelectForm({ events, @@ -76,6 +84,60 @@ export default function EventSelectForm({ setSelectedEventLocations([]); } + async function duplicateEvent(id: string) { + const eventToCopy = events.find((e) => e.id === id); + if (!eventToCopy) return; + + // Create the new event + const newEvent = await CreateEvent(eventToCopy.name + " (Copy)"); + + // Copy all locations and their states + const locationLinks = eventToCopy.locations; + console.log(locationLinks, eventToCopy); + + if (locationLinks && locationLinks.length > 0) { + // Extract location IDs directly from the links + const locationIds = locationLinks.map(link => link.locationId); + + console.log(locationIds); + if (locationIds.length > 0) { + await SyncLocations(newEvent, locationIds); + + // For each location, copy the state from the original event + for (const locationId of locationIds) { + // Get the original event's state for this location + const originalEventLocation = await GetEventLocationInfo( + id, + locationId + ); + + if (originalEventLocation?.state) { + // Save the same state to the new event's location + await SaveState( + newEvent.id, + locationId, + originalEventLocation.state, + "" // Empty client ID since this is a copy operation + ); + } + } + } + } + + // Copy the isGS and isCC flags + if (eventToCopy.isGS !== undefined) { + await UpdateGettingStarted(newEvent.id, eventToCopy.isGS); + } + if (eventToCopy.isCC !== undefined) { + await UpdateCampusChristmas(newEvent.id, eventToCopy.isCC); + } + + setDropdownEvents([...dropdownEvents, newEvent]); + setEventId(newEvent.id); + setSelectedEventLocations([]); + router.push(`/home/event/${newEvent.id}`); + } + function deleteLocation(id: string, eventId: string) { DeleteEntity("location", id, eventId); setDeleteDialogOpen(false); @@ -133,17 +195,28 @@ export default function EventSelectForm({
{event.name} {canEdit && ( - +
+ + +
)}
diff --git a/types/Event.ts b/types/Event.ts index 9e8ac18..1ad5252 100644 --- a/types/Event.ts +++ b/types/Event.ts @@ -18,5 +18,6 @@ export type EventWithLocations = { export interface EventWithLocationIds extends Event { locations: Array<{ id: string; + locationId: string; }>; } From cb50adb20524f005fca5aebc49674c586fa377db Mon Sep 17 00:00:00 2001 From: Ethan Burdett Date: Sun, 12 Oct 2025 16:49:23 -0400 Subject: [PATCH 3/6] started adding archive events feature --- app/home/page.tsx | 6 +++- components/EventSelectForm.tsx | 52 +++++++++++++++++++++++++++++++++- lib/api/update/Event.ts | 12 ++++++++ prisma/schema.prisma | 13 +++++---- 4 files changed, 75 insertions(+), 8 deletions(-) diff --git a/app/home/page.tsx b/app/home/page.tsx index 347cce5..ac83ac5 100644 --- a/app/home/page.tsx +++ b/app/home/page.tsx @@ -18,14 +18,18 @@ import { prisma } from "@/lib/api/db"; import { Box } from "@mui/material"; import { Suspense } from "react"; -// Collect all events from doradev database +// Collect all unarchived events from doradev database export default async function EventSelect() { const events = await prisma.event.findMany({ + where: { + isArchived: false, + }, select: { id: true, name: true, isGS: true, isCC: true, + isArchived: true, locations: { select: { id: true, diff --git a/components/EventSelectForm.tsx b/components/EventSelectForm.tsx index 991998f..8e64ece 100644 --- a/components/EventSelectForm.tsx +++ b/components/EventSelectForm.tsx @@ -35,12 +35,13 @@ import CreateEvent from "@/lib/api/create/createEvent"; import DeleteEntity from "@/lib/api/delete/DeleteEntity"; import AddIcon from "@mui/icons-material/Add"; import RemoveIcon from "@mui/icons-material/Remove"; -import { CopyPlus } from "lucide-react"; +import { CopyPlus, Archive } from "lucide-react"; import { EventWithLocationIds } from "@/types/Event"; import { SyncLocations, UpdateGettingStarted, UpdateCampusChristmas, + UpdateArchive, } from "@/lib/api/update/Event"; import { GetEventLocationInfo } from "@/lib/api/read/GetEventLocationInfo"; import SaveState from "@/lib/api/update/ReactFlowSave"; @@ -55,6 +56,7 @@ export default function EventSelectForm({ const [eventId, setEventId] = useState(""); const [locationAdderOpen, setLocationAdderOpen] = useState(false); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [archiveDialogOpen, setArchiveDialogOpen] = useState(false); const [insertDialogOpen, setInsertDialogOpen] = useState(false); const [eventToCreate, setEventToCreate] = useState(""); const [dropdownEvents, setDropdownEvents] = useState(events); @@ -64,6 +66,10 @@ export default function EventSelectForm({ entity: Event | Location; type: "event" | "location"; }>(); + const [eventToArchive, setEventToArchive] = useState<{ + entity: Event; + type: "event"; + }>(); const [selectedEventLocations, setSelectedEventLocations] = useState< Location[] >([]); @@ -138,6 +144,16 @@ export default function EventSelectForm({ router.push(`/home/event/${newEvent.id}`); } + async function archiveEvent(id: string) { + setArchiveDialogOpen(false); + + // Archive the event in the database + await UpdateArchive(id, true); + + // Remove the event from the dropdown list + setDropdownEvents(dropdownEvents.filter((e) => e.id !== id)); + } + function deleteLocation(id: string, eventId: string) { DeleteEntity("location", id, eventId); setDeleteDialogOpen(false); @@ -206,6 +222,17 @@ export default function EventSelectForm({ > + + + + + )) + )} + + + + + + {/* Unarchive Confirmation Dialog */} + + + + Unarchive Event + + Are you sure you want to unarchive "{selectedEvent?.name} + "? It will be restored to the active events list. + + + + { + setUnarchiveDialogOpen(false); + setSelectedEvent(null); + }} + > + Cancel + + { + if (selectedEvent) { + handleUnarchive(selectedEvent.id); + } + }} + > + Unarchive + + + + + + {/* Delete Confirmation Dialog */} + + + + Delete Event + + Are you sure you want to permanently delete " + {selectedEvent?.name}"? This action cannot be undone. + + + + { + setDeleteDialogOpen(false); + setSelectedEvent(null); + }} + > + Cancel + + { + if (selectedEvent) { + handleDelete(selectedEvent.id); + } + }} + className="bg-red-500 hover:bg-red-600" + > + Delete + + + + + + ); +} diff --git a/components/LocationList.tsx b/components/LocationList.tsx index 504d56a..9dde35d 100644 --- a/components/LocationList.tsx +++ b/components/LocationList.tsx @@ -32,8 +32,9 @@ export default function LocationList({ eventId, eventName }: Props) { const [locations, setLocations] = useState([]); const [isLoading, setIsLoading] = useState(true); const [isNavigating, setIsNavigating] = useState(false); + const [event, setEvent] = useState<{ isArchived: boolean } | null>(null); const { data: session } = useSession(); - const canEdit = session?.role === "ADMIN" || session?.role === "EDITOR"; + const canEdit = (session?.role === "ADMIN" || session?.role === "EDITOR") && !event?.isArchived; // added state const [locationAdderOpen, setLocationAdderOpen] = useState(false); @@ -55,7 +56,10 @@ export default function LocationList({ eventId, eventName }: Props) { eventData?.locations.some((evLoc) => evLoc.locationId === l.id) ) .sort((a, b) => a.name.localeCompare(b.name)) || []; - if (active) setLocations(filtered); + if (active) { + setLocations(filtered); + setEvent(eventData ? { isArchived: eventData.isArchived } : null); + } } finally { if (active) setIsLoading(false); } diff --git a/components/layout/ArchivedEventsButton.tsx b/components/layout/ArchivedEventsButton.tsx new file mode 100644 index 0000000..f0a6e72 --- /dev/null +++ b/components/layout/ArchivedEventsButton.tsx @@ -0,0 +1,25 @@ +"use client"; + +import { Archive } from "lucide-react"; +import { Button } from "../ui/button"; +import { useState } from "react"; +import ArchivedEventsDialog from "../ArchivedEventsDialog"; + +export default function ArchivedEventsButton() { + const [open, setOpen] = useState(false); + + return ( + <> +
setOpen(true)} + > + Archived Events + +
+ + + ); +} diff --git a/components/layout/sidebar.tsx b/components/layout/sidebar.tsx index f385ec0..45a2039 100644 --- a/components/layout/sidebar.tsx +++ b/components/layout/sidebar.tsx @@ -8,6 +8,7 @@ import Image from "next/image"; import Logo from "@/public/pinpoint-logo-color.png"; import { getRecents } from "@/lib/recents/read"; import Link from "next/link"; +import ArchivedEventsButton from "./ArchivedEventsButton"; export default async function Sidebar() { const session = await getServerSession(); @@ -47,6 +48,7 @@ export default async function Sidebar() { ))}
+
Settings diff --git a/lib/api/read/GetArchivedEvents.ts b/lib/api/read/GetArchivedEvents.ts new file mode 100644 index 0000000..c795e20 --- /dev/null +++ b/lib/api/read/GetArchivedEvents.ts @@ -0,0 +1,28 @@ +"use server"; + +import { prisma } from "../db"; + +export async function GetArchivedEvents() { + const events = await prisma.event.findMany({ + where: { + isArchived: true, + }, + select: { + id: true, + name: true, + isGS: true, + isCC: true, + isArchived: true, + locations: { + select: { + id: true, + locationId: true, + }, + }, + }, + orderBy: { + name: 'asc', + }, + }); + return events; +} From 21d72855c05a385bd75e59b5ab4bc3397524fe4d Mon Sep 17 00:00:00 2001 From: Ethan Burdett Date: Tue, 21 Oct 2025 15:12:18 -0400 Subject: [PATCH 5/6] moved duplicate to a shared util function --- components/ArchivedEventsDialog.tsx | 14 +++++- components/EventSelectForm.tsx | 51 ++------------------- lib/api/create/duplicateEvent.ts | 71 +++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 49 deletions(-) create mode 100644 lib/api/create/duplicateEvent.ts diff --git a/components/ArchivedEventsDialog.tsx b/components/ArchivedEventsDialog.tsx index d664476..effaab1 100644 --- a/components/ArchivedEventsDialog.tsx +++ b/components/ArchivedEventsDialog.tsx @@ -6,7 +6,7 @@ import { DialogTitle, } from "@/components/ui/dialog"; import { Label } from "@/components/ui/label"; -import { ArchiveRestore, Trash } from "lucide-react"; +import { ArchiveRestore, CopyPlus, Trash } from "lucide-react"; import { useEffect, useState } from "react"; import { AlertDialog, @@ -21,6 +21,7 @@ import { import { GetArchivedEvents } from "@/lib/api/read/GetArchivedEvents"; import { UpdateArchive } from "@/lib/api/update/Event"; import DeleteEntity from "@/lib/api/delete/DeleteEntity"; +import { duplicateEvent } from "@/lib/api/create/duplicateEvent"; import Link from "next/link"; import { EventWithLocationIds } from "@/types/Event"; @@ -134,6 +135,17 @@ export default function ArchivedEventsDialog({ open, onOpenChange }: ArchivedEve
+