diff --git a/components/EventSelectForm.tsx b/components/EventSelectForm.tsx index 0dbaa4e..3ae526f 100644 --- a/components/EventSelectForm.tsx +++ b/components/EventSelectForm.tsx @@ -10,15 +10,16 @@ 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 LocationAdder from "./LocationCreator"; - import { useSession } from "next-auth/react"; import { useRouter } from "next/navigation"; -import { useState } from "react"; +import { useState, startTransition } from "react"; +import LocationAdder from "./LocationCreator"; +import PinpointLoader from "./PinpointLoader"; import { AlertDialog, @@ -45,23 +46,23 @@ export default function EventSelectForm({ events: Array; }) { const router = useRouter(); + const [eventSelected, setEventSelected] = useState(false); const [eventId, setEventId] = useState(""); - const [selectedEventLocations, setSelectedEventLocations] = useState< - Location[] - >([]); 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); + const [isNavigating, setIsNavigating] = useState(false); const [entityToDelete, setEntityToDelete] = useState<{ entity: Event | Location; type: "event" | "location"; }>(); - - const [insertDialogOpen, setInsertDialogOpen] = useState(false); - const [eventToCreate, setEventToCreate] = useState(""); - - const [dropdownEvents, setDropdownEvents] = useState(events); + const [selectedEventLocations, setSelectedEventLocations] = useState< + Location[] + >([]); function deleteEvent(id: string) { DeleteEntity("event", id); @@ -89,6 +90,7 @@ export default function EventSelectForm({ const handleChange = async (e: SelectChangeEvent) => { setEventSelected(true); + setIsLoading(true); const selectedEventId = e.target.value; setEventId(selectedEventId); @@ -111,6 +113,7 @@ export default function EventSelectForm({ setSelectedEventLocations(updatedLocations ?? []); } + setIsLoading(false); }; const { data: session } = useSession(); @@ -203,43 +206,61 @@ export default function EventSelectForm({ id="eventLocations" className="space-y-2 rounded-md border-gray-200 border-2 p-2 pt-0 transition-all duration-300 max-h-[45vh] overflow-y-auto" > -
{ - setLocationAdderOpen(true); - }} - > -
- - Add Location + {isLoading ? ( +
+
-
- {selectedEventLocations.map((location) => ( -
{ - // Prevent the click event from triggering when clicking the trash button - if ((e.target as HTMLElement).closest(".trash-button")) - return; - router.push(`/event/edit/${eventId}/${location.id}`); - }} - > -
- {location.name}{" "} + ) : ( + <> +
{ + setLocationAdderOpen(true); + }} + > +
+ + Add Location +
- {canEdit && ( - ( +
{ - e.stopPropagation(); - setDeleteDialogOpen(true); - setEntityToDelete({ entity: location, type: "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", + }); + }} + /> + )} +
+ ))} + + )}
)} @@ -327,6 +348,8 @@ export default function EventSelectForm({ setSelectedEventLocations((prev) => [...prev, location]); }} /> + + {isNavigating && } ); } diff --git a/components/PinpointLoader.tsx b/components/PinpointLoader.tsx new file mode 100644 index 0000000..1c1aa3c --- /dev/null +++ b/components/PinpointLoader.tsx @@ -0,0 +1,78 @@ +"use client"; + +import Heading from "@components/Heading"; +import { useEffect, useState } from "react"; + +const PinpointLoader = () => { + const [progress, setProgress] = useState(0); + const [loadingText, setLoadingText] = useState("Initializing maps..."); + + useEffect(() => { + // Loading phrases... + const loadingPhrases = [ + "Initializing maps", + "Loading terrain data", + "Preparing navigation", + "Calculating routes", + "Syncing location data", + "Rendering map tiles", + ]; + + // Fake loading progress + const interval = setInterval(() => { + setProgress((prev) => { + if (prev >= 100) { + clearInterval(interval); + return 100; + } + return prev + 1; + }); + + // Change loading text every now and then + if (progress % 16 === 0) { + const nextPhrase = `${ + loadingPhrases[Math.floor((progress / 16) % loadingPhrases.length)] + }...`; + setLoadingText(nextPhrase); + } + }, 80); + + return () => clearInterval(interval); + }, [progress]); + + return ( +
+
+ {/* Logo */} +
+ +
+ + {/* Loading progress */} +
+
+
{loadingText}
+ {progress}% +
+ +
+
+
+
+
+
+ ); +}; + +export default PinpointLoader;