From 047cdf8c47dc61ead3e21605ac4b4100f51f03b8 Mon Sep 17 00:00:00 2001 From: "Zaine F." Date: Wed, 27 Aug 2025 23:33:46 -0400 Subject: [PATCH 1/3] add initial page animation and formatting --- app/home/event/[eventId]/page.tsx | 45 +++++ app/home/page.tsx | 27 ++- components/EventPageTransitionWrapper.tsx | 23 +++ components/EventSelectForm.tsx | 130 +++------------ components/Heading.tsx | 2 +- components/LocationCreator.tsx | 4 +- components/LocationList.tsx | 194 ++++++++++++++++++++++ package.json | 3 +- 8 files changed, 306 insertions(+), 122 deletions(-) create mode 100644 app/home/event/[eventId]/page.tsx create mode 100644 components/EventPageTransitionWrapper.tsx create mode 100644 components/LocationList.tsx diff --git a/app/home/event/[eventId]/page.tsx b/app/home/event/[eventId]/page.tsx new file mode 100644 index 0000000..7c6cdea --- /dev/null +++ b/app/home/event/[eventId]/page.tsx @@ -0,0 +1,45 @@ +import { GetEvent } from "@/lib/api/read/GetEvent"; +import LocationList from "@/components/LocationList"; +import EventPageTransitionWrapper from "@/components/EventPageTransitionWrapper"; +import Heading from "@/components/Heading"; +import { Box } from "@mui/material"; +import HomepageArrow from "@/components/svg/HomepageArrow"; + +type EventPageParams = { + eventId: string; +}; + +export default async function LocationSelectPage({ + params, +}: { + params: Promise; +}) { + const event = await GetEvent((await params).eventId); + + return ( + <> + + + + + {event && ( + +
+ +
+
+ )} +
+ + + + + ); +} \ No newline at end of file diff --git a/app/home/page.tsx b/app/home/page.tsx index 3541b4b..8ac352f 100644 --- a/app/home/page.tsx +++ b/app/home/page.tsx @@ -14,7 +14,7 @@ import EventSelectForm from "@/components/EventSelectForm"; import Heading from "@/components/Heading"; import HomepageArrow from "@/components/svg/HomepageArrow"; import { prisma } from "@/lib/api/db"; -import { Box, Typography } from "@mui/material"; +import { Box } from "@mui/material"; import { Suspense } from "react"; // Collect all events from doradev database @@ -34,29 +34,28 @@ export default async function EventSelect() { return ( <> - + - - - Select an Event - - + ); } + diff --git a/components/EventPageTransitionWrapper.tsx b/components/EventPageTransitionWrapper.tsx new file mode 100644 index 0000000..8731505 --- /dev/null +++ b/components/EventPageTransitionWrapper.tsx @@ -0,0 +1,23 @@ +"use client"; + +import { useEffect, useState, ReactNode } from "react"; + +export default function EventPageTransitionWrapper({ children }: { children: ReactNode }) { + const [enter, setEnter] = useState(true); + + useEffect(() => { + // allow initial render before triggering transition + const id = requestAnimationFrame(() => setEnter(false)); + return () => cancelAnimationFrame(id); + }, []); + + return ( +
+ {children} +
+ ); +} diff --git a/components/EventSelectForm.tsx b/components/EventSelectForm.tsx index 3ae526f..8161db5 100644 --- a/components/EventSelectForm.tsx +++ b/components/EventSelectForm.tsx @@ -10,14 +10,11 @@ import { Select, SelectChangeEvent, Typography, - CircularProgress, } from "@mui/material"; -import { Label } from "./ui/label"; -import { Plus, Trash } from "lucide-react"; import { Event, Location } from "@prisma/client"; import { useSession } from "next-auth/react"; import { useRouter } from "next/navigation"; -import { useState, startTransition } from "react"; +import { useState } from "react"; import LocationAdder from "./LocationCreator"; import PinpointLoader from "./PinpointLoader"; @@ -34,10 +31,8 @@ import { import CreateEvent from "@/lib/api/create/createEvent"; import DeleteEntity from "@/lib/api/delete/DeleteEntity"; -import { GetEvent } from "@/lib/api/read/GetEvent"; import AddIcon from "@mui/icons-material/Add"; import RemoveIcon from "@mui/icons-material/Remove"; -import { GetAllLocations } from "@/lib/api/read/GetAllLocations"; import { EventWithLocationIds } from "@/types/Event"; export default function EventSelectForm({ @@ -47,14 +42,13 @@ export default function EventSelectForm({ }) { const router = useRouter(); - const [eventSelected, setEventSelected] = useState(false); const [eventId, setEventId] = useState(""); const [locationAdderOpen, setLocationAdderOpen] = useState(false); - const [isLoading, setIsLoading] = useState(false); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [insertDialogOpen, setInsertDialogOpen] = useState(false); const [eventToCreate, setEventToCreate] = useState(""); const [dropdownEvents, setDropdownEvents] = useState(events); + // eslint-disable-next-line @typescript-eslint/no-unused-vars const [isNavigating, setIsNavigating] = useState(false); const [entityToDelete, setEntityToDelete] = useState<{ entity: Event | Location; @@ -63,13 +57,13 @@ export default function EventSelectForm({ const [selectedEventLocations, setSelectedEventLocations] = useState< Location[] >([]); + const [animateOut, setAnimateOut] = useState(false); function deleteEvent(id: string) { DeleteEntity("event", id); setDeleteDialogOpen(false); setDropdownEvents(dropdownEvents.filter((e) => e.id != id)); setEventId(""); - setEventSelected(false); } async function createEvent(name: string) { @@ -88,39 +82,36 @@ export default function EventSelectForm({ ); } - const handleChange = async (e: SelectChangeEvent) => { - setEventSelected(true); - setIsLoading(true); + const handleChange = (e: SelectChangeEvent) => { const selectedEventId = e.target.value; - setEventId(selectedEventId); - - const selectedEvent = dropdownEvents.find( - (event) => event.id === selectedEventId - ); - if (selectedEvent) { - const info = await GetEvent(selectedEventId); - - setSelectedEventLocations([]); // Clear the div by resetting the state - - const allLocations = await GetAllLocations(); - const updatedLocations = allLocations - ?.filter((location) => - info?.locations.some( - (eventLocation) => eventLocation.locationId === location.id - ) - ) - .sort((a, b) => a.name.localeCompare(b.name)); - - setSelectedEventLocations(updatedLocations ?? []); + if (selectedEventId === "newEvent") { + setInsertDialogOpen(true); + return; } - setIsLoading(false); + setEventId(selectedEventId); + setAnimateOut(true); + // navigate after animation finishes + setTimeout(() => router.push(`/home/event/${selectedEventId}`), 450); }; const { data: session } = useSession(); const canEdit = session?.role === "ADMIN" || session?.role === "EDITOR"; return ( -
+
+ + Select an Event + Event - {eventSelected && ( -
- -
- {isLoading ? ( -
- -
- ) : ( - <> -
{ - setLocationAdderOpen(true); - }} - > -
- - Add Location -
-
- {selectedEventLocations.map((location) => ( -
{ - // Prevent the click event from triggering when clicking the trash button - if ((e.target as HTMLElement).closest(".trash-button")) - return; - - // Show loading immediately - setIsNavigating(true); - - // Use startTransition for the navigation to indicate it's a UI update - startTransition(() => { - router.push(`/event/edit/${eventId}/${location.id}`); - }); - }} - > -
- {location.name}{" "} -
- {canEdit && ( - { - e.stopPropagation(); - setDeleteDialogOpen(true); - setEntityToDelete({ - entity: location, - type: "location", - }); - }} - /> - )} -
- ))} - - )} -
-
- )} - diff --git a/components/Heading.tsx b/components/Heading.tsx index 450ca29..983e768 100644 --- a/components/Heading.tsx +++ b/components/Heading.tsx @@ -13,7 +13,7 @@ export default function Heading() { Project Dora setCurrentTab(value)} + onValueChange={(value: SetStateAction) => setCurrentTab(value)} > Add Existing diff --git a/components/LocationList.tsx b/components/LocationList.tsx new file mode 100644 index 0000000..8015079 --- /dev/null +++ b/components/LocationList.tsx @@ -0,0 +1,194 @@ +"use client"; + +import { useEffect, useState, startTransition } from "react"; +import { Location } from "@prisma/client"; +import { CircularProgress, Typography } from "@mui/material"; +import { useRouter } from "next/navigation"; +import { GetAllLocations } from "@/lib/api/read/GetAllLocations"; +import { GetEvent } from "@/lib/api/read/GetEvent"; +import { Label } from "./ui/label"; +import { Plus, Trash } from "lucide-react"; +import { useSession } from "next-auth/react"; +import DeleteEntity from "@/lib/api/delete/DeleteEntity"; +import LocationAdder from "./LocationCreator"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; + +type Props = { + eventId: string; + eventName: string; +}; + +export default function LocationList({ eventId, eventName }: Props) { + const router = useRouter(); + const [locations, setLocations] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [isNavigating, setIsNavigating] = useState(false); + const { data: session } = useSession(); + const canEdit = session?.role === "ADMIN" || session?.role === "EDITOR"; + + // added state + const [locationAdderOpen, setLocationAdderOpen] = useState(false); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [entityToDelete, setEntityToDelete] = useState(null); + + useEffect(() => { + let active = true; + (async () => { + setIsLoading(true); + try { + const [eventData, allLocations] = await Promise.all([ + GetEvent(eventId), + GetAllLocations(), + ]); + const filtered = + allLocations + ?.filter(l => + eventData?.locations.some(evLoc => evLoc.locationId === l.id) + ) + .sort((a, b) => a.name.localeCompare(b.name)) || []; + if (active) setLocations(filtered); + } finally { + if (active) setIsLoading(false); + } + })(); + return () => { + active = false; + }; + }, [eventId]); + + function handleClick(locationId: string) { + setIsNavigating(true); + startTransition(() => { + router.push(`/event/edit/${eventId}/${locationId}`); + }); + } + + function deleteLocation(id: string) { + DeleteEntity("location", id, eventId); + setLocations(prev => prev.filter(l => l.id !== id)); + setDeleteDialogOpen(false); + setEntityToDelete(null); + } + + return ( +
+ +
+ {isLoading ? ( +
+ +
+ ) : locations.length === 0 ? ( +
+ + No locations linked to this event. + +
+ ) : ( + <> + {canEdit && ( +
{ + if (canEdit) setLocationAdderOpen(true); + }} + > +
+ + Add Location +
+
+ )} + {locations.map(location => ( +
{ + if ((e.target as HTMLElement).closest(".trash-button")) return; + handleClick(location.id); + }} + > +
+ {location.name} +
+ {canEdit && ( + { + e.stopPropagation(); + setEntityToDelete(location); + setDeleteDialogOpen(true); + }} + /> + )} +
+ ))} + + )} +
+ {isNavigating && ( +
+ +
+ )} + + {/* LocationAdder modal */} + setLocationAdderOpen(false)} + onLocationChange={(location) => { + setLocations(prev => + [...prev, location].sort((a, b) => a.name.localeCompare(b.name)) + ); + }} + /> + + {/* Delete confirmation dialog */} + + + + Delete + + {`Are you sure you want to delete "${entityToDelete?.name}"?`} + + + + { + setDeleteDialogOpen(false); + setEntityToDelete(null); + }} + > + Cancel + + { + if (entityToDelete) deleteLocation(entityToDelete.id); + }} + > + Confirm + + + + +
+ ); +} diff --git a/package.json b/package.json index 474f89c..9f7ec32 100644 --- a/package.json +++ b/package.json @@ -57,5 +57,6 @@ "postcss": "^8.5.3", "tailwindcss": "^3.4.17", "typescript": "^5.8.2" - } + }, + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } From 7d38ace82e335c4c6425e29c71bfc4c83c176bfe Mon Sep 17 00:00:00 2001 From: "Zaine F." Date: Thu, 28 Aug 2025 21:04:39 -0400 Subject: [PATCH 2/3] Finish animation --- app/home/event/[eventId]/page.tsx | 34 ++++++++------- app/home/page.tsx | 5 ++- components/BackToEventSelectButton.tsx | 26 ++++++++++++ components/EventPageTransitionWrapper.tsx | 51 +++++++++++++++++++---- components/EventSelectForm.tsx | 17 ++++---- components/LocationList.tsx | 13 +++--- 6 files changed, 105 insertions(+), 41 deletions(-) create mode 100644 components/BackToEventSelectButton.tsx diff --git a/app/home/event/[eventId]/page.tsx b/app/home/event/[eventId]/page.tsx index 7c6cdea..cdfb907 100644 --- a/app/home/event/[eventId]/page.tsx +++ b/app/home/event/[eventId]/page.tsx @@ -4,6 +4,7 @@ import EventPageTransitionWrapper from "@/components/EventPageTransitionWrapper" import Heading from "@/components/Heading"; import { Box } from "@mui/material"; import HomepageArrow from "@/components/svg/HomepageArrow"; +import BackToEventSelectButton from "@/components/BackToEventSelectButton"; type EventPageParams = { eventId: string; @@ -18,28 +19,31 @@ export default async function LocationSelectPage({ return ( <> - - - - - {event && ( - -
- -
-
- )} + + + - +
+ + +
+ + )} +
+ - - + }} + > + +
); } \ No newline at end of file diff --git a/app/home/page.tsx b/app/home/page.tsx index 8ac352f..9b9c029 100644 --- a/app/home/page.tsx +++ b/app/home/page.tsx @@ -10,6 +10,7 @@ */ import ErrorToast from "@/components/ErrorToast"; +import EventPageTransitionWrapper from "@/components/EventPageTransitionWrapper"; import EventSelectForm from "@/components/EventSelectForm"; import Heading from "@/components/Heading"; import HomepageArrow from "@/components/svg/HomepageArrow"; @@ -38,7 +39,9 @@ export default async function EventSelect() { - + + + diff --git a/components/BackToEventSelectButton.tsx b/components/BackToEventSelectButton.tsx new file mode 100644 index 0000000..5ec3009 --- /dev/null +++ b/components/BackToEventSelectButton.tsx @@ -0,0 +1,26 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { Button } from "@/components/ui/button"; +import { usePageTransitionExit } from "@/components/EventPageTransitionWrapper"; +import { useState } from "react"; + +export default function BackToEventSelectButton() { + const router = useRouter(); + const exit = usePageTransitionExit(); + const [disabled, setDisabled] = useState(false); + + return ( + + ); +} diff --git a/components/EventPageTransitionWrapper.tsx b/components/EventPageTransitionWrapper.tsx index 8731505..2bae3ad 100644 --- a/components/EventPageTransitionWrapper.tsx +++ b/components/EventPageTransitionWrapper.tsx @@ -1,9 +1,31 @@ "use client"; -import { useEffect, useState, ReactNode } from "react"; +import { useEffect, useState, ReactNode, createContext, useContext } from "react"; -export default function EventPageTransitionWrapper({ children }: { children: ReactNode }) { +type ExitFn = (after?: () => void) => void; +const ExitContext = createContext(null); +export function usePageTransitionExit() { + const ctx = useContext(ExitContext); + if (!ctx) throw new Error("usePageTransitionExit must be used within EventPageTransitionWrapper"); + return ctx; +} + +interface WrapperProps { + children: ReactNode; + entryDirection?: "left" | "right"; + exitDirection?: "left" | "right"; + durationMs?: number; +} + +// Wrapper for components to create a slide animation to one side or the other. +export default function EventPageTransitionWrapper({ + children, + entryDirection = "left", + exitDirection = "right", + durationMs = 500 +}: WrapperProps) { const [enter, setEnter] = useState(true); + const [exiting, setExiting] = useState(false); useEffect(() => { // allow initial render before triggering transition @@ -11,13 +33,24 @@ export default function EventPageTransitionWrapper({ children }: { children: Rea return () => cancelAnimationFrame(id); }, []); + const triggerExit: ExitFn = (after) => { + if (exiting) return; + setExiting(true); + setTimeout(() => after && after(), durationMs); + }; + + const entryClass = entryDirection === "right" ? "translate-x-[180%]" : "-translate-x-full"; + const exitClass = exitDirection === "right" ? "translate-x-[185%]" : "-translate-x-[185%]"; + return ( -
- {children} -
+ +
+ {children} +
+
); } diff --git a/components/EventSelectForm.tsx b/components/EventSelectForm.tsx index 8161db5..9593b99 100644 --- a/components/EventSelectForm.tsx +++ b/components/EventSelectForm.tsx @@ -15,6 +15,8 @@ import { Event, Location } from "@prisma/client"; import { useSession } from "next-auth/react"; import { useRouter } from "next/navigation"; import { useState } from "react"; +import { usePageTransitionExit } from "@/components/EventPageTransitionWrapper"; + import LocationAdder from "./LocationCreator"; import PinpointLoader from "./PinpointLoader"; @@ -57,7 +59,7 @@ export default function EventSelectForm({ const [selectedEventLocations, setSelectedEventLocations] = useState< Location[] >([]); - const [animateOut, setAnimateOut] = useState(false); + // exit handled by wrapper function deleteEvent(id: string) { DeleteEntity("event", id); @@ -89,20 +91,15 @@ export default function EventSelectForm({ return; } setEventId(selectedEventId); - setAnimateOut(true); - // navigate after animation finishes - setTimeout(() => router.push(`/home/event/${selectedEventId}`), 450); + exit(() => router.push(`/home/event/${selectedEventId}`)); }; + const exit = usePageTransitionExit(); const { data: session } = useSession(); const canEdit = session?.role === "ADMIN" || session?.role === "EDITOR"; return ( -
+
{event.name} ) : null; }} - disabled={animateOut} + // disabled during dialog; wrapper exit blocks interaction anyway > {dropdownEvents .map((event) => ( diff --git a/components/LocationList.tsx b/components/LocationList.tsx index 8015079..6e1c519 100644 --- a/components/LocationList.tsx +++ b/components/LocationList.tsx @@ -139,13 +139,14 @@ export default function LocationList({ eventId, eventName }: Props) { ))} )} -
- {isNavigating && ( -
- -
- )} + {isNavigating && ( +
+ +
+ )} +
+ {/* LocationAdder modal */} Date: Sat, 6 Sep 2025 09:30:10 -0400 Subject: [PATCH 3/3] made some small changes --- ...static-web-apps-mango-flower-07a71ba0f.yml | 54 ------------------- components/EventPageTransitionWrapper.tsx | 6 +-- 2 files changed, 3 insertions(+), 57 deletions(-) delete mode 100644 .github/workflows/azure-static-web-apps-mango-flower-07a71ba0f.yml diff --git a/.github/workflows/azure-static-web-apps-mango-flower-07a71ba0f.yml b/.github/workflows/azure-static-web-apps-mango-flower-07a71ba0f.yml deleted file mode 100644 index a29ca4d..0000000 --- a/.github/workflows/azure-static-web-apps-mango-flower-07a71ba0f.yml +++ /dev/null @@ -1,54 +0,0 @@ -name: Azure Static Web Apps CI/CD - -on: - push: - branches: - - main - pull_request: - types: [opened, synchronize, reopened, closed] - branches: - - main - -jobs: - build_and_deploy_job: - if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed') - runs-on: ubuntu-latest - name: Build and Deploy Job - steps: - - uses: actions/checkout@v3 - with: - submodules: true - lfs: false - - name: Build And Deploy - id: builddeploy - uses: Azure/static-web-apps-deploy@v1 - with: - azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_MANGO_FLOWER_07A71BA0F }} - repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments) - action: "upload" - ###### Repository/Build Configurations - These values can be configured to match your app requirements. ###### - # For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig - app_location: "/" # App source code path - api_location: "" # Api source code path - optional - output_location: "" # Built app content directory - optional - app_build_command: "yarn prodBuild" - ###### End of Repository/Build Configurations ###### - env: - DATABASE_URL: ${{ secrets.DATABASE_URL }} - MS_CLIENT_ID: ${{ secrets.MS_CLIENT_ID }} - MS_CLIENT_SECRET: ${{ secrets.MS_CLIENT_SECRET }} - MS_TENANT_ID: ${{ secrets.MS_TENANT_ID }} - NEXT_PUBLIC_ABLY_API_KEY: ${{ secrets.NEXT_PUBLIC_ABLY_API_KEY }} - NEXTAUTH_SECRET: ${{ secrets.NEXTAUTH_SECRET }} - - close_pull_request_job: - if: github.event_name == 'pull_request' && github.event.action == 'closed' - runs-on: ubuntu-latest - name: Close Pull Request Job - steps: - - name: Close Pull Request - id: closepullrequest - uses: Azure/static-web-apps-deploy@v1 - with: - azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_MANGO_FLOWER_07A71BA0F }} - action: "close" diff --git a/components/EventPageTransitionWrapper.tsx b/components/EventPageTransitionWrapper.tsx index 2bae3ad..8ed5d76 100644 --- a/components/EventPageTransitionWrapper.tsx +++ b/components/EventPageTransitionWrapper.tsx @@ -39,13 +39,13 @@ export default function EventPageTransitionWrapper({ setTimeout(() => after && after(), durationMs); }; - const entryClass = entryDirection === "right" ? "translate-x-[180%]" : "-translate-x-full"; - const exitClass = exitDirection === "right" ? "translate-x-[185%]" : "-translate-x-[185%]"; + const entryClass = entryDirection === "right" ? "translate-x-[100vw]" : "-translate-x-[100vw]"; + const exitClass = exitDirection === "right" ? "translate-x-[100vw]" : "-translate-x-[100vw]"; return (