From b9b39dd8bffe85f7b12702df6715de14df5d5ab4 Mon Sep 17 00:00:00 2001 From: dkupper4 Date: Wed, 1 Oct 2025 14:23:39 -0400 Subject: [PATCH 01/28] implemnted reservations shell --- src/app/(default)/reservation/page.tsx | 8 + src/components/ReservationSystem/index.tsx | 329 +++++++++++++++++++++ 2 files changed, 337 insertions(+) create mode 100644 src/app/(default)/reservation/page.tsx create mode 100644 src/components/ReservationSystem/index.tsx diff --git a/src/app/(default)/reservation/page.tsx b/src/app/(default)/reservation/page.tsx new file mode 100644 index 00000000..a0d54d84 --- /dev/null +++ b/src/app/(default)/reservation/page.tsx @@ -0,0 +1,8 @@ +import ReservationSystem from "@/components/ReservationSystem"; +import React from "react"; + +const ReservationPage = () => { + return ; +}; + +export default ReservationPage; \ No newline at end of file diff --git a/src/components/ReservationSystem/index.tsx b/src/components/ReservationSystem/index.tsx new file mode 100644 index 00000000..85e2a6c4 --- /dev/null +++ b/src/components/ReservationSystem/index.tsx @@ -0,0 +1,329 @@ +"use client"; + +import React, { useState } from "react"; +import { ChevronLeft, ChevronRight, Calendar, Accessibility, Monitor, Projector, PenTool } from "lucide-react"; + +interface Room { + id: string; + name: string; + building: string; + floor: string; + hasAccessibility: boolean; + hasProjector: boolean; + hasMonitor: boolean; + hasWhiteboard: boolean; +} + +const ReservationSystem: React.FC = () => { + // Generate time slots from 12:00pm to 11:00pm + const generateTimeSlots = (): string[] => { + const slots: string[] = []; + for (let hour = 12; hour <= 23; hour++) { + const displayHour = hour > 12 ? hour - 12 : hour; + const period = hour >= 12 ? "pm" : "am"; + slots.push(`${displayHour}:00${period}`); + } + return slots; + }; + + const timeSlots = generateTimeSlots(); + const currentDate = new Date().toLocaleDateString("en-US", { + weekday: "long", + year: "numeric", + month: "long", + day: "numeric", + }); + + // Sample room data + const rooms: Room[] = [ + { + id: "ecore-125", + name: "ECORE 125", + building: "ECORE", + floor: "1", + hasAccessibility: true, + hasProjector: true, + hasMonitor: true, + hasWhiteboard: true, + }, + { + id: "ecore-142", + name: "ECORE 142", + building: "ECORE", + floor: "1", + hasAccessibility: true, + hasProjector: false, + hasMonitor: true, + hasWhiteboard: true, + }, + { + id: "ecore-178", + name: "ECORE 178", + building: "ECORE", + floor: "1", + hasAccessibility: false, + hasProjector: true, + hasMonitor: true, + hasWhiteboard: true, + }, + { + id: "ecore-195", + name: "ECORE 195", + building: "ECORE", + floor: "1", + hasAccessibility: true, + hasProjector: true, + hasMonitor: false, + hasWhiteboard: true, + }, + { + id: "ecore-215", + name: "ECORE 215", + building: "ECORE", + floor: "2", + hasAccessibility: true, + hasProjector: true, + hasMonitor: true, + hasWhiteboard: true, + }, + { + id: "ecore-233", + name: "ECORE 233", + building: "ECORE", + floor: "2", + hasAccessibility: true, + hasProjector: true, + hasMonitor: true, + hasWhiteboard: false, + }, + { + id: "ecore-256", + name: "ECORE 256", + building: "ECORE", + floor: "2", + hasAccessibility: false, + hasProjector: true, + hasMonitor: true, + hasWhiteboard: true, + }, + { + id: "ecore-267", + name: "ECORE 267", + building: "ECORE", + floor: "2", + hasAccessibility: false, + hasProjector: false, + hasMonitor: true, + hasWhiteboard: true, + }, + { + id: "ecore-289", + name: "ECORE 289", + building: "ECORE", + floor: "2", + hasAccessibility: true, + hasProjector: true, + hasMonitor: true, + hasWhiteboard: true, + }, + { + id: "ecore-294", + name: "ECORE 294", + building: "ECORE", + floor: "2", + hasAccessibility: true, + hasProjector: true, + hasMonitor: false, + hasWhiteboard: false, + }, + ]; + + // Generate random availability for demo - memoized so it doesn't change on re-render + const [availability] = useState(() => { + const availabilityMap: { [key: string]: boolean } = {}; + rooms.forEach(room => { + timeSlots.forEach(time => { + availabilityMap[`${room.id}-${time}`] = Math.random() > 0.6; + }); + }); + return availabilityMap; + }); + + const [selectedSlot, setSelectedSlot] = useState<{ + roomId: string; + time: string; + } | null>(null); + + const handleSlotClick = (roomId: string, time: string, available: boolean) => { + if (!available) return; // Do nothing if slot is not available + + setSelectedSlot({ roomId, time }); + }; + + return ( +
+
+ {/* Header */} +
+

+ Room Reservations +

+
+ {currentDate} + + + +
+
+ + {/* Reservation Grid */} +
+
+ {/* Fixed Room Column */} +
+ {/* Room Header */} +
+ Space +
+ + {/* Room Names */} + {rooms.map((room, index) => ( +
+
+ + Info + + + {room.name} + +
+
+ {room.hasAccessibility && ( + + )} + {room.hasProjector && ( + + )} + {room.hasMonitor && ( + + )} + {room.hasWhiteboard && ( + + )} +
+
+ ))} +
+ + {/* Scrollable Time Slots */} +
+
+ {/* Time Header Row */} +
+ {timeSlots.map((time) => ( +
+ {time} +
+ ))} +
+ + {/* Time Slot Rows */} + {rooms.map((room, index) => ( +
+ {timeSlots.map((time) => { + const available = availability[`${room.id}-${time}`]; + const isSelected = + selectedSlot?.roomId === room.id && + selectedSlot?.time === time; + + return ( +
handleSlotClick(room.id, time, available)} + title={ + available + ? `Reserve ${room.name} at ${time}` + : "Not available" + } + /> + ); + })} +
+ ))} +
+
+
+
+ + {/* Legend */} +
+
+
+ Available +
+
+
+ Unavailable +
+
+
+ Selected +
+
+ + {/* Selection Info */} + {selectedSlot && ( +
+

+ Selected Reservation +

+
+

+ Room:{" "} + + {rooms.find((r) => r.id === selectedSlot.roomId)?.name} + +

+

+ Time:{" "} + {selectedSlot.time} +

+
+ +
+ )} +
+
+ ); +}; + +export default ReservationSystem; \ No newline at end of file From 022447191db4debd06d31837a36f3e2f24dcdf13 Mon Sep 17 00:00:00 2001 From: Khai Ta Date: Mon, 6 Oct 2025 17:15:12 -0400 Subject: [PATCH 02/28] add photos page shell and navbar link --- src/app/(protected)/photos/page.tsx | 98 +++++++++++++++++++++++++++++ src/components/Navbar/index.tsx | 10 ++- 2 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 src/app/(protected)/photos/page.tsx diff --git a/src/app/(protected)/photos/page.tsx b/src/app/(protected)/photos/page.tsx new file mode 100644 index 00000000..0a09017a --- /dev/null +++ b/src/app/(protected)/photos/page.tsx @@ -0,0 +1,98 @@ +"use client"; + +import React from "react"; +import PhotoGallery from "@/components/PhotoGallery"; +import { Button } from "@/components/ui/button"; +import { useFirebase } from "@/lib/providers/FirebaseProvider"; +import { useRouter } from "next/navigation"; +import Image from "next/image"; + +export default function PhotosPage() { + const { isAuthenticated, isLoading } = useFirebase(); + const router = useRouter(); + + // Placeholder images - reuse event images bundled in public/ + const images = [ + "/event/event_1.jpg", + "/event/event_2.jpg", + "/event/event_3.jpg", + "/event/event_4.jpg", + "/event/event_5.jpg", + "/event/event_6.jpg", + "/event/event_7.jpg", + "/event/event_8.jpg", + "/event/event_9.jpg", + "/event/event_10.jpg", + "/event/event_11.jpg", + "/event/event_12.jpg", + ]; + + const handleUpload = () => { + // For now navigate to profile as a simple action; later replace with upload UI/modal + router.push("/profile"); + }; + + if (isLoading) { + return ( +
+
+
+ ); + } + + if (!isAuthenticated) { + // The (protected) layout wraps this page with AuthGuard and should redirect, + // but add a soft-fallback here + return ( +
+
+

Sign in to view photos

+

+ You must be signed in to access the photo gallery. +

+ +
+
+ ); + } + + return ( +
+
+
+
+

+ Photos +

+

+ A collection of moments from past events. +

+
+ +
+ + HackPSU +
+
+ + +
+
+ ); +} diff --git a/src/components/Navbar/index.tsx b/src/components/Navbar/index.tsx index e1ea340b..8fa5903e 100644 --- a/src/components/Navbar/index.tsx +++ b/src/components/Navbar/index.tsx @@ -171,7 +171,15 @@ const Navbar: React.FC = () => { ? { href: "/profile", text: "Profile" } : { href: "/profile", text: "Register" }; - return [...baseItems, authItem]; + // Only show photos link for authenticated users (keeps homepage clean for guests) + const photosItem = + !isLoading && isAuthenticated + ? { href: "/photos", text: "Photos" } + : null; + + return photosItem + ? [...baseItems, photosItem, authItem] + : [...baseItems, authItem]; }; const navItems = getNavItems(); From 1ed75a0c6f7bcd3a5fd355cff35ea2ba6b77c7ee Mon Sep 17 00:00:00 2001 From: joeboppell Date: Tue, 7 Oct 2025 14:20:42 -0400 Subject: [PATCH 03/28] fix type error --- src/components/ReservationSystem/index.tsx | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/components/ReservationSystem/index.tsx b/src/components/ReservationSystem/index.tsx index 85e2a6c4..24b633ce 100644 --- a/src/components/ReservationSystem/index.tsx +++ b/src/components/ReservationSystem/index.tsx @@ -211,16 +211,24 @@ const ReservationSystem: React.FC = () => {
{room.hasAccessibility && ( - + + + )} {room.hasProjector && ( - + + + )} {room.hasMonitor && ( - + + + )} {room.hasWhiteboard && ( - + + + )}
From 7d4fbd1600c04eaa14b627410359d32f04249070 Mon Sep 17 00:00:00 2001 From: Khai Ta Date: Wed, 8 Oct 2025 17:27:46 -0400 Subject: [PATCH 04/28] simplify gallery + add uploader hooked to API --- src/app/(protected)/photos/page.tsx | 133 +++++++++++----------------- src/components/PhotoUpload.tsx | 106 ++++++++++++++++++++++ src/lib/api/photo/hook.ts | 27 ++++++ src/lib/api/photo/provider.ts | 52 +++++++++++ 4 files changed, 235 insertions(+), 83 deletions(-) create mode 100644 src/components/PhotoUpload.tsx create mode 100644 src/lib/api/photo/hook.ts create mode 100644 src/lib/api/photo/provider.ts diff --git a/src/app/(protected)/photos/page.tsx b/src/app/(protected)/photos/page.tsx index 0a09017a..9dcb863c 100644 --- a/src/app/(protected)/photos/page.tsx +++ b/src/app/(protected)/photos/page.tsx @@ -2,97 +2,64 @@ import React from "react"; import PhotoGallery from "@/components/PhotoGallery"; -import { Button } from "@/components/ui/button"; +import PhotoUpload from "@/components/PhotoUpload"; import { useFirebase } from "@/lib/providers/FirebaseProvider"; -import { useRouter } from "next/navigation"; +import { Button } from "@/components/ui/button"; import Image from "next/image"; +import { usePhotos } from "@/lib/api/photo/hook"; export default function PhotosPage() { - const { isAuthenticated, isLoading } = useFirebase(); - const router = useRouter(); - - // Placeholder images - reuse event images bundled in public/ - const images = [ - "/event/event_1.jpg", - "/event/event_2.jpg", - "/event/event_3.jpg", - "/event/event_4.jpg", - "/event/event_5.jpg", - "/event/event_6.jpg", - "/event/event_7.jpg", - "/event/event_8.jpg", - "/event/event_9.jpg", - "/event/event_10.jpg", - "/event/event_11.jpg", - "/event/event_12.jpg", - ]; - - const handleUpload = () => { - // For now navigate to profile as a simple action; later replace with upload UI/modal - router.push("/profile"); - }; + const { isAuthenticated, isLoading } = useFirebase(); + const { data, isLoading: isPhotosLoading, refetch } = usePhotos(); - if (isLoading) { - return ( -
-
-
- ); - } - - if (!isAuthenticated) { - // The (protected) layout wraps this page with AuthGuard and should redirect, - // but add a soft-fallback here - return ( -
-
-

Sign in to view photos

-

- You must be signed in to access the photo gallery. -

- -
-
- ); - } + if (isLoading) { + return ( +
+
+
+ ); + } + if (!isAuthenticated) { return ( -
-
-
-
-

- Photos -

-

- A collection of moments from past events. -

-
+
+
+

Sign in to view photos

+

You must be signed in to access the photo gallery.

+ +
+
+ ); + } -
- - HackPSU -
-
+ return ( +
+
+
+
+

+ Photos +

+

A collection of moments from past events.

+
- -
+
+ HackPSU +
- ); + +
+ refetch()} /> +
+ + {isPhotosLoading ? ( +
Loading photos...
+ ) : ( + p.url || p.photoUrl || p.name)} /> + )} +
+
+ ); } diff --git a/src/components/PhotoUpload.tsx b/src/components/PhotoUpload.tsx new file mode 100644 index 00000000..8b7864f2 --- /dev/null +++ b/src/components/PhotoUpload.tsx @@ -0,0 +1,106 @@ +"use client"; + +import React, { useRef, useState } from "react"; +import { useUploadPhoto } from "@/lib/api/photo/hook"; +import { Button } from "@/components/ui/button"; + +export default function PhotoUpload({ + onUploaded, +}: { + onUploaded?: () => void; +}) { + const inputRef = useRef(null); + const [preview, setPreview] = useState(null); + const [file, setFile] = useState(null); + const [isDragging, setIsDragging] = useState(false); + + const upload = useUploadPhoto(); + const [isUploading, setIsUploading] = useState(false); + + const onPick = (f?: File) => { + if (!f) return; + setFile(f); + setPreview(URL.createObjectURL(f)); + }; + + const handleFile = (e: React.ChangeEvent) => { + const f = e.target.files?.[0]; + if (f) onPick(f); + }; + + const handleDrop = (e: React.DragEvent) => { + e.preventDefault(); + setIsDragging(false); + const f = e.dataTransfer.files?.[0]; + if (f) onPick(f); + }; + + const startUpload = async () => { + if (!file) return; + setIsUploading(true); + try { + await upload.mutateAsync({ file }); + setFile(null); + setPreview(null); + onUploaded?.(); + } catch (e) { + console.error(e); + alert("Upload failed"); + } finally { + setIsUploading(false); + } + }; + + return ( +
+
e.preventDefault()} + onDragEnter={() => setIsDragging(true)} + onDragLeave={() => setIsDragging(false)} + onDrop={handleDrop} + className={`p-4 border-2 rounded-md ${isDragging ? "border-blue-400 bg-blue-50" : "border-dashed"}`} + > + +
+
+

Drag & drop a photo or

+ +
+ {preview && ( + preview + )} +
+
+ +
+ + +
+
+ ); +} diff --git a/src/lib/api/photo/hook.ts b/src/lib/api/photo/hook.ts new file mode 100644 index 00000000..a6a0af4c --- /dev/null +++ b/src/lib/api/photo/hook.ts @@ -0,0 +1,27 @@ +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { listPhotos, uploadPhoto, deletePhoto } from "./provider"; + +export function usePhotos() { + return useQuery({ + queryKey: ["photos"], + queryFn: () => listPhotos(), + staleTime: 1000 * 60, // 1 minute + }); +} + +export function useUploadPhoto() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: ({ file, fileType }: { file: File; fileType?: string }) => + uploadPhoto(file, fileType), + onSuccess: () => qc.invalidateQueries({ queryKey: ["photos"] }), + }); +} + +export function useDeletePhoto() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (photoId: string) => deletePhoto(photoId), + onSuccess: () => qc.invalidateQueries({ queryKey: ["photos"] }), + }); +} diff --git a/src/lib/api/photo/provider.ts b/src/lib/api/photo/provider.ts new file mode 100644 index 00000000..83c98ecc --- /dev/null +++ b/src/lib/api/photo/provider.ts @@ -0,0 +1,52 @@ +// Backend exposes routes under /photos (see apiv3 PhotoController) +const BASE = process.env.NEXT_PUBLIC_PHOTO_API_BASE || "/photos"; + +export async function listPhotos() { + const res = await fetch(`${BASE}`, { + method: "GET", + credentials: "include", + headers: { + "Content-Type": "application/json", + }, + }); + + if (!res.ok) { + const text = await res.text(); + throw new Error(`Failed to list photos: ${res.status} ${text}`); + } + + return res.json(); +} + +export async function uploadPhoto(file: File, fileType = "default") { + const fd = new FormData(); + fd.append("photo", file); + fd.append("fileType", fileType); + + const res = await fetch(`${BASE}/upload`, { + method: "POST", + body: fd, + credentials: "include", + }); + + if (!res.ok) { + const text = await res.text(); + throw new Error(`Upload failed: ${res.status} ${text}`); + } + + return res.json(); +} + +export async function deletePhoto(photoId: string) { + const res = await fetch(`${BASE}/${encodeURIComponent(photoId)}`, { + method: "DELETE", + credentials: "include", + }); + + if (!res.ok) { + const text = await res.text(); + throw new Error(`Delete failed: ${res.status} ${text}`); + } + + return res.json(); +} From 9e9aaa175c7ef0937dbf5a209e6e6406ad43288e Mon Sep 17 00:00:00 2001 From: dkupper4 Date: Thu, 9 Oct 2025 17:45:15 -0400 Subject: [PATCH 05/28] Res system partially complete --- src/app/(protected)/profile/page.tsx | 14 + .../reservation/page.tsx | 0 src/components/ReservationSystem/index.tsx | 404 ++++++++++++------ src/lib/api/reservation/entity.ts | 28 ++ src/lib/api/reservation/hook.ts | 52 +++ src/lib/api/reservation/index.ts | 3 + src/lib/api/reservation/provider.ts | 31 ++ src/lib/providers/FirebaseProvider.tsx | 32 +- 8 files changed, 421 insertions(+), 143 deletions(-) rename src/app/{(default) => (protected)}/reservation/page.tsx (100%) create mode 100644 src/lib/api/reservation/entity.ts create mode 100644 src/lib/api/reservation/hook.ts create mode 100644 src/lib/api/reservation/index.ts create mode 100644 src/lib/api/reservation/provider.ts diff --git a/src/app/(protected)/profile/page.tsx b/src/app/(protected)/profile/page.tsx index 23ff82f0..33733ffe 100644 --- a/src/app/(protected)/profile/page.tsx +++ b/src/app/(protected)/profile/page.tsx @@ -37,6 +37,7 @@ import { Lock, GraduationCap, } from "lucide-react"; +import { Roofing, Room } from "@mui/icons-material"; export default function Profile() { const { isAuthenticated, user, logout, isLoading } = useFirebase(); @@ -138,6 +139,10 @@ export default function Profile() { router.push("/team"); }; + const handleReserve = () => { + router.push("/reservation") + } + const handleProject = () => { router.push("/project"); }; @@ -379,6 +384,15 @@ export default function Profile() { Manage Team + ) : ( <> diff --git a/src/app/(default)/reservation/page.tsx b/src/app/(protected)/reservation/page.tsx similarity index 100% rename from src/app/(default)/reservation/page.tsx rename to src/app/(protected)/reservation/page.tsx diff --git a/src/components/ReservationSystem/index.tsx b/src/components/ReservationSystem/index.tsx index 24b633ce..0efb3ba9 100644 --- a/src/components/ReservationSystem/index.tsx +++ b/src/components/ReservationSystem/index.tsx @@ -1,13 +1,18 @@ "use client"; -import React, { useState } from "react"; -import { ChevronLeft, ChevronRight, Calendar, Accessibility, Monitor, Projector, PenTool } from "lucide-react"; +import React, { useState, useMemo } from "react"; +import { ChevronLeft, ChevronRight, Calendar, Accessibility, Monitor, Projector, PenTool, Loader2 } from "lucide-react"; +import { useReservations, useLocations, useCreateReservation, useCancelReservation } from "@/lib/api/reservation/hook"; +import { useAllTeams } from "@/lib/api/team/hook"; +import { useFirebase } from "@/lib/providers/FirebaseProvider"; +import { toast } from "sonner"; interface Room { - id: string; + id: number; name: string; building: string; floor: string; + capacity: number; hasAccessibility: boolean; hasProjector: boolean; hasMonitor: boolean; @@ -15,6 +20,49 @@ interface Room { } const ReservationSystem: React.FC = () => { + const { user } = useFirebase(); + const [selectedDate, setSelectedDate] = useState(new Date()); + + // Get user's team first to determine hackathon ID + const { data: teams } = useAllTeams(); + + const userTeam = useMemo(() => { + if (!teams || !user) return null; + return teams.find((team) => + [team.member1, team.member2, team.member3, team.member4, team.member5].includes(user.uid) + ); + }, [teams, user]); + + // Fetch data using hackathon ID from user's team + const { data: reservations, isLoading: reservationsLoading, error: reservationsError } = useReservations(userTeam?.hackathonId || ""); + const { data: locations, isLoading: locationsLoading, error: locationsError } = useLocations(); + const { mutateAsync: createReservation, isPending: isCreating } = useCreateReservation(); + const { mutateAsync: cancelReservation, isPending: isCanceling } = useCancelReservation(userTeam?.hackathonId || ""); + + const [selectedSlot, setSelectedSlot] = useState<{ + roomId: number; + time: string; + } | null>(null); + + // Log errors for debugging + React.useEffect(() => { + console.log("=== Reservation System Debug ==="); + console.log("User:", user?.uid); + console.log("Teams:", teams); + console.log("User Team:", userTeam); + console.log("User Team Hackathon ID:", userTeam?.hackathonId); + console.log("Reservations:", reservations); + console.log("Locations:", locations); + console.log("Reservations Loading:", reservationsLoading); + console.log("Locations Loading:", locationsLoading); + console.log("Reservations Error:", reservationsError); + console.log("Locations Error:", locationsError); + console.log("================================"); + + if (reservationsError) console.error("Reservations error:", reservationsError); + if (locationsError) console.error("Locations error:", locationsError); + }, [user, teams, userTeam, reservations, locations, reservationsLoading, locationsLoading, reservationsError, locationsError]); + // Generate time slots from 12:00pm to 11:00pm const generateTimeSlots = (): string[] => { const slots: string[] = []; @@ -27,173 +75,236 @@ const ReservationSystem: React.FC = () => { }; const timeSlots = generateTimeSlots(); - const currentDate = new Date().toLocaleDateString("en-US", { + + const currentDate = selectedDate.toLocaleDateString("en-US", { weekday: "long", year: "numeric", month: "long", day: "numeric", }); - // Sample room data - const rooms: Room[] = [ - { - id: "ecore-125", - name: "ECORE 125", - building: "ECORE", - floor: "1", - hasAccessibility: true, - hasProjector: true, - hasMonitor: true, - hasWhiteboard: true, - }, - { - id: "ecore-142", - name: "ECORE 142", - building: "ECORE", - floor: "1", - hasAccessibility: true, - hasProjector: false, - hasMonitor: true, - hasWhiteboard: true, - }, - { - id: "ecore-178", - name: "ECORE 178", - building: "ECORE", - floor: "1", - hasAccessibility: false, - hasProjector: true, - hasMonitor: true, - hasWhiteboard: true, - }, - { - id: "ecore-195", - name: "ECORE 195", - building: "ECORE", - floor: "1", - hasAccessibility: true, - hasProjector: true, - hasMonitor: false, - hasWhiteboard: true, - }, - { - id: "ecore-215", - name: "ECORE 215", - building: "ECORE", - floor: "2", - hasAccessibility: true, - hasProjector: true, - hasMonitor: true, - hasWhiteboard: true, - }, - { - id: "ecore-233", - name: "ECORE 233", - building: "ECORE", - floor: "2", - hasAccessibility: true, - hasProjector: true, - hasMonitor: true, - hasWhiteboard: false, - }, - { - id: "ecore-256", - name: "ECORE 256", - building: "ECORE", - floor: "2", - hasAccessibility: false, - hasProjector: true, - hasMonitor: true, - hasWhiteboard: true, - }, - { - id: "ecore-267", - name: "ECORE 267", - building: "ECORE", - floor: "2", - hasAccessibility: false, - hasProjector: false, - hasMonitor: true, - hasWhiteboard: true, - }, - { - id: "ecore-289", - name: "ECORE 289", - building: "ECORE", - floor: "2", + // Convert time string to Unix timestamp + const timeToTimestamp = (timeStr: string, date: Date): number => { + const [time, period] = timeStr.split(/(am|pm)/); + let hour = parseInt(time); + if (period === "pm" && hour !== 12) hour += 12; + if (period === "am" && hour === 12) hour = 0; + + const newDate = new Date(date); + newDate.setHours(hour, 0, 0, 0); + return Math.floor(newDate.getTime() / 1000); + }; + + // Convert timestamp to time string + const timestampToTime = (timestamp: number): string => { + const date = new Date(timestamp * 1000); + const hour = date.getHours(); + const displayHour = hour > 12 ? hour - 12 : hour === 0 ? 12 : hour; + const period = hour >= 12 ? "pm" : "am"; + return `${displayHour}:00${period}`; + }; + + // Transform locations into rooms + const rooms: Room[] = useMemo(() => { + if (!locations) return []; + return locations.map((loc) => ({ + id: loc.id, + name: loc.name, + building: loc.name.split(" ")[0], + floor: loc.name.match(/\d+/)?.[0]?.charAt(0) || "1", + capacity: loc.capacity, hasAccessibility: true, hasProjector: true, hasMonitor: true, hasWhiteboard: true, - }, - { - id: "ecore-294", - name: "ECORE 294", - building: "ECORE", - floor: "2", - hasAccessibility: true, - hasProjector: true, - hasMonitor: false, - hasWhiteboard: false, - }, - ]; - - // Generate random availability for demo - memoized so it doesn't change on re-render - const [availability] = useState(() => { - const availabilityMap: { [key: string]: boolean } = {}; + })); + }, [locations]); + + // Build availability map + const availability = useMemo(() => { + const availabilityMap: { [key: string]: { available: boolean; reservationId?: string } } = {}; + + if (!reservations || !rooms) return availabilityMap; + + // Initialize all slots as available rooms.forEach(room => { timeSlots.forEach(time => { - availabilityMap[`${room.id}-${time}`] = Math.random() > 0.6; + availabilityMap[`${room.id}-${time}`] = { available: true }; }); }); + + // Mark reserved slots + reservations.forEach(reservation => { + const startTime = timestampToTime(reservation.startTime); + const key = `${reservation.locationId}-${startTime}`; + + // Check if this is the user's team reservation + const isUserTeamReservation = reservation.teamId === userTeam?.id; + + availabilityMap[key] = { + available: false, + reservationId: isUserTeamReservation ? reservation.id : undefined, + }; + }); + return availabilityMap; - }); + }, [reservations, rooms, timeSlots, userTeam]); - const [selectedSlot, setSelectedSlot] = useState<{ - roomId: string; - time: string; - } | null>(null); + const handleSlotClick = async (roomId: number, time: string, slotInfo: { available: boolean; reservationId?: string }) => { + if (!userTeam) { + toast.error("You must be part of a team to make reservations"); + return; + } - const handleSlotClick = (roomId: string, time: string, available: boolean) => { - if (!available) return; // Do nothing if slot is not available + // If clicking an existing reservation by this team, cancel it + if (slotInfo.reservationId) { + try { + await cancelReservation(slotInfo.reservationId); + toast.success("Reservation canceled"); + setSelectedSlot(null); + } catch (error) { + console.error("Cancel error:", error); + toast.error("Failed to cancel reservation"); + } + return; + } + + // If slot is not available, do nothing + if (!slotInfo.available) { + toast.error("This time slot is not available"); + return; + } setSelectedSlot({ roomId, time }); }; + const handleConfirmReservation = async () => { + if (!selectedSlot || !userTeam) return; + + try { + const startTime = timeToTimestamp(selectedSlot.time, selectedDate); + const endTime = startTime + 3600; // 1 hour reservation + + await createReservation({ + locationId: selectedSlot.roomId, + teamId: userTeam.id, + startTime, + endTime, + hackathonId: userTeam.hackathonId, + }); + + toast.success("Reservation created successfully!"); + setSelectedSlot(null); + } catch (error: any) { + console.error("Create reservation error:", error); + toast.error(error?.message || "Failed to create reservation"); + } + }; + + const handleDateChange = (direction: "prev" | "next") => { + const newDate = new Date(selectedDate); + newDate.setDate(newDate.getDate() + (direction === "next" ? 1 : -1)); + setSelectedDate(newDate); + setSelectedSlot(null); + }; + + // Show message if user has no team + if (!userTeam && teams && !reservationsLoading) { + return ( +
+
+
No Team Found
+
You must be part of a team to make room reservations.
+
Teams available: {teams.length}
+ +
+
+ ); + } + + if (!teams || reservationsLoading || locationsLoading) { + return ( +
+
+ + Loading reservations... +
+ {!teams &&
Loading teams...
} + {reservationsLoading &&
Loading reservations...
} + {locationsLoading &&
Loading locations...
} +
+
+
+ ); + } + + if (reservationsError || locationsError) { + return ( +
+
+
Error Loading Data
+
+ {reservationsError &&
Reservations Error: {String(reservationsError)}
} + {locationsError &&
Locations Error: {String(locationsError)}
} +
+ +
+
+ ); + } + + if (!locations || locations.length === 0) { + return ( +
+
+
No Locations Available
+
There are no rooms available for reservation at this time.
+
+
+ ); + } + return (
- {/* Header */}

Room Reservations

{currentDate} - - -
- {/* Reservation Grid */}
- {/* Fixed Room Column */}
- {/* Room Header */}
Space
- {/* Room Names */} {rooms.map((room, index) => (
{ ))}
- {/* Scrollable Time Slots */}
- {/* Time Header Row */}
{timeSlots.map((time) => (
{ ))}
- {/* Time Slot Rows */} {rooms.map((room, index) => (
{ }`} > {timeSlots.map((time) => { - const available = availability[`${room.id}-${time}`]; + const slotInfo = availability[`${room.id}-${time}`] || { available: true }; const isSelected = selectedSlot?.roomId === room.id && selectedSlot?.time === time; + const isUserReservation = Boolean(slotInfo.reservationId); return (
handleSlotClick(room.id, time, available)} + onClick={() => handleSlotClick(room.id, time, slotInfo)} title={ - available + isUserReservation + ? `Your reservation at ${room.name} - Click to cancel` + : slotInfo.available ? `Reserve ${room.name} at ${time}` : "Not available" } @@ -290,7 +403,6 @@ const ReservationSystem: React.FC = () => {
- {/* Legend */}
@@ -304,9 +416,12 @@ const ReservationSystem: React.FC = () => {
Selected
+
+
+ Your Reservation (Click to Cancel) +
- {/* Selection Info */} {selectedSlot && (

@@ -323,9 +438,24 @@ const ReservationSystem: React.FC = () => { Time:{" "} {selectedSlot.time}

+

+ Team:{" "} + {userTeam?.name || "No Team"} +

-
)} diff --git a/src/lib/api/reservation/entity.ts b/src/lib/api/reservation/entity.ts new file mode 100644 index 00000000..094616b3 --- /dev/null +++ b/src/lib/api/reservation/entity.ts @@ -0,0 +1,28 @@ +export enum ReservationType { + PARTICIPANT = "participant", + ADMIN = "admin", +} + +export interface ReservationEntity { + id: string; + locationId: number; + teamId: string | null; + reservationType: ReservationType; + startTime: number; + endTime: number; + hackathonId: string; +} + +export interface CreateReservationEntity { + locationId: number; + teamId: string; + startTime: number; + endTime: number; + hackathonId: string; +} + +export interface LocationEntity { + id: number; + name: string; + capacity: number; +} \ No newline at end of file diff --git a/src/lib/api/reservation/hook.ts b/src/lib/api/reservation/hook.ts new file mode 100644 index 00000000..fb47bbdf --- /dev/null +++ b/src/lib/api/reservation/hook.ts @@ -0,0 +1,52 @@ +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { + getReservations, + createReservation, + cancelReservation, + getLocations, +} from "./provider"; +import { ReservationEntity, CreateReservationEntity } from "./entity"; + +export const reservationQueryKeys = { + all: (hackathonId: string) => ["reservations", hackathonId] as const, + locations: ["locations"] as const, +}; + +export function useReservations(hackathonId: string) { + return useQuery({ + queryKey: reservationQueryKeys.all(hackathonId), + queryFn: () => getReservations(hackathonId), + enabled: Boolean(hackathonId), + }); +} + +export function useLocations() { + return useQuery({ + queryKey: reservationQueryKeys.locations, + queryFn: getLocations, + }); +} + +export function useCreateReservation() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (data: CreateReservationEntity) => createReservation(data), + onSuccess: (_, variables) => { + qc.invalidateQueries({ + queryKey: reservationQueryKeys.all(variables.hackathonId), + }); + }, + }); +} + +export function useCancelReservation(hackathonId: string) { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (reservationId: string) => cancelReservation(reservationId), + onSuccess: () => { + qc.invalidateQueries({ + queryKey: reservationQueryKeys.all(hackathonId), + }); + }, + }); +} \ No newline at end of file diff --git a/src/lib/api/reservation/index.ts b/src/lib/api/reservation/index.ts new file mode 100644 index 00000000..61f2c36f --- /dev/null +++ b/src/lib/api/reservation/index.ts @@ -0,0 +1,3 @@ +export * from "./entity"; +export * from "./provider"; +export * from "./hook"; diff --git a/src/lib/api/reservation/provider.ts b/src/lib/api/reservation/provider.ts new file mode 100644 index 00000000..86c32e28 --- /dev/null +++ b/src/lib/api/reservation/provider.ts @@ -0,0 +1,31 @@ +import { apiFetch } from "@/lib/api/apiClient"; +import { ReservationEntity, CreateReservationEntity, LocationEntity } from "./entity"; + +export async function getReservations( + hackathonId: string +): Promise { + return apiFetch( + `/reservations?hackathonId=${hackathonId}`, + { method: "GET" } + ); +} + +export async function createReservation( + data: CreateReservationEntity +): Promise { + return apiFetch("/reservations", { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }); +} + +export async function cancelReservation(reservationId: string): Promise { + return apiFetch(`/reservations/${reservationId}`, { + method: "DELETE", + }); +} + +export async function getLocations(): Promise { + return apiFetch("/locations", { method: "GET" }); +} \ No newline at end of file diff --git a/src/lib/providers/FirebaseProvider.tsx b/src/lib/providers/FirebaseProvider.tsx index a126247e..93673f1e 100644 --- a/src/lib/providers/FirebaseProvider.tsx +++ b/src/lib/providers/FirebaseProvider.tsx @@ -14,6 +14,12 @@ import { type Auth, type User, signOut } from "firebase/auth"; import { auth } from "@/lib/config/firebase"; import posthog from "posthog-js"; +// Helper function to get auth service URL from environment +function getAuthServiceURL(): string { + // Use environment variable if set, otherwise default to production + return process.env.NEXT_PUBLIC_AUTH_SERVICE_URL || "https://auth.hackpsu.org"; +} + type FirebaseContextType = { auth: Auth; isLoading: boolean; @@ -37,7 +43,7 @@ export const FirebaseProvider: FC = ({ children }) => { const [hasInitialized, setHasInitialized] = useState(false); const [isLoggingOut, setIsLoggingOut] = useState(false); - // Verify session with the auth server + // Verify session with the auth server (environment-aware) const verifySession = useCallback(async () => { // Don't verify session if we're in the middle of logging out if (isLoggingOut) { @@ -45,9 +51,11 @@ export const FirebaseProvider: FC = ({ children }) => { return; } - console.log("Verifying session..."); + const authServiceURL = getAuthServiceURL(); + console.log("Verifying session with:", authServiceURL); + try { - const response = await fetch("https://auth.hackpsu.org/api/sessionUser", { + const response = await fetch(`${authServiceURL}/api/sessionUser`, { method: "GET", credentials: "include", headers: { @@ -57,6 +65,14 @@ export const FirebaseProvider: FC = ({ children }) => { console.log("Session verification response:", response.status); + if (response.status === 401) { + // No session or session invalid - redirect to login + console.log("No valid session, redirecting to login"); + const currentUrl = encodeURIComponent(window.location.href); + window.location.href = `${authServiceURL}/login?returnTo=${currentUrl}`; + return; + } + if (!response.ok) { throw new Error(`Session verification failed: ${response.status}`); } @@ -110,6 +126,8 @@ export const FirebaseProvider: FC = ({ children }) => { console.log("Initial session check successful"); } catch (err) { console.log("No valid session found or timeout occurred:", err); + // Don't redirect here on initial load - let the user stay on the page + // Only redirect when explicitly calling verifySession } finally { setIsLoading(false); setHasInitialized(true); @@ -119,20 +137,22 @@ export const FirebaseProvider: FC = ({ children }) => { checkSession(); }, [verifySession, hasInitialized, isLoggingOut]); - // Enhanced logout function + // Enhanced logout function (environment-aware) const logout = useCallback(async () => { console.log("Starting logout process..."); setIsLoggingOut(true); setError(undefined); setIsLoading(true); + const authServiceURL = getAuthServiceURL(); + try { // Clear PostHog identity posthog.reset(); // Clear the session on the auth server first console.log("Clearing auth server session..."); - await fetch("https://auth.hackpsu.org/api/sessionLogout", { + await fetch(`${authServiceURL}/api/sessionLogout`, { method: "POST", credentials: "include", headers: { @@ -184,4 +204,4 @@ export const useFirebase = () => { const ctx = useContext(FirebaseContext); if (!ctx) throw new Error("useFirebase must be used within FirebaseProvider"); return ctx; -}; +}; \ No newline at end of file From c3b5869e1d1576a5a0ec673f2586888a2d8d1421 Mon Sep 17 00:00:00 2001 From: joeboppell Date: Tue, 14 Oct 2025 17:32:28 -0400 Subject: [PATCH 06/28] fixed photo provider and aesthetic changes --- src/app/(protected)/photos/page.tsx | 106 ++++++++++++++++------------ src/components/PhotoGallery.tsx | 38 ++++++---- src/components/PhotoUpload.tsx | 7 +- src/lib/api/photo/provider.ts | 37 ++-------- 4 files changed, 93 insertions(+), 95 deletions(-) diff --git a/src/app/(protected)/photos/page.tsx b/src/app/(protected)/photos/page.tsx index 9dcb863c..dfec00f5 100644 --- a/src/app/(protected)/photos/page.tsx +++ b/src/app/(protected)/photos/page.tsx @@ -9,57 +9,69 @@ import Image from "next/image"; import { usePhotos } from "@/lib/api/photo/hook"; export default function PhotosPage() { - const { isAuthenticated, isLoading } = useFirebase(); - const { data, isLoading: isPhotosLoading, refetch } = usePhotos(); + const { isAuthenticated, isLoading } = useFirebase(); + const { data, isLoading: isPhotosLoading, refetch } = usePhotos(); - if (isLoading) { - return ( -
-
-
- ); - } + if (isLoading) { + return ( +
+
+
+ ); + } - if (!isAuthenticated) { - return ( -
-
-

Sign in to view photos

-

You must be signed in to access the photo gallery.

- -
-
- ); - } + if (!isAuthenticated) { + return ( +
+
+

Sign in to view photos

+

+ You must be signed in to access the photo gallery. +

+ +
+
+ ); + } - return ( -
-
-
-
-

- Photos -

-

A collection of moments from past events.

-
+ return ( +
+
+
+
+

+ Photos +

+

+ A collection of moments from past events. +

+
+
-
- HackPSU -
-
+
+ refetch()} /> +
-
- refetch()} /> + {isPhotosLoading ? ( +
Loading photos...
+ ) : ( + p.url || p.photoUrl || p.name)} + variant="photos" + /> + )} +
- - {isPhotosLoading ? ( -
Loading photos...
- ) : ( - p.url || p.photoUrl || p.name)} /> - )} -
-
- ); + ); } diff --git a/src/components/PhotoGallery.tsx b/src/components/PhotoGallery.tsx index 4f3f7a93..30b5e042 100644 --- a/src/components/PhotoGallery.tsx +++ b/src/components/PhotoGallery.tsx @@ -10,20 +10,31 @@ import Image from "next/image"; interface CarouselProps { /** Array of image URLs (e.g. ['/images/carousel/1.jpg', '/images/carousel/2.jpg', ...]) */ images: string[]; + /** Variant for different styling - 'default' for home page, 'photos' for photos page */ + variant?: 'default' | 'photos'; } -const PhotoGallery: React.FC = ({ images }) => ( -
- {/* Header */} -
-

- Gallery -

-
-
+const PhotoGallery: React.FC = ({ images, variant = 'default' }) => { + const isPhotosPage = variant === 'photos'; + + return ( +
+ {/* Header */} +
+

+ Gallery +

+
+
= ({ images }) => ( } `}
-); + ); +}; export default PhotoGallery; diff --git a/src/components/PhotoUpload.tsx b/src/components/PhotoUpload.tsx index 8b7864f2..db1b2698 100644 --- a/src/components/PhotoUpload.tsx +++ b/src/components/PhotoUpload.tsx @@ -58,7 +58,7 @@ export default function PhotoUpload({ onDragEnter={() => setIsDragging(true)} onDragLeave={() => setIsDragging(false)} onDrop={handleDrop} - className={`p-4 border-2 rounded-md ${isDragging ? "border-blue-400 bg-blue-50" : "border-dashed"}`} + className={`p-4 border-2 border-[#215172] rounded-md ${isDragging ? "border-[#215172]-400 bg-blue-50" : "border-dashed"}`} > inputRef.current?.click()} > - select a file + Select a file
{preview && ( @@ -88,7 +88,7 @@ export default function PhotoUpload({
- diff --git a/src/lib/api/photo/provider.ts b/src/lib/api/photo/provider.ts index 83c98ecc..5e7b27d2 100644 --- a/src/lib/api/photo/provider.ts +++ b/src/lib/api/photo/provider.ts @@ -1,21 +1,10 @@ -// Backend exposes routes under /photos (see apiv3 PhotoController) -const BASE = process.env.NEXT_PUBLIC_PHOTO_API_BASE || "/photos"; +import { apiFetch } from "../apiClient"; +// Backend exposes routes under /photos (see apiv3 PhotoController) export async function listPhotos() { - const res = await fetch(`${BASE}`, { + return apiFetch("/photos", { method: "GET", - credentials: "include", - headers: { - "Content-Type": "application/json", - }, }); - - if (!res.ok) { - const text = await res.text(); - throw new Error(`Failed to list photos: ${res.status} ${text}`); - } - - return res.json(); } export async function uploadPhoto(file: File, fileType = "default") { @@ -23,30 +12,14 @@ export async function uploadPhoto(file: File, fileType = "default") { fd.append("photo", file); fd.append("fileType", fileType); - const res = await fetch(`${BASE}/upload`, { + return apiFetch("/photos/upload", { method: "POST", body: fd, - credentials: "include", }); - - if (!res.ok) { - const text = await res.text(); - throw new Error(`Upload failed: ${res.status} ${text}`); - } - - return res.json(); } export async function deletePhoto(photoId: string) { - const res = await fetch(`${BASE}/${encodeURIComponent(photoId)}`, { + return apiFetch(`/photos/${encodeURIComponent(photoId)}`, { method: "DELETE", - credentials: "include", }); - - if (!res.ok) { - const text = await res.text(); - throw new Error(`Delete failed: ${res.status} ${text}`); - } - - return res.json(); } From 899fa2f7b214d06632817b79ed111528a9b283af Mon Sep 17 00:00:00 2001 From: joeboppell Date: Tue, 14 Oct 2025 17:38:30 -0400 Subject: [PATCH 07/28] build fix --- src/app/(protected)/photos/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/(protected)/photos/page.tsx b/src/app/(protected)/photos/page.tsx index dfec00f5..d7ef6f8f 100644 --- a/src/app/(protected)/photos/page.tsx +++ b/src/app/(protected)/photos/page.tsx @@ -67,7 +67,7 @@ export default function PhotosPage() {
Loading photos...
) : ( p.url || p.photoUrl || p.name)} + images={Array.isArray(data) ? data.map((p: any) => p.url || p.photoUrl || p.name) : []} variant="photos" /> )} From f7d7ee5681d7b37ef0394a7a3e6cecd509f16eef Mon Sep 17 00:00:00 2001 From: joeboppell Date: Wed, 15 Oct 2025 14:32:19 -0400 Subject: [PATCH 08/28] fix file type --- src/lib/api/photo/provider.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib/api/photo/provider.ts b/src/lib/api/photo/provider.ts index 5e7b27d2..0c91b846 100644 --- a/src/lib/api/photo/provider.ts +++ b/src/lib/api/photo/provider.ts @@ -10,6 +10,7 @@ export async function listPhotos() { export async function uploadPhoto(file: File, fileType = "default") { const fd = new FormData(); fd.append("photo", file); + fileType = file.type.split('/')[1] || 'default'; fd.append("fileType", fileType); return apiFetch("/photos/upload", { From 2d1b7643635a3e7b7f8e5d2570d621d4c96fe27e Mon Sep 17 00:00:00 2001 From: dkupper4 Date: Wed, 15 Oct 2025 15:04:56 -0400 Subject: [PATCH 09/28] Reservations work, cancel does not, UI is right --- src/components/ReservationSystem/index.tsx | 1130 ++++++++++++-------- src/lib/api/reservation/entity.ts | 36 +- src/lib/api/reservation/hook.ts | 68 +- src/lib/api/reservation/provider.ts | 38 +- 4 files changed, 749 insertions(+), 523 deletions(-) diff --git a/src/components/ReservationSystem/index.tsx b/src/components/ReservationSystem/index.tsx index 0efb3ba9..6405a08e 100644 --- a/src/components/ReservationSystem/index.tsx +++ b/src/components/ReservationSystem/index.tsx @@ -1,467 +1,689 @@ "use client"; import React, { useState, useMemo } from "react"; -import { ChevronLeft, ChevronRight, Calendar, Accessibility, Monitor, Projector, PenTool, Loader2 } from "lucide-react"; -import { useReservations, useLocations, useCreateReservation, useCancelReservation } from "@/lib/api/reservation/hook"; +import { ChevronLeft, ChevronRight, Loader2 } from "lucide-react"; +import { + useReservations, + useLocations, + useCreateReservation, + useCancelReservation, +} from "@/lib/api/reservation/hook"; import { useAllTeams } from "@/lib/api/team/hook"; +import { useActiveHackathonForStatic } from "@/lib/api/hackathon/hook"; import { useFirebase } from "@/lib/providers/FirebaseProvider"; import { toast } from "sonner"; interface Room { - id: number; - name: string; - building: string; - floor: string; - capacity: number; - hasAccessibility: boolean; - hasProjector: boolean; - hasMonitor: boolean; - hasWhiteboard: boolean; + id: number; + name: string; + building: string; + floor: string; + capacity: number; } const ReservationSystem: React.FC = () => { - const { user } = useFirebase(); - const [selectedDate, setSelectedDate] = useState(new Date()); - - // Get user's team first to determine hackathon ID - const { data: teams } = useAllTeams(); - - const userTeam = useMemo(() => { - if (!teams || !user) return null; - return teams.find((team) => - [team.member1, team.member2, team.member3, team.member4, team.member5].includes(user.uid) - ); - }, [teams, user]); - - // Fetch data using hackathon ID from user's team - const { data: reservations, isLoading: reservationsLoading, error: reservationsError } = useReservations(userTeam?.hackathonId || ""); - const { data: locations, isLoading: locationsLoading, error: locationsError } = useLocations(); - const { mutateAsync: createReservation, isPending: isCreating } = useCreateReservation(); - const { mutateAsync: cancelReservation, isPending: isCanceling } = useCancelReservation(userTeam?.hackathonId || ""); - - const [selectedSlot, setSelectedSlot] = useState<{ - roomId: number; - time: string; - } | null>(null); - - // Log errors for debugging - React.useEffect(() => { - console.log("=== Reservation System Debug ==="); - console.log("User:", user?.uid); - console.log("Teams:", teams); - console.log("User Team:", userTeam); - console.log("User Team Hackathon ID:", userTeam?.hackathonId); - console.log("Reservations:", reservations); - console.log("Locations:", locations); - console.log("Reservations Loading:", reservationsLoading); - console.log("Locations Loading:", locationsLoading); - console.log("Reservations Error:", reservationsError); - console.log("Locations Error:", locationsError); - console.log("================================"); - - if (reservationsError) console.error("Reservations error:", reservationsError); - if (locationsError) console.error("Locations error:", locationsError); - }, [user, teams, userTeam, reservations, locations, reservationsLoading, locationsLoading, reservationsError, locationsError]); - - // Generate time slots from 12:00pm to 11:00pm - const generateTimeSlots = (): string[] => { - const slots: string[] = []; - for (let hour = 12; hour <= 23; hour++) { - const displayHour = hour > 12 ? hour - 12 : hour; - const period = hour >= 12 ? "pm" : "am"; - slots.push(`${displayHour}:00${period}`); - } - return slots; - }; - - const timeSlots = generateTimeSlots(); - - const currentDate = selectedDate.toLocaleDateString("en-US", { - weekday: "long", - year: "numeric", - month: "long", - day: "numeric", - }); - - // Convert time string to Unix timestamp - const timeToTimestamp = (timeStr: string, date: Date): number => { - const [time, period] = timeStr.split(/(am|pm)/); - let hour = parseInt(time); - if (period === "pm" && hour !== 12) hour += 12; - if (period === "am" && hour === 12) hour = 0; - - const newDate = new Date(date); - newDate.setHours(hour, 0, 0, 0); - return Math.floor(newDate.getTime() / 1000); - }; - - // Convert timestamp to time string - const timestampToTime = (timestamp: number): string => { - const date = new Date(timestamp * 1000); - const hour = date.getHours(); - const displayHour = hour > 12 ? hour - 12 : hour === 0 ? 12 : hour; - const period = hour >= 12 ? "pm" : "am"; - return `${displayHour}:00${period}`; - }; - - // Transform locations into rooms - const rooms: Room[] = useMemo(() => { - if (!locations) return []; - return locations.map((loc) => ({ - id: loc.id, - name: loc.name, - building: loc.name.split(" ")[0], - floor: loc.name.match(/\d+/)?.[0]?.charAt(0) || "1", - capacity: loc.capacity, - hasAccessibility: true, - hasProjector: true, - hasMonitor: true, - hasWhiteboard: true, - })); - }, [locations]); - - // Build availability map - const availability = useMemo(() => { - const availabilityMap: { [key: string]: { available: boolean; reservationId?: string } } = {}; - - if (!reservations || !rooms) return availabilityMap; - - // Initialize all slots as available - rooms.forEach(room => { - timeSlots.forEach(time => { - availabilityMap[`${room.id}-${time}`] = { available: true }; - }); - }); - - // Mark reserved slots - reservations.forEach(reservation => { - const startTime = timestampToTime(reservation.startTime); - const key = `${reservation.locationId}-${startTime}`; - - // Check if this is the user's team reservation - const isUserTeamReservation = reservation.teamId === userTeam?.id; - - availabilityMap[key] = { - available: false, - reservationId: isUserTeamReservation ? reservation.id : undefined, - }; - }); - - return availabilityMap; - }, [reservations, rooms, timeSlots, userTeam]); - - const handleSlotClick = async (roomId: number, time: string, slotInfo: { available: boolean; reservationId?: string }) => { - if (!userTeam) { - toast.error("You must be part of a team to make reservations"); - return; - } - - // If clicking an existing reservation by this team, cancel it - if (slotInfo.reservationId) { - try { - await cancelReservation(slotInfo.reservationId); - toast.success("Reservation canceled"); - setSelectedSlot(null); - } catch (error) { - console.error("Cancel error:", error); - toast.error("Failed to cancel reservation"); - } - return; - } - - // If slot is not available, do nothing - if (!slotInfo.available) { - toast.error("This time slot is not available"); - return; - } - - setSelectedSlot({ roomId, time }); - }; - - const handleConfirmReservation = async () => { - if (!selectedSlot || !userTeam) return; - - try { - const startTime = timeToTimestamp(selectedSlot.time, selectedDate); - const endTime = startTime + 3600; // 1 hour reservation - - await createReservation({ - locationId: selectedSlot.roomId, - teamId: userTeam.id, - startTime, - endTime, - hackathonId: userTeam.hackathonId, - }); - - toast.success("Reservation created successfully!"); - setSelectedSlot(null); - } catch (error: any) { - console.error("Create reservation error:", error); - toast.error(error?.message || "Failed to create reservation"); - } - }; - - const handleDateChange = (direction: "prev" | "next") => { - const newDate = new Date(selectedDate); - newDate.setDate(newDate.getDate() + (direction === "next" ? 1 : -1)); - setSelectedDate(newDate); - setSelectedSlot(null); - }; - - // Show message if user has no team - if (!userTeam && teams && !reservationsLoading) { - return ( -
-
-
No Team Found
-
You must be part of a team to make room reservations.
-
Teams available: {teams.length}
- -
-
- ); - } - - if (!teams || reservationsLoading || locationsLoading) { - return ( -
-
- - Loading reservations... -
- {!teams &&
Loading teams...
} - {reservationsLoading &&
Loading reservations...
} - {locationsLoading &&
Loading locations...
} -
-
-
- ); - } - - if (reservationsError || locationsError) { - return ( -
-
-
Error Loading Data
-
- {reservationsError &&
Reservations Error: {String(reservationsError)}
} - {locationsError &&
Locations Error: {String(locationsError)}
} -
- -
-
- ); - } - - if (!locations || locations.length === 0) { - return ( -
-
-
No Locations Available
-
There are no rooms available for reservation at this time.
-
-
- ); - } - - return ( -
-
-
-

- Room Reservations -

-
- {currentDate} - - -
-
- -
-
-
-
- Space -
- - {rooms.map((room, index) => ( -
-
- - Info - - - {room.name} - -
-
- {room.hasAccessibility && ( - - - - )} - {room.hasProjector && ( - - - - )} - {room.hasMonitor && ( - - - - )} - {room.hasWhiteboard && ( - - - - )} -
-
- ))} -
- -
-
-
- {timeSlots.map((time) => ( -
- {time} -
- ))} -
- - {rooms.map((room, index) => ( -
- {timeSlots.map((time) => { - const slotInfo = availability[`${room.id}-${time}`] || { available: true }; - const isSelected = - selectedSlot?.roomId === room.id && - selectedSlot?.time === time; - const isUserReservation = Boolean(slotInfo.reservationId); - - return ( -
handleSlotClick(room.id, time, slotInfo)} - title={ - isUserReservation - ? `Your reservation at ${room.name} - Click to cancel` - : slotInfo.available - ? `Reserve ${room.name} at ${time}` - : "Not available" - } - /> - ); - })} -
- ))} -
-
-
-
- -
-
-
- Available -
-
-
- Unavailable -
-
-
- Selected -
-
-
- Your Reservation (Click to Cancel) -
-
- - {selectedSlot && ( -
-

- Selected Reservation -

-
-

- Room:{" "} - - {rooms.find((r) => r.id === selectedSlot.roomId)?.name} - -

-

- Time:{" "} - {selectedSlot.time} -

-

- Team:{" "} - {userTeam?.name || "No Team"} -

-
- -
- )} -
-
- ); + const { user } = useFirebase(); + + // Get user's team first to determine hackathon ID + const { data: teams } = useAllTeams(); + + const userTeam = useMemo(() => { + if (!teams || !user) return null; + return teams.find((team) => + [ + team.member1, + team.member2, + team.member3, + team.member4, + team.member5, + ].includes(user.uid) + ); + }, [teams, user]); + + // Fetch hackathon data to get timeframe - using the active hackathon endpoint + const { data: activeHackathon, isLoading: hackathonLoading } = + useActiveHackathonForStatic(); + + // Extract the hackathon data from the active hackathon response + const hackathon = useMemo(() => { + if (!activeHackathon) return null; + return { + id: activeHackathon.id, + name: activeHackathon.name, + startTime: activeHackathon.startTime, + endTime: activeHackathon.endTime, + active: activeHackathon.active, + }; + }, [activeHackathon]); + + // Initialize selected date based on hackathon start time (normalize to ms) + const initialDate = useMemo(() => { + if (hackathon?.startTime) { + const tsMs = + hackathon.startTime > 9999999999 + ? hackathon.startTime + : hackathon.startTime * 1000; + return new Date(tsMs); + } + return new Date(); + }, [hackathon]); + + const [selectedDate, setSelectedDate] = useState(initialDate); + + // Update selected date when hackathon data loads + React.useEffect(() => { + if ( + hackathon?.startTime && + selectedDate.getTime() !== initialDate.getTime() + ) { + setSelectedDate(initialDate); + } + }, [hackathon, initialDate]); + + // Fetch data using hackathon ID from user's team + const { + data: reservations, + isLoading: reservationsLoading, + error: reservationsError, + } = useReservations(userTeam?.hackathonId || ""); + const { + data: locations, + isLoading: locationsLoading, + error: locationsError, + } = useLocations(); + const { mutateAsync: createReservation, isPending: isCreating } = + useCreateReservation(); + const { mutateAsync: cancelReservation, isPending: isCanceling } = + useCancelReservation(userTeam?.hackathonId || ""); + + const [selectedSlot, setSelectedSlot] = useState<{ + roomId: number; + time: string; + } | null>(null); + + // Get valid date range for the hackathon (normalize to ms) + const dateRange = useMemo(() => { + if (!hackathon) + return { startDate: null, endDate: null, dates: [] as Date[] }; + + const startMs = + hackathon.startTime > 9999999999 + ? hackathon.startTime + : hackathon.startTime * 1000; + const endMs = + hackathon.endTime > 9999999999 + ? hackathon.endTime + : hackathon.endTime * 1000; + + const startDate = new Date(startMs); + const endDate = new Date(endMs); + + const dates: Date[] = []; + const currentDate = new Date(startDate); + + while (currentDate <= endDate) { + dates.push(new Date(currentDate)); + currentDate.setDate(currentDate.getDate() + 1); + } + + return { startDate, endDate, dates }; + }, [hackathon]); + + // Debug + React.useEffect(() => { + console.log("=== Reservation System Debug ==="); + console.log("User:", user?.uid); + console.log("User Team:", userTeam); + console.log("Hackathon ID:", userTeam?.hackathonId); + console.log("Hackathon Data:", hackathon); + if (hackathon) { + const startMs = + hackathon.startTime > 9999999999 + ? hackathon.startTime + : hackathon.startTime * 1000; + const endMs = + hackathon.endTime > 9999999999 + ? hackathon.endTime + : hackathon.endTime * 1000; + console.log("Hackathon Start:", new Date(startMs)); + console.log("Hackathon End:", new Date(endMs)); + } + console.log("Selected Date:", selectedDate); + console.log("Date Range:", dateRange); + console.log("Reservations:", reservations); + console.log("Locations:", locations); + console.log("================================"); + }, [ + user, + userTeam, + hackathon, + selectedDate, + dateRange, + reservations, + locations, + ]); + + // Generate time slots based on selected date and hackathon timeframe + const generateTimeSlots = (): string[] => { + if (!hackathon) return []; + + const slots: string[] = []; + const selectedDateStart = new Date(selectedDate); + selectedDateStart.setHours(0, 0, 0, 0); + + const startMs = + hackathon.startTime > 9999999999 + ? hackathon.startTime + : hackathon.startTime * 1000; + const endMs = + hackathon.endTime > 9999999999 + ? hackathon.endTime + : hackathon.endTime * 1000; + + const hackathonStart = new Date(startMs); + const hackathonEnd = new Date(endMs); + + let startHour = 0; + let endHour = 24; // ← include 23:00 (11pm) by default + + // First day: start at hackathon start hour + if (selectedDateStart.toDateString() === hackathonStart.toDateString()) { + startHour = hackathonStart.getHours(); + } + + // Last day: end at hackathon end hour (exclusive) + if (selectedDateStart.toDateString() === hackathonEnd.toDateString()) { + endHour = hackathonEnd.getHours(); + if (endHour === 0) endHour = 24; // if it ends at midnight, show up to 11pm + } + + for (let hour = startHour; hour < endHour; hour++) { + const displayHour = hour === 0 ? 12 : hour > 12 ? hour - 12 : hour; + const period = hour >= 12 ? "pm" : "am"; + slots.push(`${displayHour}:00${period}`); + } + + return slots; + }; + + const timeSlots = generateTimeSlots(); + + const currentDate = selectedDate.toLocaleDateString("en-US", { + weekday: "long", + year: "numeric", + month: "long", + day: "numeric", + }); + + /** + * Helpers: milliseconds everywhere + */ + + // Convert "h:00am/pm" + date -> timestamp in **ms** + const timeToTimestamp = (timeStr: string, date: Date): number => { + const [time, period] = timeStr.split(/(am|pm)/); + let hour = parseInt(time); + if (period === "pm" && hour !== 12) hour += 12; + if (period === "am" && hour === 12) hour = 0; + + const d = new Date(date); + d.setHours(hour, 0, 0, 0); + return d.getTime(); // ms + }; + + // Convert timestamp (sec OR ms) -> "h:00am/pm" + const timestampToTime = (ts: number): string => { + const ms = ts > 9999999999 ? ts : ts * 1000; // normalize to ms + const date = new Date(ms); + const hour = date.getHours(); + const displayHour = hour > 12 ? hour - 12 : hour === 0 ? 12 : hour; + const period = hour >= 12 ? "pm" : "am"; + return `${displayHour}:00${period}`; + }; + + const rooms: Room[] = useMemo(() => { + if (!locations) return []; + return locations + .filter((loc) => loc.capacity !== -1) + .map((loc) => ({ + id: loc.id, + name: loc.name, + building: loc.name.split(" ")[0], + floor: loc.name.match(/\d+/)?.[0]?.charAt(0) || "1", + capacity: loc.capacity, + })); + }, [locations]); + + // Build availability map (normalize incoming reservation times) + const availability = useMemo(() => { + const availabilityMap: { + [key: string]: { available: boolean; reservationId?: string }; + } = {}; + + if (!reservations || !rooms) return availabilityMap; + + // Initialize all slots as available + rooms.forEach((room) => { + timeSlots.forEach((time) => { + availabilityMap[`${room.id}-${time}`] = { available: true }; + }); + }); + + // Mark reserved slots + reservations.forEach((reservation) => { + const startLabel = timestampToTime(reservation.startTime); + const key = `${reservation.locationId}-${startLabel}`; + + const isUserTeamReservation = reservation.teamId === userTeam?.id; + + availabilityMap[key] = { + available: false, + reservationId: isUserTeamReservation ? reservation.id : undefined, + }; + }); + + return availabilityMap; + }, [reservations, rooms, timeSlots, userTeam]); + + const handleSlotClick = async ( + roomId: number, + time: string, + slotInfo: { available: boolean; reservationId?: string } + ) => { + if (!userTeam) { + toast.error("You must be part of a team to make reservations"); + return; + } + + // If clicking an existing reservation by this team, cancel it + if (slotInfo.reservationId) { + try { + await cancelReservation(slotInfo.reservationId); + toast.success("Reservation canceled"); + setSelectedSlot(null); + } catch (error) { + console.error("Cancel error:", error); + toast.error("Failed to cancel reservation"); + } + return; + } + + // If slot is not available, do nothing + if (!slotInfo.available) { + toast.error("This time slot is not available"); + return; + } + + setSelectedSlot({ roomId, time }); + }; + + const handleConfirmReservation = async () => { + if (!selectedSlot || !userTeam || !hackathon) return; + + try { + // ms throughout + const startTimeMs = timeToTimestamp(selectedSlot.time, selectedDate); + const endTimeMs = startTimeMs + 60 * 60 * 1000; // 1 hour + + const hackathonStartMs = + hackathon.startTime > 9999999999 + ? hackathon.startTime + : hackathon.startTime * 1000; + const hackathonEndMs = + hackathon.endTime > 9999999999 + ? hackathon.endTime + : hackathon.endTime * 1000; + + // Validate within bounds (ms) + if (startTimeMs < hackathonStartMs || endTimeMs > hackathonEndMs) { + toast.error("Reservation time must be within the hackathon period"); + return; + } + + await createReservation({ + locationId: selectedSlot.roomId, + teamId: userTeam.id, + startTime: startTimeMs, // ms + endTime: endTimeMs, // ms + hackathonId: userTeam.hackathonId, + }); + + toast.success("Reservation created successfully!"); + setSelectedSlot(null); + } catch (error: any) { + console.error("Create reservation error:", error); + toast.error(error?.message || "Failed to create reservation"); + } + }; + + const handleDateChange = (direction: "prev" | "next") => { + if (!dateRange.dates || dateRange.dates.length === 0) return; + + const currentIndex = dateRange.dates.findIndex( + (d) => d.toDateString() === selectedDate.toDateString() + ); + + if (direction === "next" && currentIndex < dateRange.dates.length - 1) { + setSelectedDate(dateRange.dates[currentIndex + 1]); + setSelectedSlot(null); + } else if (direction === "prev" && currentIndex > 0) { + setSelectedDate(dateRange.dates[currentIndex - 1]); + setSelectedSlot(null); + } + }; + + // Check if we can navigate to prev/next dates + const canNavigate = useMemo(() => { + if (!dateRange.dates || dateRange.dates.length === 0) + return { prev: false, next: false }; + + const currentIndex = dateRange.dates.findIndex( + (d) => d.toDateString() === selectedDate.toDateString() + ); + + return { + prev: currentIndex > 0, + next: currentIndex < dateRange.dates.length - 1, + }; + }, [dateRange.dates, selectedDate]); + + // Show message if user has no team + if (!userTeam && teams && !reservationsLoading) { + return ( +
+
+
No Team Found
+
+ You must be part of a team to make room reservations. +
+
+ Teams available: {teams.length} +
+ +
+
+ ); + } + + if (!teams || reservationsLoading || locationsLoading || hackathonLoading) { + return ( +
+
+ + Loading reservations... +
+ {!teams &&
Loading teams...
} + {hackathonLoading &&
Loading hackathon details...
} + {reservationsLoading &&
Loading reservations...
} + {locationsLoading &&
Loading locations...
} +
+
+
+ ); + } + + if (reservationsError || locationsError) { + return ( +
+
+
+ Error Loading Data +
+
+ {reservationsError && ( +
Reservations Error: {String(reservationsError)}
+ )} + {locationsError && ( +
Locations Error: {String(locationsError)}
+ )} +
+ +
+
+ ); + } + + if (!locations || locations.length === 0) { + return ( +
+
+
No Locations Available
+
+ There are no rooms available for reservation at this time. +
+
+
+ ); + } + + if (!hackathon) { + return ( +
+
+
No Hackathon Data
+
+ Unable to load hackathon timeframe information. +
+
+
+ ); + } + + return ( +
+
+
+

+ Room Reservations +

+
+ + {currentDate} + + + +
+
+ + {timeSlots.length === 0 ? ( +
+

+ No reservation slots available for this date. +

+
+ ) : ( + <> +
+ {/* Unified Grid Container */} +
+
+ {/* Header Row */} +
+ Space +
+ {timeSlots.map((time) => ( +
+ {time} +
+ ))} + + {/* Data Rows */} + {rooms.map((room, rowIndex) => ( + + {/* Room Name Cell */} +
+ + {room.name} + +
+ + {/* Time Slot Cells */} + {timeSlots.map((time) => { + const slotInfo = availability[`${room.id}-${time}`] || { + available: true, + }; + const isSelected = + selectedSlot?.roomId === room.id && + selectedSlot?.time === time; + const isUserReservation = Boolean( + slotInfo.reservationId + ); + + return ( +
+ handleSlotClick(room.id, time, slotInfo) + } + title={ + isUserReservation + ? `Your reservation at ${room.name} - Click to cancel` + : slotInfo.available + ? `Reserve ${room.name} at ${time}` + : "Not available" + } + /> + ); + })} + + ))} +
+
+
+ +
+
+
+ Available +
+
+
+ Unavailable +
+
+
+ Selected +
+
+
+ + Your Reservation (Click to Cancel) + +
+
+ + {selectedSlot && ( +
+

+ Selected Reservation +

+
+

+ Room:{" "} + + {rooms.find((r) => r.id === selectedSlot.roomId)?.name} + +

+

+ + Date & Time: + {" "} + + {selectedDate.toLocaleDateString()} at {selectedSlot.time} + +

+

+ + Duration: + {" "} + 1 hour +

+

+ Team:{" "} + + {userTeam?.name || "No Team"} + +

+
+ +
+ )} + + )} +
+
+ ); }; -export default ReservationSystem; \ No newline at end of file +export default ReservationSystem; diff --git a/src/lib/api/reservation/entity.ts b/src/lib/api/reservation/entity.ts index 094616b3..6b59fb52 100644 --- a/src/lib/api/reservation/entity.ts +++ b/src/lib/api/reservation/entity.ts @@ -1,28 +1,28 @@ export enum ReservationType { - PARTICIPANT = "participant", - ADMIN = "admin", + PARTICIPANT = "participant", + ADMIN = "admin", } export interface ReservationEntity { - id: string; - locationId: number; - teamId: string | null; - reservationType: ReservationType; - startTime: number; - endTime: number; - hackathonId: string; + id: string; + locationId: number; + teamId: string | null; + reservationType: ReservationType; + startTime: number; + endTime: number; + hackathonId: string; } export interface CreateReservationEntity { - locationId: number; - teamId: string; - startTime: number; - endTime: number; - hackathonId: string; + locationId: number; + teamId: string; + startTime: number; + endTime: number; + hackathonId: string; } export interface LocationEntity { - id: number; - name: string; - capacity: number; -} \ No newline at end of file + id: number; + name: string; + capacity: number; +} diff --git a/src/lib/api/reservation/hook.ts b/src/lib/api/reservation/hook.ts index fb47bbdf..d20fa3d1 100644 --- a/src/lib/api/reservation/hook.ts +++ b/src/lib/api/reservation/hook.ts @@ -1,52 +1,52 @@ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { - getReservations, - createReservation, - cancelReservation, - getLocations, + getReservations, + createReservation, + cancelReservation, + getLocations, } from "./provider"; import { ReservationEntity, CreateReservationEntity } from "./entity"; export const reservationQueryKeys = { - all: (hackathonId: string) => ["reservations", hackathonId] as const, - locations: ["locations"] as const, + all: (hackathonId: string) => ["reservations", hackathonId] as const, + locations: ["locations"] as const, }; export function useReservations(hackathonId: string) { - return useQuery({ - queryKey: reservationQueryKeys.all(hackathonId), - queryFn: () => getReservations(hackathonId), - enabled: Boolean(hackathonId), - }); + return useQuery({ + queryKey: reservationQueryKeys.all(hackathonId), + queryFn: () => getReservations(hackathonId), + enabled: Boolean(hackathonId), + }); } export function useLocations() { - return useQuery({ - queryKey: reservationQueryKeys.locations, - queryFn: getLocations, - }); + return useQuery({ + queryKey: reservationQueryKeys.locations, + queryFn: getLocations, + }); } export function useCreateReservation() { - const qc = useQueryClient(); - return useMutation({ - mutationFn: (data: CreateReservationEntity) => createReservation(data), - onSuccess: (_, variables) => { - qc.invalidateQueries({ - queryKey: reservationQueryKeys.all(variables.hackathonId), - }); - }, - }); + const qc = useQueryClient(); + return useMutation({ + mutationFn: (data: CreateReservationEntity) => createReservation(data), + onSuccess: (_, variables) => { + qc.invalidateQueries({ + queryKey: reservationQueryKeys.all(variables.hackathonId), + }); + }, + }); } export function useCancelReservation(hackathonId: string) { - const qc = useQueryClient(); - return useMutation({ - mutationFn: (reservationId: string) => cancelReservation(reservationId), - onSuccess: () => { - qc.invalidateQueries({ - queryKey: reservationQueryKeys.all(hackathonId), - }); - }, - }); -} \ No newline at end of file + const qc = useQueryClient(); + return useMutation({ + mutationFn: (reservationId: string) => cancelReservation(reservationId), + onSuccess: () => { + qc.invalidateQueries({ + queryKey: reservationQueryKeys.all(hackathonId), + }); + }, + }); +} diff --git a/src/lib/api/reservation/provider.ts b/src/lib/api/reservation/provider.ts index 86c32e28..3070360b 100644 --- a/src/lib/api/reservation/provider.ts +++ b/src/lib/api/reservation/provider.ts @@ -1,31 +1,35 @@ import { apiFetch } from "@/lib/api/apiClient"; -import { ReservationEntity, CreateReservationEntity, LocationEntity } from "./entity"; +import { + ReservationEntity, + CreateReservationEntity, + LocationEntity, +} from "./entity"; export async function getReservations( - hackathonId: string + hackathonId: string ): Promise { - return apiFetch( - `/reservations?hackathonId=${hackathonId}`, - { method: "GET" } - ); + return apiFetch( + `/reservations?hackathonId=${hackathonId}`, + { method: "GET" } + ); } export async function createReservation( - data: CreateReservationEntity + data: CreateReservationEntity ): Promise { - return apiFetch("/reservations", { - method: "POST", - body: JSON.stringify(data), - headers: { "Content-Type": "application/json" }, - }); + return apiFetch("/reservations", { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }); } export async function cancelReservation(reservationId: string): Promise { - return apiFetch(`/reservations/${reservationId}`, { - method: "DELETE", - }); + return apiFetch(`/reservations/${reservationId}`, { + method: "DELETE", + }); } export async function getLocations(): Promise { - return apiFetch("/locations", { method: "GET" }); -} \ No newline at end of file + return apiFetch("/locations", { method: "GET" }); +} From c811141a46f40e5b46804e4696065efef9c9e254 Mon Sep 17 00:00:00 2001 From: Khai Ta Date: Sun, 19 Oct 2025 15:59:52 -0400 Subject: [PATCH 10/28] only show team photos on photo gallery --- src/app/(protected)/photos/page.tsx | 70 ++++++++++++++++++++++++++--- src/lib/api/photo/hook.ts | 6 +-- src/lib/api/photo/provider.ts | 13 ++++-- 3 files changed, 78 insertions(+), 11 deletions(-) diff --git a/src/app/(protected)/photos/page.tsx b/src/app/(protected)/photos/page.tsx index d7ef6f8f..19b1ec6d 100644 --- a/src/app/(protected)/photos/page.tsx +++ b/src/app/(protected)/photos/page.tsx @@ -1,18 +1,62 @@ "use client"; -import React from "react"; +import React, { useMemo } from "react"; import PhotoGallery from "@/components/PhotoGallery"; import PhotoUpload from "@/components/PhotoUpload"; import { useFirebase } from "@/lib/providers/FirebaseProvider"; import { Button } from "@/components/ui/button"; import Image from "next/image"; import { usePhotos } from "@/lib/api/photo/hook"; +import { useUserInfoMe } from "@/lib/api/user/hook"; +import { useAllTeams } from "@/lib/api/team"; export default function PhotosPage() { const { isAuthenticated, isLoading } = useFirebase(); + const { data: userData, isLoading: isUserLoading } = useUserInfoMe(); + const { data: teams } = useAllTeams(); + + // Find user's team + const userTeam = teams?.find((team) => + [ + team.member1, + team.member2, + team.member3, + team.member4, + team.member5, + ].includes(userData?.id) + ); + const { data, isLoading: isPhotosLoading, refetch } = usePhotos(); - if (isLoading) { + // Filter photos to only show photos from the user's team + const teamPhotos = useMemo(() => { + if (!data || !Array.isArray(data) || !userTeam) return []; + + // Get list of all team member IDs + const teamMemberIds = [ + userTeam.member1, + userTeam.member2, + userTeam.member3, + userTeam.member4, + userTeam.member5, + ].filter(Boolean); + + // Filter photos that were uploaded by team members + // Photo names are in format: {userId}_{fileType}_{uuid}.{extension} + return data.filter((photo: any) => { + const photoName = photo.name || ""; + const parts = photoName.split("_"); + + // Extract userId (first part) and fileType (second part) + const userId = parts[0]; + const fileType = parts[1]; + + // Check if this photo is a "team" photo AND uploaded by a team member + return fileType === "team" && teamMemberIds.includes(userId); + }); + }, [data, userTeam]); + + if (isLoading || isUserLoading) { return (
@@ -42,6 +86,22 @@ export default function PhotosPage() { ); } + if (!userTeam) { + return ( +
+
+

Join a Team First

+

+ You need to be part of a team to view and upload team photos. +

+ +
+
+ ); + } + return (
@@ -51,10 +111,10 @@ export default function PhotosPage() { className="text-3xl md:text-4xl font-bold" style={{ fontFamily: "Monomaniac One, monospace" }} > - Photos + Team Photos

- A collection of moments from past events. + Photos from your team: {userTeam.name}

@@ -67,7 +127,7 @@ export default function PhotosPage() {
Loading photos...
) : ( p.url || p.photoUrl || p.name) : []} + images={teamPhotos.map((p: any) => p.url || p.photoUrl || p.name)} variant="photos" /> )} diff --git a/src/lib/api/photo/hook.ts b/src/lib/api/photo/hook.ts index a6a0af4c..9e09b88d 100644 --- a/src/lib/api/photo/hook.ts +++ b/src/lib/api/photo/hook.ts @@ -1,10 +1,10 @@ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { listPhotos, uploadPhoto, deletePhoto } from "./provider"; -export function usePhotos() { +export function usePhotos(photoType?: string) { return useQuery({ - queryKey: ["photos"], - queryFn: () => listPhotos(), + queryKey: ["photos", photoType], + queryFn: () => listPhotos(photoType), staleTime: 1000 * 60, // 1 minute }); } diff --git a/src/lib/api/photo/provider.ts b/src/lib/api/photo/provider.ts index 0c91b846..7606402a 100644 --- a/src/lib/api/photo/provider.ts +++ b/src/lib/api/photo/provider.ts @@ -1,8 +1,15 @@ import { apiFetch } from "../apiClient"; // Backend exposes routes under /photos (see apiv3 PhotoController) -export async function listPhotos() { - return apiFetch("/photos", { +export async function listPhotos(photoType?: string) { + const params = new URLSearchParams(); + if (photoType) { + params.append("photoType", photoType); + } + const queryString = params.toString(); + const url = queryString ? `/photos?${queryString}` : "/photos"; + + return apiFetch(url, { method: "GET", }); } @@ -10,7 +17,7 @@ export async function listPhotos() { export async function uploadPhoto(file: File, fileType = "default") { const fd = new FormData(); fd.append("photo", file); - fileType = file.type.split('/')[1] || 'default'; + fileType = file.type.split("/")[1] || "default"; fd.append("fileType", fileType); return apiFetch("/photos/upload", { From 4f4db53bd675bfec1f1c665504d11fad6a1dfe26 Mon Sep 17 00:00:00 2001 From: Nirja Rai Date: Sun, 19 Oct 2025 17:16:57 -0400 Subject: [PATCH 11/28] change flag enabling (local variable instead of flag) --- src/components/PrizesChallenges/index.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/PrizesChallenges/index.tsx b/src/components/PrizesChallenges/index.tsx index 8fefcc31..b0c930a4 100644 --- a/src/components/PrizesChallenges/index.tsx +++ b/src/components/PrizesChallenges/index.tsx @@ -59,7 +59,8 @@ const AwardBox: React.FC = ({ }; const PrizesChallenges: React.FC = () => { - const { data: prizesAndChallengesFlag } = useFlagState("PrizesEnabled"); + const { data: prizesAndChallengesFlag } = useFlagState("PrizeEnable"); + const isFlagEnabled = true; return (
{
- {prizesAndChallengesFlag?.isEnabled ? ( + {/* {prizesAndChallengesFlag?.isEnabled ? ( */} + {isFlagEnabled ? (
{ ); }; -export default PrizesChallenges; +export default PrizesChallenges; \ No newline at end of file From c907b12821c78389fda28074bb5171a2ff9ffbf1 Mon Sep 17 00:00:00 2001 From: Nirja Rai Date: Sun, 19 Oct 2025 17:36:41 -0400 Subject: [PATCH 12/28] updated prizes and challanges text --- src/components/PrizesChallenges/index.tsx | 49 ++++++++++------------- 1 file changed, 21 insertions(+), 28 deletions(-) diff --git a/src/components/PrizesChallenges/index.tsx b/src/components/PrizesChallenges/index.tsx index b0c930a4..f067d1e0 100644 --- a/src/components/PrizesChallenges/index.tsx +++ b/src/components/PrizesChallenges/index.tsx @@ -89,52 +89,45 @@ const PrizesChallenges: React.FC = () => { title="HackPSU Grand Prize" description="The standard HackPSU experience: work together alone or in a team to build something awesome! All monetary prizes will be split among the winning team members equally." prizes={[ - { place: "1st Place", amount: "$350 in cash" }, - { place: "2nd Place", amount: "$200 in cash" }, - { place: "3rd Place", amount: "$150 in cash" }, + { place: "1st Place", amount: "$500 in cash" }, + { place: "2nd Place", amount: "$300 in cash" }, + { place: "3rd Place", amount: "$100 in cash" }, ]} /> - - +
) : (
From 4e75dc50ea6dc0191d68e0d702991960cca2a485 Mon Sep 17 00:00:00 2001 From: Kanishk Sachdev Date: Sun, 19 Oct 2025 18:45:09 -0400 Subject: [PATCH 13/28] new track --- src/lib/api/judging/entity.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/lib/api/judging/entity.ts b/src/lib/api/judging/entity.ts index 86bcf0a4..76aa446d 100644 --- a/src/lib/api/judging/entity.ts +++ b/src/lib/api/judging/entity.ts @@ -32,9 +32,7 @@ export interface ProjectCreateEntity export interface ProjectPatchEntity extends Partial {} export const PROJECT_CATEGORIES = [ - "Machine Learning", - "Entrepreneurship", - "10th Anniversary: Timeless Tech", + "Beginner Track", ] as const; export type ProjectCategory = (typeof PROJECT_CATEGORIES)[number]; From de96fe1f87e9c60aa4406dcd22460d9aeca11a40 Mon Sep 17 00:00:00 2001 From: Kanishk Sachdev Date: Sun, 19 Oct 2025 21:47:51 -0400 Subject: [PATCH 14/28] Modify the photo gallery --- src/app/(protected)/photos/page.tsx | 348 +++++++++++++++++--------- src/components/PhotoUpload.tsx | 365 ++++++++++++++++++++++------ src/lib/api/photo/entity.ts | 112 +++++++++ src/lib/api/photo/hook.ts | 41 ++-- src/lib/api/photo/index.ts | 3 + src/lib/api/photo/provider.ts | 37 ++- 6 files changed, 677 insertions(+), 229 deletions(-) create mode 100644 src/lib/api/photo/entity.ts create mode 100644 src/lib/api/photo/index.ts diff --git a/src/app/(protected)/photos/page.tsx b/src/app/(protected)/photos/page.tsx index 19b1ec6d..32909903 100644 --- a/src/app/(protected)/photos/page.tsx +++ b/src/app/(protected)/photos/page.tsx @@ -1,137 +1,243 @@ "use client"; -import React, { useMemo } from "react"; -import PhotoGallery from "@/components/PhotoGallery"; +import React, { useState } from "react"; import PhotoUpload from "@/components/PhotoUpload"; +import { usePhotos } from "@/lib/api/photo"; +import { PHOTO_MILESTONES } from "@/lib/api/photo"; +import { Toaster } from "sonner"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Dialog, DialogContent } from "@/components/ui/dialog"; +import { Camera, Image as ImageIcon, Clock, X, ChevronLeft, ChevronRight } from "lucide-react"; import { useFirebase } from "@/lib/providers/FirebaseProvider"; import { Button } from "@/components/ui/button"; -import Image from "next/image"; -import { usePhotos } from "@/lib/api/photo/hook"; -import { useUserInfoMe } from "@/lib/api/user/hook"; -import { useAllTeams } from "@/lib/api/team"; export default function PhotosPage() { - const { isAuthenticated, isLoading } = useFirebase(); - const { data: userData, isLoading: isUserLoading } = useUserInfoMe(); - const { data: teams } = useAllTeams(); - - // Find user's team - const userTeam = teams?.find((team) => - [ - team.member1, - team.member2, - team.member3, - team.member4, - team.member5, - ].includes(userData?.id) - ); + const { data: photos, isLoading, refetch } = usePhotos(); + const { user } = useFirebase(); + const [selectedImageIndex, setSelectedImageIndex] = useState(null); + const [viewingMyPhotos, setViewingMyPhotos] = useState(false); - const { data, isLoading: isPhotosLoading, refetch } = usePhotos(); - - // Filter photos to only show photos from the user's team - const teamPhotos = useMemo(() => { - if (!data || !Array.isArray(data) || !userTeam) return []; - - // Get list of all team member IDs - const teamMemberIds = [ - userTeam.member1, - userTeam.member2, - userTeam.member3, - userTeam.member4, - userTeam.member5, - ].filter(Boolean); - - // Filter photos that were uploaded by team members - // Photo names are in format: {userId}_{fileType}_{uuid}.{extension} - return data.filter((photo: any) => { - const photoName = photo.name || ""; - const parts = photoName.split("_"); - - // Extract userId (first part) and fileType (second part) - const userId = parts[0]; - const fileType = parts[1]; - - // Check if this photo is a "team" photo AND uploaded by a team member - return fileType === "team" && teamMemberIds.includes(userId); - }); - }, [data, userTeam]); - - if (isLoading || isUserLoading) { - return ( -
-
-
- ); - } - - if (!isAuthenticated) { - return ( -
-
-

Sign in to view photos

-

- You must be signed in to access the photo gallery. -

- -
-
- ); - } - - if (!userTeam) { - return ( -
-
-

Join a Team First

-

- You need to be part of a team to view and upload team photos. -

- -
-
- ); - } + // Separate my photos (all of them) from community photos (only public) + const myPhotos = photos?.filter((photo) => { + const userId = photo.name.split("_")[0]; + return userId === user?.uid; + }) || []; + + const communityPhotos = photos?.filter((photo) => { + const userId = photo.name.split("_")[0]; + const fileType = photo.name.split("_")[1]; + return userId !== user?.uid && fileType === "public"; + }) || []; + + const currentPhotos = viewingMyPhotos ? myPhotos : communityPhotos; + const selectedPhoto = selectedImageIndex !== null ? currentPhotos[selectedImageIndex] : null; + + const openLightbox = (index: number, isMyPhotos: boolean) => { + setSelectedImageIndex(index); + setViewingMyPhotos(isMyPhotos); + }; + + const closeLightbox = () => { + setSelectedImageIndex(null); + }; + + const goToPrevious = () => { + if (selectedImageIndex !== null && selectedImageIndex > 0) { + setSelectedImageIndex(selectedImageIndex - 1); + } + }; + + const goToNext = () => { + if (selectedImageIndex !== null && selectedImageIndex < currentPhotos.length - 1) { + setSelectedImageIndex(selectedImageIndex + 1); + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "ArrowLeft") goToPrevious(); + if (e.key === "ArrowRight") goToNext(); + if (e.key === "Escape") closeLightbox(); + }; return ( -
-
-
-
-

- Team Photos -

-

- Photos from your team: {userTeam.name} -

+ <> + +
+
+ {/* Header */} +
+
+

+ Photo Gallery +

+

+ Share your HackPSU experience with the community. Upload photos, browse what others have captured, and relive the highlights of the event. +

+
-
-
- refetch()} /> -
+ {/* Upload Section */} +
+
+

+ Share a Photo +

+

+ Upload a photo to the public gallery and let everyone see your HackPSU moment. +

+
+ refetch()} /> +
+ + {/* My Photos Section */} + {myPhotos.length > 0 && ( +
+
+

Your Photos

+

+ {myPhotos.length} {myPhotos.length === 1 ? "photo" : "photos"} uploaded +

+
+
+ {myPhotos.map((photo, idx) => ( + + ))} +
+
+ )} - {isPhotosLoading ? ( -
Loading photos...
- ) : ( - p.url || p.photoUrl || p.name)} - variant="photos" - /> - )} + {/* Community Gallery Section */} +
+
+

+ Community Highlights +

+

+ {isLoading + ? "Loading gallery..." + : communityPhotos.length > 0 + ? `${communityPhotos.length} ${communityPhotos.length === 1 ? "photo" : "photos"} from the HackPSU community` + : "Waiting for the first community photo"} +

+
+ + {isLoading ? ( +
+
+
+

Loading gallery...

+
+
+ ) : communityPhotos.length > 0 ? ( +
+ {communityPhotos.map((photo, idx) => ( + + ))} +
+ ) : ( + + +
+
+ +
+

+ {myPhotos.length > 0 ? "You're the First!" : "Gallery Opening Soon"} +

+

+ {myPhotos.length > 0 + ? "Your photo is the first in the gallery. Others will join soon!" + : "Upload a photo above to start the gallery and inspire others to share their moments."} +

+
+
+
+ )} +
+
-
+ + {/* Lightbox Modal */} + + +
+ {/* Close Button */} + + + {/* Previous Button */} + {selectedImageIndex !== null && selectedImageIndex > 0 && ( + + )} + + {/* Next Button */} + {selectedImageIndex !== null && selectedImageIndex < currentPhotos.length - 1 && ( + + )} + + {/* Image Container */} + {selectedPhoto && ( +
+ {selectedPhoto.name} + {/* Image Counter */} +
+ {selectedImageIndex !== null && `${selectedImageIndex + 1} / ${currentPhotos.length}`} +
+
+ )} +
+
+
+ ); } diff --git a/src/components/PhotoUpload.tsx b/src/components/PhotoUpload.tsx index db1b2698..1fc19b5e 100644 --- a/src/components/PhotoUpload.tsx +++ b/src/components/PhotoUpload.tsx @@ -1,8 +1,25 @@ "use client"; import React, { useRef, useState } from "react"; -import { useUploadPhoto } from "@/lib/api/photo/hook"; +import { useUploadPhoto } from "@/lib/api/photo"; +import { PHOTO_MILESTONES } from "@/lib/api/photo"; import { Button } from "@/components/ui/button"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Card, CardContent } from "@/components/ui/card"; +import { Upload, X, Camera, Loader2 } from "lucide-react"; +import { toast } from "sonner"; +import { Progress } from "@/components/ui/progress"; + +interface FileWithPreview { + file: File; + preview: string; +} export default function PhotoUpload({ onUploaded, @@ -10,98 +27,304 @@ export default function PhotoUpload({ onUploaded?: () => void; }) { const inputRef = useRef(null); - const [preview, setPreview] = useState(null); - const [file, setFile] = useState(null); + const [files, setFiles] = useState([]); + const [fileType, setFileType] = useState("public"); const [isDragging, setIsDragging] = useState(false); + const [isUploading, setIsUploading] = useState(false); + const [uploadProgress, setUploadProgress] = useState<{current: number, total: number}>({current: 0, total: 0}); const upload = useUploadPhoto(); - const [isUploading, setIsUploading] = useState(false); - const onPick = (f?: File) => { - if (!f) return; - setFile(f); - setPreview(URL.createObjectURL(f)); + const validateFile = (f: File): boolean => { + // Validate file type + const validImageTypes = [ + "image/jpeg", + "image/jpg", + "image/png", + "image/gif", + "image/webp", + "image/heic", + "image/heif", + ]; + const validVideoTypes = [ + "video/mp4", + "video/quicktime", + "video/x-msvideo", + ]; + + if ( + !validImageTypes.includes(f.type) && + !validVideoTypes.includes(f.type) + ) { + toast.error(`${f.name}: Please upload an image or video file`); + return false; + } + + // Validate file size (100MB) + const maxSize = 100 * 1024 * 1024; + if (f.size > maxSize) { + toast.error(`${f.name}: File size must be less than 100MB`); + return false; + } + + return true; + }; + + const onPick = (fileList: FileList | null) => { + if (!fileList) return; + + const newFiles: FileWithPreview[] = []; + for (let i = 0; i < fileList.length; i++) { + const f = fileList[i]; + if (validateFile(f)) { + newFiles.push({ + file: f, + preview: URL.createObjectURL(f), + }); + } + } + + if (newFiles.length > 0) { + setFiles(prev => [...prev, ...newFiles]); + } }; const handleFile = (e: React.ChangeEvent) => { - const f = e.target.files?.[0]; - if (f) onPick(f); + onPick(e.target.files); + // Reset input so same files can be selected again + if (inputRef.current) { + inputRef.current.value = ""; + } }; const handleDrop = (e: React.DragEvent) => { e.preventDefault(); setIsDragging(false); - const f = e.dataTransfer.files?.[0]; - if (f) onPick(f); + onPick(e.dataTransfer.files); + }; + + const removeFile = (index: number) => { + setFiles(prev => { + const newFiles = [...prev]; + // Revoke object URL to prevent memory leak + URL.revokeObjectURL(newFiles[index].preview); + newFiles.splice(index, 1); + return newFiles; + }); }; const startUpload = async () => { - if (!file) return; + if (files.length === 0 || !fileType) { + toast.error("Please select at least one file and category"); + return; + } + setIsUploading(true); - try { - await upload.mutateAsync({ file }); - setFile(null); - setPreview(null); - onUploaded?.(); - } catch (e) { - console.error(e); - alert("Upload failed"); - } finally { - setIsUploading(false); + setUploadProgress({current: 0, total: files.length}); + + let successCount = 0; + let failCount = 0; + + for (let i = 0; i < files.length; i++) { + try { + await upload.mutateAsync({ file: files[i].file, fileType }); + successCount++; + setUploadProgress({current: i + 1, total: files.length}); + } catch (err: any) { + failCount++; + toast.error(`Failed to upload ${files[i].file.name}: ${err?.message || "Unknown error"}`); + } + } + + // Clean up + files.forEach(f => URL.revokeObjectURL(f.preview)); + setFiles([]); + setFileType("public"); + setIsUploading(false); + setUploadProgress({current: 0, total: 0}); + onUploaded?.(); + + // Show summary toast + if (successCount > 0) { + toast.success(`${successCount} ${successCount === 1 ? "photo" : "photos"} uploaded successfully!`, { + description: "All photos are reviewed by our team before appearing in the gallery. This typically takes a few minutes.", + duration: 6000, + }); + } + + if (failCount > 0) { + toast.error(`${failCount} ${failCount === 1 ? "photo" : "photos"} failed to upload.`); } }; + const selectedMilestone = PHOTO_MILESTONES.find((m) => m.id === fileType); + return ( -
-
e.preventDefault()} - onDragEnter={() => setIsDragging(true)} - onDragLeave={() => setIsDragging(false)} - onDrop={handleDrop} - className={`p-4 border-2 border-[#215172] rounded-md ${isDragging ? "border-[#215172]-400 bg-blue-50" : "border-dashed"}`} - > - -
-
-

Drag & drop a photo or

- -
- {preview && ( - preview + +
+ {/* Upload Area */} +
e.preventDefault()} + onDragEnter={() => setIsDragging(true)} + onDragLeave={() => setIsDragging(false)} + onDrop={handleDrop} + className={`relative border-2 border-dashed rounded-lg p-8 transition-colors ${ + isDragging + ? "border-primary bg-primary/5" + : "border-muted-foreground/25 hover:border-muted-foreground/50" + }`} + > + + + {files.length > 0 ? ( +
+
+ {files.map((fileWithPreview, idx) => ( +
+ {fileWithPreview.file.type.startsWith("video/") ? ( +
+ ))} +
+
+

+ {files.length} {files.length === 1 ? "file" : "files"} selected +

+ +
+
+ ) : ( +
+ +

+ Upload Photos or Videos +

+

+ Drag and drop your files here, or click to browse +

+ +

+ Supports: JPG, PNG, GIF, WebP, MP4, MOV (max 100MB each) +

+
+ )} +
+ + {/* Milestone Selection */} +
+ + + {selectedMilestone && ( +

+ {selectedMilestone.description} +

+ )} +
+ + {/* Upload Progress */} + {isUploading && ( +
+
+ + Uploading {uploadProgress.current} of {uploadProgress.total} + + + {Math.round((uploadProgress.current / uploadProgress.total) * 100)}% + +
+ +
)} + + {/* Upload Button */} +
+ + {files.length > 0 && !isUploading && ( + + )} +
-
- -
- - -
-
+ + ); } diff --git a/src/lib/api/photo/entity.ts b/src/lib/api/photo/entity.ts new file mode 100644 index 00000000..c9a2eb0b --- /dev/null +++ b/src/lib/api/photo/entity.ts @@ -0,0 +1,112 @@ +export interface PhotoEntity { + name: string; + url: string; + createdAt: string; +} + +export interface PhotoUploadResponse { + photoId: string; + photoUrl: string; +} + +export interface PendingPhoto { + photoId: string; + photoUrl: string; + fileType: string; + uploadedAt: string; +} + +export interface PhotoMilestone { + id: string; + label: string; + description: string; + category: "event" | "food" | "coding" | "social" | "general"; +} + +// Photo milestones for the HackPSU journey +export const PHOTO_MILESTONES: PhotoMilestone[] = [ + { + id: "public", + label: "Public Gallery", + description: "Share with the entire HackPSU community", + category: "general", + }, + { + id: "check-in", + label: "Check-In", + description: "Arrival at HackPSU", + category: "event", + }, + { + id: "team-formation", + label: "Team Formation", + description: "Meeting your team", + category: "social", + }, + { + id: "opening-ceremony", + label: "Opening Ceremony", + description: "Event kickoff", + category: "event", + }, + { + id: "hacking", + label: "Hacking Session", + description: "Building your project", + category: "coding", + }, + { + id: "lunch", + label: "Lunch", + description: "Midday meal", + category: "food", + }, + { + id: "dinner", + label: "Dinner", + description: "Evening meal", + category: "food", + }, + { + id: "midnight-snack", + label: "Midnight Snack", + description: "Late night fuel", + category: "food", + }, + { + id: "workshop", + label: "Workshop", + description: "Learning session", + category: "event", + }, + { + id: "mentoring", + label: "Mentoring", + description: "Getting help from mentors", + category: "social", + }, + { + id: "networking", + label: "Networking", + description: "Meeting sponsors and peers", + category: "social", + }, + { + id: "demo", + label: "Project Demo", + description: "Presenting your work", + category: "event", + }, + { + id: "closing-ceremony", + label: "Closing Ceremony", + description: "Awards and wrap-up", + category: "event", + }, + { + id: "other", + label: "Other Moment", + description: "Any other memorable moment", + category: "general", + }, +]; diff --git a/src/lib/api/photo/hook.ts b/src/lib/api/photo/hook.ts index 9e09b88d..2a310a6d 100644 --- a/src/lib/api/photo/hook.ts +++ b/src/lib/api/photo/hook.ts @@ -1,27 +1,32 @@ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; -import { listPhotos, uploadPhoto, deletePhoto } from "./provider"; +import { listPhotos, uploadPhoto } from "./provider"; +import type { PhotoEntity, PhotoUploadResponse } from "./entity"; -export function usePhotos(photoType?: string) { - return useQuery({ - queryKey: ["photos", photoType], - queryFn: () => listPhotos(photoType), - staleTime: 1000 * 60, // 1 minute +/** + * Hook to fetch all approved photos from the gallery + */ +export function usePhotos() { + return useQuery({ + queryKey: ["photos"], + queryFn: () => listPhotos(), + staleTime: 1000 * 30, // 30 seconds + refetchOnWindowFocus: true, }); } +/** + * Hook to upload a photo with a milestone/reason + */ export function useUploadPhoto() { const qc = useQueryClient(); - return useMutation({ - mutationFn: ({ file, fileType }: { file: File; fileType?: string }) => - uploadPhoto(file, fileType), - onSuccess: () => qc.invalidateQueries({ queryKey: ["photos"] }), - }); -} - -export function useDeletePhoto() { - const qc = useQueryClient(); - return useMutation({ - mutationFn: (photoId: string) => deletePhoto(photoId), - onSuccess: () => qc.invalidateQueries({ queryKey: ["photos"] }), + return useMutation< + PhotoUploadResponse, + Error, + { file: File; fileType: string } + >({ + mutationFn: ({ file, fileType }) => uploadPhoto(file, fileType), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ["photos"] }); + }, }); } diff --git a/src/lib/api/photo/index.ts b/src/lib/api/photo/index.ts new file mode 100644 index 00000000..61f2c36f --- /dev/null +++ b/src/lib/api/photo/index.ts @@ -0,0 +1,3 @@ +export * from "./entity"; +export * from "./provider"; +export * from "./hook"; diff --git a/src/lib/api/photo/provider.ts b/src/lib/api/photo/provider.ts index 7606402a..169badb1 100644 --- a/src/lib/api/photo/provider.ts +++ b/src/lib/api/photo/provider.ts @@ -1,33 +1,32 @@ import { apiFetch } from "../apiClient"; +import type { PhotoEntity, PhotoUploadResponse } from "./entity"; -// Backend exposes routes under /photos (see apiv3 PhotoController) -export async function listPhotos(photoType?: string) { - const params = new URLSearchParams(); - if (photoType) { - params.append("photoType", photoType); - } - const queryString = params.toString(); - const url = queryString ? `/photos?${queryString}` : "/photos"; - - return apiFetch(url, { +/** + * Get all approved photos from the gallery + * Only returns photos with approvalStatus === "approved" + */ +export async function listPhotos(): Promise { + return apiFetch("/photos", { method: "GET", }); } -export async function uploadPhoto(file: File, fileType = "default") { +/** + * Upload a photo or video to the gallery + * @param file - The image or video file to upload + * @param fileType - The milestone/reason for the photo (e.g., "check-in", "lunch", "midnight-snack") + * @returns Upload response with photoId and photoUrl + */ +export async function uploadPhoto( + file: File, + fileType: string = "other" +): Promise { const fd = new FormData(); fd.append("photo", file); - fileType = file.type.split("/")[1] || "default"; fd.append("fileType", fileType); - return apiFetch("/photos/upload", { + return apiFetch("/photos/upload", { method: "POST", body: fd, }); } - -export async function deletePhoto(photoId: string) { - return apiFetch(`/photos/${encodeURIComponent(photoId)}`, { - method: "DELETE", - }); -} From c0c30dead4e3c34f2f70faaf69a5d16f5b62d696 Mon Sep 17 00:00:00 2001 From: Kanishk Sachdev Date: Sun, 19 Oct 2025 22:08:10 -0400 Subject: [PATCH 15/28] Small mods to room reservation system --- src/components/ReservationSystem/index.tsx | 481 ++++++++++++++------- 1 file changed, 333 insertions(+), 148 deletions(-) diff --git a/src/components/ReservationSystem/index.tsx b/src/components/ReservationSystem/index.tsx index 6405a08e..8fb1bc9d 100644 --- a/src/components/ReservationSystem/index.tsx +++ b/src/components/ReservationSystem/index.tsx @@ -1,7 +1,7 @@ "use client"; import React, { useState, useMemo } from "react"; -import { ChevronLeft, ChevronRight, Loader2 } from "lucide-react"; +import { ChevronLeft, ChevronRight, Loader2, Calendar, Clock, Users, MapPin, X } from "lucide-react"; import { useReservations, useLocations, @@ -12,6 +12,14 @@ import { useAllTeams } from "@/lib/api/team/hook"; import { useActiveHackathonForStatic } from "@/lib/api/hackathon/hook"; import { useFirebase } from "@/lib/providers/FirebaseProvider"; import { toast } from "sonner"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; interface Room { id: number; @@ -80,12 +88,12 @@ const ReservationSystem: React.FC = () => { } }, [hackathon, initialDate]); - // Fetch data using hackathon ID from user's team + // Fetch data using hackathon ID from active hackathon const { data: reservations, isLoading: reservationsLoading, error: reservationsError, - } = useReservations(userTeam?.hackathonId || ""); + } = useReservations(hackathon?.id || ""); const { data: locations, isLoading: locationsLoading, @@ -94,12 +102,15 @@ const ReservationSystem: React.FC = () => { const { mutateAsync: createReservation, isPending: isCreating } = useCreateReservation(); const { mutateAsync: cancelReservation, isPending: isCanceling } = - useCancelReservation(userTeam?.hackathonId || ""); + useCancelReservation(hackathon?.id || ""); - const [selectedSlot, setSelectedSlot] = useState<{ + const [selectedSlots, setSelectedSlots] = useState<{ roomId: number; - time: string; + times: string[]; } | null>(null); + const [isModalOpen, setIsModalOpen] = useState(false); + const [cancelModalOpen, setCancelModalOpen] = useState(false); + const [reservationToCancel, setReservationToCancel] = useState(null); // Get valid date range for the hackathon (normalize to ms) const dateRange = useMemo(() => { @@ -134,7 +145,7 @@ const ReservationSystem: React.FC = () => { console.log("=== Reservation System Debug ==="); console.log("User:", user?.uid); console.log("User Team:", userTeam); - console.log("Hackathon ID:", userTeam?.hackathonId); + console.log("Hackathon ID:", hackathon?.id); console.log("Hackathon Data:", hackathon); if (hackathon) { const startMs = @@ -295,16 +306,11 @@ const ReservationSystem: React.FC = () => { return; } - // If clicking an existing reservation by this team, cancel it + // If clicking an existing reservation by this team, open cancel modal if (slotInfo.reservationId) { - try { - await cancelReservation(slotInfo.reservationId); - toast.success("Reservation canceled"); - setSelectedSlot(null); - } catch (error) { - console.error("Cancel error:", error); - toast.error("Failed to cancel reservation"); - } + setReservationToCancel(slotInfo.reservationId); + setSelectedSlots({ roomId, times: [time] }); + setCancelModalOpen(true); return; } @@ -314,17 +320,31 @@ const ReservationSystem: React.FC = () => { return; } - setSelectedSlot({ roomId, time }); + // Toggle slot selection + if (selectedSlots?.roomId === roomId) { + const timeIndex = selectedSlots.times.indexOf(time); + if (timeIndex > -1) { + // Deselect this time + const newTimes = selectedSlots.times.filter((t) => t !== time); + if (newTimes.length === 0) { + setSelectedSlots(null); + } else { + setSelectedSlots({ roomId, times: newTimes }); + } + } else { + // Add this time + setSelectedSlots({ roomId, times: [...selectedSlots.times, time].sort() }); + } + } else { + // New room selection + setSelectedSlots({ roomId, times: [time] }); + } }; const handleConfirmReservation = async () => { - if (!selectedSlot || !userTeam || !hackathon) return; + if (!selectedSlots || !userTeam || !hackathon) return; try { - // ms throughout - const startTimeMs = timeToTimestamp(selectedSlot.time, selectedDate); - const endTimeMs = startTimeMs + 60 * 60 * 1000; // 1 hour - const hackathonStartMs = hackathon.startTime > 9999999999 ? hackathon.startTime @@ -334,28 +354,60 @@ const ReservationSystem: React.FC = () => { ? hackathon.endTime : hackathon.endTime * 1000; - // Validate within bounds (ms) - if (startTimeMs < hackathonStartMs || endTimeMs > hackathonEndMs) { - toast.error("Reservation time must be within the hackathon period"); - return; - } + // Sort times to create consecutive reservations + const sortedTimes = [...selectedSlots.times].sort((a, b) => { + const aMs = timeToTimestamp(a, selectedDate); + const bMs = timeToTimestamp(b, selectedDate); + return aMs - bMs; + }); - await createReservation({ - locationId: selectedSlot.roomId, - teamId: userTeam.id, - startTime: startTimeMs, // ms - endTime: endTimeMs, // ms - hackathonId: userTeam.hackathonId, + // Create reservations for each selected slot + const reservationPromises = sortedTimes.map(async (time) => { + const startTimeMs = timeToTimestamp(time, selectedDate); + const endTimeMs = startTimeMs + 60 * 60 * 1000; // 1 hour + + // Validate within bounds (ms) + if (startTimeMs < hackathonStartMs || endTimeMs > hackathonEndMs) { + throw new Error("Reservation time must be within the hackathon period"); + } + + return createReservation({ + locationId: selectedSlots.roomId, + teamId: userTeam.id, + startTime: startTimeMs, // ms + endTime: endTimeMs, // ms + hackathonId: hackathon.id, + }); }); - toast.success("Reservation created successfully!"); - setSelectedSlot(null); + await Promise.all(reservationPromises); + + toast.success( + `${sortedTimes.length} reservation${sortedTimes.length > 1 ? "s" : ""} created successfully!` + ); + setSelectedSlots(null); + setIsModalOpen(false); } catch (error: any) { console.error("Create reservation error:", error); toast.error(error?.message || "Failed to create reservation"); } }; + const handleCancelReservation = async () => { + if (!reservationToCancel) return; + + try { + await cancelReservation(reservationToCancel); + toast.success("Reservation canceled successfully!"); + setReservationToCancel(null); + setSelectedSlots(null); + setCancelModalOpen(false); + } catch (error) { + console.error("Cancel error:", error); + toast.error("Failed to cancel reservation"); + } + }; + const handleDateChange = (direction: "prev" | "next") => { if (!dateRange.dates || dateRange.dates.length === 0) return; @@ -365,10 +417,10 @@ const ReservationSystem: React.FC = () => { if (direction === "next" && currentIndex < dateRange.dates.length - 1) { setSelectedDate(dateRange.dates[currentIndex + 1]); - setSelectedSlot(null); + setSelectedSlots(null); } else if (direction === "prev" && currentIndex > 0) { setSelectedDate(dateRange.dates[currentIndex - 1]); - setSelectedSlot(null); + setSelectedSlots(null); } }; @@ -482,39 +534,66 @@ const ReservationSystem: React.FC = () => { return (
-
+

Room Reservations

-
- - {currentDate} - - - + +
+
+ +
+ + + {currentDate} + +
+ +
+ +
+
+
+ Available +
+
+
+ Selected +
+
+
+ Unavailable +
+
+
+ Your Reservation +
+
@@ -526,24 +605,24 @@ const ReservationSystem: React.FC = () => {
) : ( <> -
+
{/* Unified Grid Container */}
{/* Header Row */} -
+
Space
{timeSlots.map((time) => (
{time}
@@ -554,11 +633,11 @@ const ReservationSystem: React.FC = () => { {/* Room Name Cell */}
- + {room.name}
@@ -569,8 +648,8 @@ const ReservationSystem: React.FC = () => { available: true, }; const isSelected = - selectedSlot?.roomId === room.id && - selectedSlot?.time === time; + selectedSlots?.roomId === room.id && + selectedSlots?.times.includes(time); const isUserReservation = Boolean( slotInfo.reservationId ); @@ -578,14 +657,14 @@ const ReservationSystem: React.FC = () => { return (
handleSlotClick(room.id, time, slotInfo) @@ -594,7 +673,9 @@ const ReservationSystem: React.FC = () => { isUserReservation ? `Your reservation at ${room.name} - Click to cancel` : slotInfo.available - ? `Reserve ${room.name} at ${time}` + ? isSelected + ? `Deselect ${time}` + : `Select ${time} at ${room.name}` : "Not available" } /> @@ -606,79 +687,183 @@ const ReservationSystem: React.FC = () => {
-
-
-
- Available -
-
-
- Unavailable -
-
-
- Selected -
-
-
- - Your Reservation (Click to Cancel) - -
-
- - {selectedSlot && ( -
-

- Selected Reservation -

-
-

- Room:{" "} - - {rooms.find((r) => r.id === selectedSlot.roomId)?.name} - -

-

- - Date & Time: - {" "} - - {selectedDate.toLocaleDateString()} at {selectedSlot.time} - -

-

- - Duration: - {" "} - 1 hour -

-

- Team:{" "} - - {userTeam?.name || "No Team"} - -

-
+ {selectedSlots && selectedSlots.times.length > 0 && ( +
)} + + {/* Booking Modal */} + + + + + Confirm Reservation{selectedSlots && selectedSlots.times.length > 1 ? 's' : ''} + + + Review your reservation details before confirming. + + + +
+

+ Note: Reserving a room does not guarantee exclusive use. + Larger rooms are shared hacking spaces. This system ensures your team has a designated spot + and helps manage room capacity to keep everyone comfortable and productive. +

+
+ + {selectedSlots && ( +
+
+ +
+

Room

+

+ {rooms.find((r) => r.id === selectedSlots.roomId)?.name} +

+
+
+ +
+ +
+

Date

+

+ {selectedDate.toLocaleDateString("en-US", { + weekday: "long", + month: "long", + day: "numeric", + })} +

+
+
+ +
+ +
+

+ Time Slots ({selectedSlots.times.length} hour{selectedSlots.times.length > 1 ? 's' : ''}) +

+
+ {selectedSlots.times.map((time) => ( + + {time} + + ))} +
+
+
+ +
+ +
+

Team

+

+ {userTeam?.name || "No Team"} +

+
+
+
+ )} + + + + + +
+
+ + {/* Cancel Modal */} + + + + + Cancel Reservation + + + Are you sure you want to cancel this reservation? + + + + {selectedSlots && ( +
+
+ +
+

Room

+

+ {rooms.find((r) => r.id === selectedSlots.roomId)?.name} +

+
+
+ +
+ +
+

Time

+

+ {selectedDate.toLocaleDateString()} at {selectedSlots.times[0]} +

+
+
+
+ )} + + + + + +
+
)}
From bd4d6a81c14dd88f1793424f6824a2553203759f Mon Sep 17 00:00:00 2001 From: Nirja Rai Date: Sun, 19 Oct 2025 22:14:20 -0400 Subject: [PATCH 16/28] Text font update --- src/components/PrizesChallenges/index.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/PrizesChallenges/index.tsx b/src/components/PrizesChallenges/index.tsx index f067d1e0..c7a8b062 100644 --- a/src/components/PrizesChallenges/index.tsx +++ b/src/components/PrizesChallenges/index.tsx @@ -23,7 +23,7 @@ const AwardBox: React.FC = ({ }) => { return (
-
+

{title}

@@ -59,8 +59,9 @@ const AwardBox: React.FC = ({ }; const PrizesChallenges: React.FC = () => { - const { data: prizesAndChallengesFlag } = useFlagState("PrizeEnable"); - const isFlagEnabled = true; + // const { data: prizesAndChallengesFlag } = useFlagState("PrizeEnable"); + const { data: prizesAndChallengesFlag } = {data: {isEnabled: true}}; + // const isFlagEnabled = true; return (
{
{/* {prizesAndChallengesFlag?.isEnabled ? ( */} - {isFlagEnabled ? ( + {prizesAndChallengesFlag?.isEnabled ? (
Date: Sun, 19 Oct 2025 23:22:35 -0400 Subject: [PATCH 17/28] Update AwardBox styling and improve layout for better readability --- src/components/PrizesChallenges/index.tsx | 76 +++++++++++------------ src/components/Schedule/index.tsx | 2 +- 2 files changed, 36 insertions(+), 42 deletions(-) diff --git a/src/components/PrizesChallenges/index.tsx b/src/components/PrizesChallenges/index.tsx index c7a8b062..26bb456d 100644 --- a/src/components/PrizesChallenges/index.tsx +++ b/src/components/PrizesChallenges/index.tsx @@ -22,34 +22,32 @@ const AwardBox: React.FC = ({ extra, }) => { return ( -
-
-

+
+
+

{title}

{description && ( -

+

{description}

)} {prizes.length > 0 && ( - - - {prizes.map((prize, index) => ( - - - - - ))} - -
- {prize.place}: - - {prize.amount} -
+
+ {prizes.map((prize, index) => ( +
+
+ {prize.place} +
+
+ {prize.amount} +
+
+ ))} +
)} {extra && ( -
+
{extra}
)} @@ -66,26 +64,26 @@ const PrizesChallenges: React.FC = () => { return (
-
+

Prizes & Challenges

-
+
{/* {prizesAndChallengesFlag?.isEnabled ? ( */} {prizesAndChallengesFlag?.isEnabled ? ( -
+
{ /> @@ -122,9 +109,16 @@ const PrizesChallenges: React.FC = () => { description="Use the power of artificial intelligence to address a problem in the fields of Health, Humanitarianism, Education, Environment, and/or Agriculture." prizes={[ { - place: "Prize", - amount: - "$870 in cash", + place: "1st Place", + amount: "$99 Amazon Gift Card per team member (up to 5)", + }, + { + place: "2nd Place", + amount: "$50 Amazon Gift Card per team member (up to 5)", + }, + { + place: "3rd Place", + amount: "$25 Amazon Gift Card per team member (up to 5)", }, ]} /> diff --git a/src/components/Schedule/index.tsx b/src/components/Schedule/index.tsx index 30ccf5b0..f35f4c19 100644 --- a/src/components/Schedule/index.tsx +++ b/src/components/Schedule/index.tsx @@ -632,7 +632,7 @@ const Schedule: React.FC = () => { // State to track if component is mounted (client-side const tempScroll = useScroll({ - offset: ["1700px", "2500px"], + offset: ["2200px", "2900px"], }); // pick the real scrollYProgress only after mount From 3725d17ec092be37ad86f5f2ddc50069f9738404 Mon Sep 17 00:00:00 2001 From: Kanishk Sachdev Date: Sun, 19 Oct 2025 23:33:46 -0400 Subject: [PATCH 18/28] fix flags --- src/components/PrizesChallenges/index.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/PrizesChallenges/index.tsx b/src/components/PrizesChallenges/index.tsx index 26bb456d..2ce798e8 100644 --- a/src/components/PrizesChallenges/index.tsx +++ b/src/components/PrizesChallenges/index.tsx @@ -57,9 +57,7 @@ const AwardBox: React.FC = ({ }; const PrizesChallenges: React.FC = () => { - // const { data: prizesAndChallengesFlag } = useFlagState("PrizeEnable"); - const { data: prizesAndChallengesFlag } = {data: {isEnabled: true}}; - // const isFlagEnabled = true; + const { data: prizesAndChallengesFlag } = useFlagState("PrizeEnable"); return (
Date: Mon, 20 Oct 2025 03:38:35 +0000 Subject: [PATCH 19/28] Update dependency tailwindcss to v3.4.18 --- package.json | 2 +- yarn.lock | 32 +++++++++++++------------------- 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index 68ffa747..1dcd6bcd 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "sonner": "^2.0.6", "swiper": "^12.0.0", "tailwind-merge": "^3.3.1", - "tailwindcss": "3.4.17", + "tailwindcss": "3.4.18", "tailwindcss-animate": "^1.0.7", "typescript": "^5.9.2", "zod": "^3.24.2" diff --git a/yarn.lock b/yarn.lock index 7cea636b..08a3719a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4941,7 +4941,7 @@ jackspeak@^4.1.1: dependencies: "@isaacs/cliui" "^8.0.2" -jiti@^1.21.6: +jiti@^1.21.7: version "1.21.7" resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.7.tgz#9dd81043424a3d28458b193d965f0d18a2300ba9" integrity sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A== @@ -5065,7 +5065,7 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" -lilconfig@^3.0.0, lilconfig@^3.1.3: +lilconfig@^3.1.1, lilconfig@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.1.3.tgz#a1bcfd6257f9585bf5ae14ceeebb7b559025e4c4" integrity sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw== @@ -5628,13 +5628,12 @@ postcss-js@^4.0.1: dependencies: camelcase-css "^2.0.1" -postcss-load-config@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-4.0.2.tgz#7159dcf626118d33e299f485d6afe4aff7c4a3e3" - integrity sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ== +"postcss-load-config@^4.0.2 || ^5.0 || ^6.0": + version "6.0.1" + resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-6.0.1.tgz#6fd7dcd8ae89badcf1b2d644489cbabf83aa8096" + integrity sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g== dependencies: - lilconfig "^3.0.0" - yaml "^2.3.4" + lilconfig "^3.1.1" postcss-nested@^6.2.0: version "6.2.0" @@ -6481,10 +6480,10 @@ tailwindcss-animate@^1.0.7: resolved "https://registry.yarnpkg.com/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz#318b692c4c42676cc9e67b19b78775742388bef4" integrity sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA== -tailwindcss@3.4.17: - version "3.4.17" - resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.4.17.tgz#ae8406c0f96696a631c790768ff319d46d5e5a63" - integrity sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og== +tailwindcss@3.4.18: + version "3.4.18" + resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.4.18.tgz#9fa9650aace186644b608242f1e57d2d55593301" + integrity sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ== dependencies: "@alloc/quick-lru" "^5.2.0" arg "^5.0.2" @@ -6494,7 +6493,7 @@ tailwindcss@3.4.17: fast-glob "^3.3.2" glob-parent "^6.0.2" is-glob "^4.0.3" - jiti "^1.21.6" + jiti "^1.21.7" lilconfig "^3.1.3" micromatch "^4.0.8" normalize-path "^3.0.0" @@ -6503,7 +6502,7 @@ tailwindcss@3.4.17: postcss "^8.4.47" postcss-import "^15.1.0" postcss-js "^4.0.1" - postcss-load-config "^4.0.2" + postcss-load-config "^4.0.2 || ^5.0 || ^6.0" postcss-nested "^6.2.0" postcss-selector-parser "^6.1.2" resolve "^1.22.8" @@ -6870,11 +6869,6 @@ yaml@^1.10.0: resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== -yaml@^2.3.4: - version "2.7.0" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.7.0.tgz#aef9bb617a64c937a9a748803786ad8d3ffe1e98" - integrity sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA== - yaml@^2.8.1: version "2.8.1" resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.8.1.tgz#1870aa02b631f7e8328b93f8bc574fac5d6c4d79" From 4edef8603c4f98f7f806644592e226cd9d7dde78 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 20 Oct 2025 03:38:45 +0000 Subject: [PATCH 20/28] Update dependency @types/node to v24.8.1 --- yarn.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/yarn.lock b/yarn.lock index 7cea636b..745a73e5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2854,11 +2854,11 @@ integrity sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg== "@types/node@>=12.12.47", "@types/node@>=13.7.0", "@types/node@^24.1.0": - version "24.6.1" - resolved "https://registry.yarnpkg.com/@types/node/-/node-24.6.1.tgz#29cd365beb4419b3b8271c7464f1a563446d7481" - integrity sha512-ljvjjs3DNXummeIaooB4cLBKg2U6SPI6Hjra/9rRIy7CpM0HpLtG9HptkMKAb4HYWy5S7HUvJEuWgr/y0U8SHw== + version "24.8.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-24.8.1.tgz#74c8ae00b045a0a351f2837ec00f25dfed0053be" + integrity sha512-alv65KGRadQVfVcG69MuB4IzdYVpRwMG/mq8KWOaoOdyY617P5ivaDiMCGOFDWD2sAn5Q0mR3mRtUOgm99hL9Q== dependencies: - undici-types "~7.13.0" + undici-types "~7.14.0" "@types/parse-json@^4.0.0": version "4.0.2" @@ -6655,10 +6655,10 @@ unbox-primitive@^1.1.0: has-symbols "^1.1.0" which-boxed-primitive "^1.1.1" -undici-types@~7.13.0: - version "7.13.0" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.13.0.tgz#a20ba7c0a2be0c97bd55c308069d29d167466bff" - integrity sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ== +undici-types@~7.14.0: + version "7.14.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.14.0.tgz#4c037b32ca4d7d62fae042174604341588bc0840" + integrity sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA== unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.1" From e334cdf4ee51530cbdb615bb844138f0c98d19aa Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 20 Oct 2025 03:38:51 +0000 Subject: [PATCH 21/28] Update dependency eslint to v9.38.0 --- package.json | 2 +- yarn.lock | 73 ++++++++++++++++++++++++++-------------------------- 2 files changed, 38 insertions(+), 37 deletions(-) diff --git a/package.json b/package.json index 68ffa747..4427684c 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", - "eslint": "9.36.0", + "eslint": "9.38.0", "eslint-config-next": "15.5.4", "firebase": "^12.0.0", "form-data": "^4.0.0", diff --git a/yarn.lock b/yarn.lock index 7cea636b..4990d7eb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1111,24 +1111,26 @@ resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.1.tgz#cfc6cffe39df390a3841cde2abccf92eaa7ae0e0" integrity sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ== -"@eslint/config-array@^0.21.0": - version "0.21.0" - resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.21.0.tgz#abdbcbd16b124c638081766392a4d6b509f72636" - integrity sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ== +"@eslint/config-array@^0.21.1": + version "0.21.1" + resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.21.1.tgz#7d1b0060fea407f8301e932492ba8c18aff29713" + integrity sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA== dependencies: - "@eslint/object-schema" "^2.1.6" + "@eslint/object-schema" "^2.1.7" debug "^4.3.1" minimatch "^3.1.2" -"@eslint/config-helpers@^0.3.1": - version "0.3.1" - resolved "https://registry.yarnpkg.com/@eslint/config-helpers/-/config-helpers-0.3.1.tgz#d316e47905bd0a1a931fa50e669b9af4104d1617" - integrity sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA== +"@eslint/config-helpers@^0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@eslint/config-helpers/-/config-helpers-0.4.1.tgz#7d173a1a35fe256f0989a0fdd8d911ebbbf50037" + integrity sha512-csZAzkNhsgwb0I/UAV6/RGFTbiakPCf0ZrGmrIxQpYvGZ00PhTkSnyKNolphgIvmnJeGw6rcGVEXfTzUnFuEvw== + dependencies: + "@eslint/core" "^0.16.0" -"@eslint/core@^0.15.2": - version "0.15.2" - resolved "https://registry.yarnpkg.com/@eslint/core/-/core-0.15.2.tgz#59386327d7862cc3603ebc7c78159d2dcc4a868f" - integrity sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg== +"@eslint/core@^0.16.0": + version "0.16.0" + resolved "https://registry.yarnpkg.com/@eslint/core/-/core-0.16.0.tgz#490254f275ba9667ddbab344f4f0a6b7a7bd7209" + integrity sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q== dependencies: "@types/json-schema" "^7.0.15" @@ -1147,22 +1149,22 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@9.36.0": - version "9.36.0" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.36.0.tgz#b1a3893dd6ce2defed5fd49de805ba40368e8fef" - integrity sha512-uhCbYtYynH30iZErszX78U+nR3pJU3RHGQ57NXy5QupD4SBVwDeU8TNBy+MjMngc1UyIW9noKqsRqfjQTBU2dw== +"@eslint/js@9.38.0": + version "9.38.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.38.0.tgz#f7aa9c7577577f53302c1d795643589d7709ebd1" + integrity sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A== -"@eslint/object-schema@^2.1.6": - version "2.1.6" - resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-2.1.6.tgz#58369ab5b5b3ca117880c0f6c0b0f32f6950f24f" - integrity sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA== +"@eslint/object-schema@^2.1.7": + version "2.1.7" + resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-2.1.7.tgz#6e2126a1347e86a4dedf8706ec67ff8e107ebbad" + integrity sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA== -"@eslint/plugin-kit@^0.3.5": - version "0.3.5" - resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz#fd8764f0ee79c8ddab4da65460c641cefee017c5" - integrity sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w== +"@eslint/plugin-kit@^0.4.0": + version "0.4.0" + resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz#f6a245b42886abf6fc9c7ab7744a932250335ab2" + integrity sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A== dependencies: - "@eslint/core" "^0.15.2" + "@eslint/core" "^0.16.0" levn "^0.4.1" "@firebase/ai@2.3.0": @@ -4154,24 +4156,23 @@ eslint-visitor-keys@^4.2.1: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz#4cfea60fe7dd0ad8e816e1ed026c1d5251b512c1" integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ== -eslint@9.36.0: - version "9.36.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.36.0.tgz#9cc5cbbfb9c01070425d9bfed81b4e79a1c09088" - integrity sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ== +eslint@9.38.0: + version "9.38.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.38.0.tgz#3957d2af804e5cf6cc503c618f60acc71acb2e7e" + integrity sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw== dependencies: "@eslint-community/eslint-utils" "^4.8.0" "@eslint-community/regexpp" "^4.12.1" - "@eslint/config-array" "^0.21.0" - "@eslint/config-helpers" "^0.3.1" - "@eslint/core" "^0.15.2" + "@eslint/config-array" "^0.21.1" + "@eslint/config-helpers" "^0.4.1" + "@eslint/core" "^0.16.0" "@eslint/eslintrc" "^3.3.1" - "@eslint/js" "9.36.0" - "@eslint/plugin-kit" "^0.3.5" + "@eslint/js" "9.38.0" + "@eslint/plugin-kit" "^0.4.0" "@humanfs/node" "^0.16.6" "@humanwhocodes/module-importer" "^1.0.1" "@humanwhocodes/retry" "^0.4.2" "@types/estree" "^1.0.6" - "@types/json-schema" "^7.0.15" ajv "^6.12.4" chalk "^4.0.0" cross-spawn "^7.0.6" From 9817bdf7d17432d45a8bd8fd4eabafd1438a308e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 20 Oct 2025 03:39:00 +0000 Subject: [PATCH 22/28] Update dependency posthog-js to v1.276.0 --- yarn.lock | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/yarn.lock b/yarn.lock index 7cea636b..390b5bd4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2183,6 +2183,11 @@ resolved "https://registry.yarnpkg.com/@posthog/core/-/core-1.2.2.tgz#63382e2e208f501b2c22246ccaae55052f77ac1d" integrity sha512-f16Ozx6LIigRG+HsJdt+7kgSxZTHeX5f1JlCGKI1lXcvlZgfsCR338FuMI2QRYXGl+jg/vYFzGOTQBxl90lnBg== +"@posthog/core@1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@posthog/core/-/core-1.3.0.tgz#59f11c10485f61bba45c89d53554e41c7f6c9e3e" + integrity sha512-hxLL8kZNHH098geedcxCz8y6xojkNYbmJEW+1vFXsmPcExyCXIUUJ/34X6xa9GcprKxd0Wsx3vfJQLQX4iVPhw== + "@posthog/nextjs-config@^1.0.2": version "1.3.1" resolved "https://registry.yarnpkg.com/@posthog/nextjs-config/-/nextjs-config-1.3.1.tgz#cc934e18cb5327cd8176cfe85b4ef3d907577bb8" @@ -3562,9 +3567,9 @@ core-js-compat@^3.40.0: browserslist "^4.24.4" core-js@^3.38.1: - version "3.45.1" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.45.1.tgz#5810e04a1b4e9bc5ddaa4dd12e702ff67300634d" - integrity sha512-L4NPsJlCfZsPeXukyzHFlg/i7IIVwHSItR0wg0FLNqYClJ4MQYTYLbC7EkjKYRLZF2iof2MUgN0EGy7MdQFChg== + version "3.46.0" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.46.0.tgz#323a092b96381a9184d0cd49ee9083b2f93373bb" + integrity sha512-vDMm9B0xnqqZ8uSBpZ8sNtRtOdmfShrvT6h2TuQGLs0Is+cR0DYbj/KWP6ALVNbWPpqA/qPLoOuppJN07humpA== cosmiconfig@^7.0.0: version "7.1.0" @@ -5675,11 +5680,11 @@ postcss@^8.4.47: source-map-js "^1.2.1" posthog-js@^1.257.0: - version "1.268.9" - resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.268.9.tgz#08cced88984b14353ca12f2a7313bdd40b3158f3" - integrity sha512-ejK5/i0TUQ8I1SzaIn7xWNf5TzOjWquawpgjKit8DyucD3Z1yf7LTMtgCYZN8oRx9VjiPcP34fSk8YsWQmmkTQ== + version "1.276.0" + resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.276.0.tgz#3605e99dd42e69b772fc43c661c064edc3693aa1" + integrity sha512-FYZE1037LrAoKKeUU0pUL7u8WwNK2BVeg5TFApwquVPUdj9h7u5Z077A313hPN19Ar+7Y+VHxqYqdHc4VNsVgw== dependencies: - "@posthog/core" "1.2.2" + "@posthog/core" "1.3.0" core-js "^3.38.1" fflate "^0.4.8" preact "^10.19.3" From d986857786b622a04a2cf08c9175388f1547b416 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 20 Oct 2025 03:39:09 +0000 Subject: [PATCH 23/28] Update react monorepo --- package.json | 4 ++-- yarn.lock | 38 +++++++++++++++++++------------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/package.json b/package.json index 68ffa747..af63ace4 100644 --- a/package.json +++ b/package.json @@ -36,8 +36,8 @@ "@tanstack/react-query": "^5.66.0", "@types/luxon": "^3.3.2", "@types/node": "^24.1.0", - "@types/react": "19.1.16", - "@types/react-dom": "19.1.9", + "@types/react": "19.2.2", + "@types/react-dom": "19.2.2", "@vercel/analytics": "^1.5.0", "@vercel/speed-insights": "^1.2.0", "autoprefixer": "^10.4.20", diff --git a/yarn.lock b/yarn.lock index 7cea636b..41eee824 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2870,20 +2870,20 @@ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.15.tgz#e6e5a86d602beaca71ce5163fadf5f95d70931c7" integrity sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw== -"@types/react-dom@19.1.9": - version "19.1.9" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.1.9.tgz#5ab695fce1e804184767932365ae6569c11b4b4b" - integrity sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ== +"@types/react-dom@19.2.2": + version "19.2.2" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.2.2.tgz#a4cc874797b7ddc9cb180ef0d5dc23f596fc2332" + integrity sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw== "@types/react-transition-group@^4.4.12": version "4.4.12" resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.12.tgz#b5d76568485b02a307238270bfe96cb51ee2a044" integrity sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w== -"@types/react@19.1.16": - version "19.1.16" - resolved "https://registry.yarnpkg.com/@types/react/-/react-19.1.16.tgz#6329b1c17a5de624439eae673f86a2c5569a42be" - integrity sha512-WBM/nDbEZmDUORKnh5i1bTnAz6vTohUf9b8esSMu+b24+srbaxa04UbJgWx78CVfNXA20sNu0odEIluZDFdCog== +"@types/react@19.2.2": + version "19.2.2" + resolved "https://registry.yarnpkg.com/@types/react/-/react-19.2.2.tgz#ba123a75d4c2a51158697160a4ea2ff70aa6bf36" + integrity sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA== dependencies: csstype "^3.0.2" @@ -5765,11 +5765,11 @@ react-card-flip@^1.2.3: integrity sha512-yb8+yyeTf5UVlZ/FC78XDgxYeWhgA0W28OXNB31LjiFeY1Y2kFhLP7bFiED4KNaRWKjaafT74G38XU53a6eIuw== react-dom@^19.0.0: - version "19.1.1" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.1.1.tgz#2daa9ff7f3ae384aeb30e76d5ee38c046dc89893" - integrity sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw== + version "19.2.0" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.2.0.tgz#00ed1e959c365e9a9d48f8918377465466ec3af8" + integrity sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ== dependencies: - scheduler "^0.26.0" + scheduler "^0.27.0" react-hook-form@^7.54.2: version "7.63.0" @@ -5854,9 +5854,9 @@ react-transition-group@^4.4.5: prop-types "^15.6.2" react@^19.0.0: - version "19.1.1" - resolved "https://registry.yarnpkg.com/react/-/react-19.1.1.tgz#06d9149ec5e083a67f9a1e39ce97b06a03b644af" - integrity sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ== + version "19.2.0" + resolved "https://registry.yarnpkg.com/react/-/react-19.2.0.tgz#d33dd1721698f4376ae57a54098cb47fc75d93a5" + integrity sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ== read-cache@^1.0.0: version "1.0.0" @@ -6067,10 +6067,10 @@ safe-regex-test@^1.0.3, safe-regex-test@^1.1.0: es-errors "^1.3.0" is-regex "^1.2.1" -scheduler@^0.26.0: - version "0.26.0" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.26.0.tgz#4ce8a8c2a2095f13ea11bf9a445be50c555d6337" - integrity sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA== +scheduler@^0.27.0: + version "0.27.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.27.0.tgz#0c4ef82d67d1e5c1e359e8fc76d3a87f045fe5bd" + integrity sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q== semver@^6.3.1: version "6.3.1" From 417544359b9617d4022cfa138c5cb716a7c0e57b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 20 Oct 2025 04:09:33 +0000 Subject: [PATCH 24/28] Update dependency @posthog/nextjs-config to v1.3.4 --- yarn.lock | 40 +++++++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/yarn.lock b/yarn.lock index d812f6ce..a5a8f57d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2169,12 +2169,12 @@ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f" integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== -"@posthog/cli@^0.4.7": - version "0.4.8" - resolved "https://registry.yarnpkg.com/@posthog/cli/-/cli-0.4.8.tgz#d6d6ec14bfbde4a637785ffae9059d55ec3ea333" - integrity sha512-LdPMD9vppl8TOMeG+sYsKDgs3dK8cA8vvgG65i0DrXlCPt2+lpRINtQJ+M7Bm8VZFUpzMieNOl+/o7axwxwXlw== +"@posthog/cli@^0.5.1": + version "0.5.1" + resolved "https://registry.yarnpkg.com/@posthog/cli/-/cli-0.5.1.tgz#a7f2110466bcec78738d9c69b34cec463b0303f2" + integrity sha512-1a3zcLyP7fJp+IJ+jmodZ9poaQwGXJuz1/N9vEquudHqXi8cnhPy4OrbBoY3y1wZN4doYTwGqbfbts0662OUEw== dependencies: - axios "^1.10.0" + axios "^1.11.0" axios-proxy-builder "^0.1.2" console.table "^0.10.0" detect-libc "^2.0.4" @@ -2191,11 +2191,12 @@ integrity sha512-hxLL8kZNHH098geedcxCz8y6xojkNYbmJEW+1vFXsmPcExyCXIUUJ/34X6xa9GcprKxd0Wsx3vfJQLQX4iVPhw== "@posthog/nextjs-config@^1.0.2": - version "1.3.1" - resolved "https://registry.yarnpkg.com/@posthog/nextjs-config/-/nextjs-config-1.3.1.tgz#cc934e18cb5327cd8176cfe85b4ef3d907577bb8" - integrity sha512-0CKzXX7Oh9tgFdqTQMq9FUMDIBhu0td9NYsyU20y+pu4meh9xSCcJI7GIcwxRTBoDIJvR//YU98niMlnu+uObQ== + version "1.3.4" + resolved "https://registry.yarnpkg.com/@posthog/nextjs-config/-/nextjs-config-1.3.4.tgz#2b1a80cbdba09ecbde10502000be54cf8a56632d" + integrity sha512-cf1XIkK28y/5qL/as/IutH0EwysWy1gKdGM/xETZ0l25jTTN9wASx1ucP/BeE3dOYAVF039zlU9WNCFbIxW4Uw== dependencies: - "@posthog/cli" "^0.4.7" + "@posthog/cli" "^0.5.1" + "@posthog/core" "1.3.0" semver "^7.7.2" "@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": @@ -3261,7 +3262,7 @@ axios-proxy-builder@^0.1.2: dependencies: tunnel "^0.0.6" -axios@^1.10.0: +axios@^1.11.0: version "1.12.2" resolved "https://registry.yarnpkg.com/axios/-/axios-1.12.2.tgz#6c307390136cf7a2278d09cec63b136dfc6e6da7" integrity sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw== @@ -3750,7 +3751,12 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== -detect-libc@^2.0.4, detect-libc@^2.1.0: +detect-libc@^2.0.4: + version "2.1.2" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.1.2.tgz#689c5dcdc1900ef5583a4cb9f6d7b473742074ad" + integrity sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ== + +detect-libc@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.1.0.tgz#3ca811f60a7b504b0480e5008adacc660b0b8c4f" integrity sha512-vEtk+OcP7VBRtQZ1EJ3bdgzSfBjgnEalLTp5zjJrS+2Z1w2KZly4SBdac/WDU3hhsNAZ9E8SC96ME4Ey8MZ7cg== @@ -5179,9 +5185,9 @@ lru-cache@^10.2.0: integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== lru-cache@^11.0.0: - version "11.2.1" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-11.2.1.tgz#d426ac471521729c6c1acda5f7a633eadaa28db2" - integrity sha512-r8LA6i4LP4EeWOhqBaZZjDWwehd1xUJPCJd9Sv300H0ZmcUER4+JPh7bqqZeqs1o5pgtgvXm+d9UGrB5zZGDiQ== + version "11.2.2" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-11.2.2.tgz#40fd37edffcfae4b2940379c0722dc6eeaa75f24" + integrity sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg== lru-cache@^5.1.1: version "5.1.1" @@ -6088,9 +6094,9 @@ semver@^7.6.0, semver@^7.6.3: integrity sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA== semver@^7.7.2: - version "7.7.2" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58" - integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA== + version "7.7.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.3.tgz#4b5f4143d007633a8dc671cd0a6ef9147b8bb946" + integrity sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q== set-function-length@^1.2.2: version "1.2.2" From f639cc64c7b233b44eca3c2ca1bc34a08f35e490 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 20 Oct 2025 04:09:44 +0000 Subject: [PATCH 25/28] Update dependency @tanstack/react-query to v5.90.5 --- yarn.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/yarn.lock b/yarn.lock index d812f6ce..32786569 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2797,17 +2797,17 @@ dependencies: mini-svg-data-uri "^1.2.3" -"@tanstack/query-core@5.90.2": - version "5.90.2" - resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.90.2.tgz#ac5d0d0f19a38071db2d21d758b5c35a85d9c1d8" - integrity sha512-k/TcR3YalnzibscALLwxeiLUub6jN5EDLwKDiO7q5f4ICEoptJ+n9+7vcEFy5/x/i6Q+Lb/tXrsKCggf5uQJXQ== +"@tanstack/query-core@5.90.5": + version "5.90.5" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.90.5.tgz#0175f9f517514906db8ab379589ed3f96694ecc4" + integrity sha512-wLamYp7FaDq6ZnNehypKI5fNvxHPfTYylE0m/ZpuuzJfJqhR5Pxg9gvGBHZx4n7J+V5Rg5mZxHHTlv25Zt5u+w== "@tanstack/react-query@^5.66.0": - version "5.90.2" - resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.90.2.tgz#2f045931b7d44bef02c5261fedba75ef1a418726" - integrity sha512-CLABiR+h5PYfOWr/z+vWFt5VsOA2ekQeRQBFSKlcoW6Ndx/f8rfyVmq4LbgOM4GG2qtxAxjLYLOpCNTYm4uKzw== + version "5.90.5" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.90.5.tgz#545e61282c787bd87ac5785da9a4943462f78ef6" + integrity sha512-pN+8UWpxZkEJ/Rnnj2v2Sxpx1WFlaa9L6a4UO89p6tTQbeo+m0MS8oYDjbggrR8QcTyjKoYWKS3xJQGr3ExT8Q== dependencies: - "@tanstack/query-core" "5.90.2" + "@tanstack/query-core" "5.90.5" "@tanstack/react-virtual@^3.13.9": version "3.13.12" From c10209c1918e2657b0226bfa5a0089ec3bd687b8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 20 Oct 2025 04:09:53 +0000 Subject: [PATCH 26/28] Update dependency framer-motion to v12.23.24 --- yarn.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/yarn.lock b/yarn.lock index d812f6ce..89d39290 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4439,11 +4439,11 @@ fraction.js@^4.3.7: integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew== framer-motion@^12.0.0: - version "12.23.22" - resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-12.23.22.tgz#798333fdcc5cd65fabfc450985b7236bbdbf5d64" - integrity sha512-ZgGvdxXCw55ZYvhoZChTlG6pUuehecgvEAJz0BHoC5pQKW1EC5xf1Mul1ej5+ai+pVY0pylyFfdl45qnM1/GsA== + version "12.23.24" + resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-12.23.24.tgz#4895b67e880bd2b1089e61fbaa32ae802fc24b8c" + integrity sha512-HMi5HRoRCTou+3fb3h9oTLyJGBxHfW+HnNE25tAXOvVx/IvwMHK0cx7IR4a2ZU6sh3IX1Z+4ts32PcYBOqka8w== dependencies: - motion-dom "^12.23.21" + motion-dom "^12.23.23" motion-utils "^12.23.6" tslib "^2.4.0" @@ -5288,10 +5288,10 @@ minimist@^1.2.0, minimist@^1.2.6, minimist@^1.2.8: resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== -motion-dom@^12.23.21: - version "12.23.21" - resolved "https://registry.yarnpkg.com/motion-dom/-/motion-dom-12.23.21.tgz#1efe2d1bdda31875f0ee56d06f02be7b562a0bdd" - integrity sha512-5xDXx/AbhrfgsQmSE7YESMn4Dpo6x5/DTZ4Iyy4xqDvVHWvFVoV+V2Ri2S/ksx+D40wrZ7gPYiMWshkdoqNgNQ== +motion-dom@^12.23.23: + version "12.23.23" + resolved "https://registry.yarnpkg.com/motion-dom/-/motion-dom-12.23.23.tgz#8f874333ea1a04ee3a89eb928f518b463d589e0e" + integrity sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA== dependencies: motion-utils "^12.23.6" From 27be042001373fea2fc0981cd4bc0fc0a80afb19 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 20 Oct 2025 04:10:05 +0000 Subject: [PATCH 27/28] Update dependency lint-staged to v16.2.4 --- yarn.lock | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/yarn.lock b/yarn.lock index d812f6ce..3b4fab38 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3855,9 +3855,9 @@ electron-to-chromium@^1.5.73: integrity sha512-Lv65Btwv9W4J9pyODI6EWpdnhfvrve/us5h1WspW8B2Fb0366REPtY3hX7ounk1CkV/TBjWCEvCBBbYbmV0qCA== emoji-regex@^10.3.0: - version "10.5.0" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.5.0.tgz#be23498b9e39db476226d8e81e467f39aca26b78" - integrity sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg== + version "10.6.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.6.0.tgz#bf3d6e8f7f8fd22a65d9703475bc0147357a6b0d" + integrity sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A== emoji-regex@^8.0.0: version "8.0.0" @@ -5082,14 +5082,14 @@ lines-and-columns@^1.1.6: integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== lint-staged@^16.0.0: - version "16.2.3" - resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-16.2.3.tgz#790866221d75602510507b5be40b2c7963715960" - integrity sha512-1OnJEESB9zZqsp61XHH2fvpS1es3hRCxMplF/AJUDa8Ho8VrscYDIuxGrj3m8KPXbcWZ8fT9XTMUhEQmOVKpKw== + version "16.2.4" + resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-16.2.4.tgz#1f166370e32d9b7eb10583e86d86e1117f7ab489" + integrity sha512-Pkyr/wd90oAyXk98i/2KwfkIhoYQUMtss769FIT9hFM5ogYZwrk+GRE46yKXSg2ZGhcJ1p38Gf5gmI5Ohjg2yg== dependencies: commander "^14.0.1" listr2 "^9.0.4" micromatch "^4.0.8" - nano-spawn "^1.0.3" + nano-spawn "^2.0.0" pidtree "^0.6.0" string-argv "^0.3.2" yaml "^2.8.1" @@ -5314,10 +5314,10 @@ mz@^2.7.0: object-assign "^4.0.1" thenify-all "^1.0.0" -nano-spawn@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/nano-spawn/-/nano-spawn-1.0.3.tgz#ef8d89a275eebc8657e67b95fc312a6527a05b8d" - integrity sha512-jtpsQDetTnvS2Ts1fiRdci5rx0VYws5jGyC+4IYOTnIQ/wwdf6JdomlHBwqC3bJYOvaKu0C2GSZ1A60anrYpaA== +nano-spawn@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/nano-spawn/-/nano-spawn-2.0.0.tgz#f1250434c09ae18870d4f729fc54b406cf85a3e1" + integrity sha512-tacvGzUY5o2D8CBh2rrwxyNojUsZNU2zjNTzKQrkgGJQTbGAfArVWXSKMBokBeeg6C7OLRGUEyoFlYbfeWQIqw== nanoid@^3.1.23, nanoid@^3.3.6, nanoid@^3.3.8: version "3.3.11" From 4c90cfd1acd1f7479a642348b7d8faa5101f1910 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 20 Oct 2025 04:10:18 +0000 Subject: [PATCH 28/28] Update nextjs monorepo to v15.5.6 --- package.json | 4 +- yarn.lock | 136 +++++++++++++++++++++++++-------------------------- 2 files changed, 70 insertions(+), 70 deletions(-) diff --git a/package.json b/package.json index 82ddc8b2..df1899b0 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "clsx": "^2.1.1", "cmdk": "^1.1.1", "eslint": "9.38.0", - "eslint-config-next": "15.5.4", + "eslint-config-next": "15.5.6", "firebase": "^12.0.0", "form-data": "^4.0.0", "framer-motion": "^12.0.0", @@ -54,7 +54,7 @@ "lint-staged": "^16.0.0", "lucide-react": "^0.544.0", "luxon": "^3.4.2", - "next": "15.5.4", + "next": "15.5.6", "posthog-js": "^1.257.0", "posthog-node": "^5.5.1", "prettier": "^3.3.3", diff --git a/yarn.lock b/yarn.lock index d812f6ce..a0693d0e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1977,62 +1977,62 @@ "@emnapi/runtime" "^1.5.0" "@tybys/wasm-util" "^0.10.1" -"@next/env@15.5.4": - version "15.5.4" - resolved "https://registry.yarnpkg.com/@next/env/-/env-15.5.4.tgz#1d4aa6b238662d9cd95aea356b149b6f73061f95" - integrity sha512-27SQhYp5QryzIT5uO8hq99C69eLQ7qkzkDPsk3N+GuS2XgOgoYEeOav7Pf8Tn4drECOVDsDg8oj+/DVy8qQL2A== +"@next/env@15.5.6": + version "15.5.6" + resolved "https://registry.yarnpkg.com/@next/env/-/env-15.5.6.tgz#7009d88d419a36a4ba9e110c151604444744a74d" + integrity sha512-3qBGRW+sCGzgbpc5TS1a0p7eNxnOarGVQhZxfvTdnV0gFI61lX7QNtQ4V1TSREctXzYn5NetbUsLvyqwLFJM6Q== -"@next/eslint-plugin-next@15.5.4": - version "15.5.4" - resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-15.5.4.tgz#e7af86b7197a26e8a9d3784d1e365a2eb9298f5b" - integrity sha512-SR1vhXNNg16T4zffhJ4TS7Xn7eq4NfKfcOsRwea7RIAHrjRpI9ALYbamqIJqkAhowLlERffiwk0FMvTLNdnVtw== +"@next/eslint-plugin-next@15.5.6": + version "15.5.6" + resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-15.5.6.tgz#8d08f11cb98a8401ff64746cb3035948ae1d1f61" + integrity sha512-YxDvsT2fwy1j5gMqk3ppXlsgDopHnkM4BoxSVASbvvgh5zgsK8lvWerDzPip8k3WVzsTZ1O7A7si1KNfN4OZfQ== dependencies: fast-glob "3.3.1" -"@next/swc-darwin-arm64@15.5.4": - version "15.5.4" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.4.tgz#80cba1bec831d4b01fd03cbc48dfb7050775e5ee" - integrity sha512-nopqz+Ov6uvorej8ndRX6HlxCYWCO3AHLfKK2TYvxoSB2scETOcfm/HSS3piPqc3A+MUgyHoqE6je4wnkjfrOA== - -"@next/swc-darwin-x64@15.5.4": - version "15.5.4" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.4.tgz#d5408b19298f40da2b3dc9c2f9d1063ad98bd626" - integrity sha512-QOTCFq8b09ghfjRJKfb68kU9k2K+2wsC4A67psOiMn849K9ZXgCSRQr0oVHfmKnoqCbEmQWG1f2h1T2vtJJ9mA== - -"@next/swc-linux-arm64-gnu@15.5.4": - version "15.5.4" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.4.tgz#3b6b389bb4a1c9728a14afbbd59d2366ccd80b55" - integrity sha512-eRD5zkts6jS3VfE/J0Kt1VxdFqTnMc3QgO5lFE5GKN3KDI/uUpSyK3CjQHmfEkYR4wCOl0R0XrsjpxfWEA++XA== - -"@next/swc-linux-arm64-musl@15.5.4": - version "15.5.4" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.4.tgz#956127ecdfd56cda535af4651eed72a3b7270971" - integrity sha512-TOK7iTxmXFc45UrtKqWdZ1shfxuL4tnVAOuuJK4S88rX3oyVV4ZkLjtMT85wQkfBrOOvU55aLty+MV8xmcJR8A== - -"@next/swc-linux-x64-gnu@15.5.4": - version "15.5.4" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.4.tgz#9386de65e86c0b34ef19e14f0ffbd4328a08d5e6" - integrity sha512-7HKolaj+481FSW/5lL0BcTkA4Ueam9SPYWyN/ib/WGAFZf0DGAN8frNpNZYFHtM4ZstrHZS3LY3vrwlIQfsiMA== - -"@next/swc-linux-x64-musl@15.5.4": - version "15.5.4" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.4.tgz#c9094e5479b58c89d35b465f165b69be68de5a75" - integrity sha512-nlQQ6nfgN0nCO/KuyEUwwOdwQIGjOs4WNMjEUtpIQJPR2NUfmGpW2wkJln1d4nJ7oUzd1g4GivH5GoEPBgfsdw== - -"@next/swc-win32-arm64-msvc@15.5.4": - version "15.5.4" - resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.4.tgz#e83ca6b5ce9499bde5a4f3351cf74dc9e92cc83e" - integrity sha512-PcR2bN7FlM32XM6eumklmyWLLbu2vs+D7nJX8OAIoWy69Kef8mfiN4e8TUv2KohprwifdpFKPzIP1njuCjD0YA== - -"@next/swc-win32-x64-msvc@15.5.4": - version "15.5.4" - resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.4.tgz#5b5baf1bcb0ecba70d1768a0c8be59dfdcb2f111" - integrity sha512-1ur2tSHZj8Px/KMAthmuI9FMp/YFusMMGoRNJaRZMOlSkgvLjzosSdQI0cJAKogdHl3qXUQKL9MGaYvKwA7DXg== +"@next/swc-darwin-arm64@15.5.6": + version "15.5.6" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.6.tgz#f80d3fe536f29f3217ca07d031f7b43862234059" + integrity sha512-ES3nRz7N+L5Umz4KoGfZ4XX6gwHplwPhioVRc25+QNsDa7RtUF/z8wJcbuQ2Tffm5RZwuN2A063eapoJ1u4nPg== + +"@next/swc-darwin-x64@15.5.6": + version "15.5.6" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.6.tgz#289334478617318a0d8d9f1f6661a15952f4e4ab" + integrity sha512-JIGcytAyk9LQp2/nuVZPAtj8uaJ/zZhsKOASTjxDug0SPU9LAM3wy6nPU735M1OqacR4U20LHVF5v5Wnl9ptTA== + +"@next/swc-linux-arm64-gnu@15.5.6": + version "15.5.6" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.6.tgz#efdd993cd9ad88b82c948c8e518e045566dd2f98" + integrity sha512-qvz4SVKQ0P3/Im9zcS2RmfFL/UCQnsJKJwQSkissbngnB/12c6bZTCB0gHTexz1s6d/mD0+egPKXAIRFVS7hQg== + +"@next/swc-linux-arm64-musl@15.5.6": + version "15.5.6" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.6.tgz#a45185126faf69eb65a017bd2c015ad7e86f5c84" + integrity sha512-FsbGVw3SJz1hZlvnWD+T6GFgV9/NYDeLTNQB2MXoPN5u9VA9OEDy6fJEfePfsUKAhJufFbZLgp0cPxMuV6SV0w== + +"@next/swc-linux-x64-gnu@15.5.6": + version "15.5.6" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.6.tgz#8174dc8e03a1f7df292bead360f83c53f8dd8b73" + integrity sha512-3QnHGFWlnvAgyxFxt2Ny8PTpXtQD7kVEeaFat5oPAHHI192WKYB+VIKZijtHLGdBBvc16tiAkPTDmQNOQ0dyrA== + +"@next/swc-linux-x64-musl@15.5.6": + version "15.5.6" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.6.tgz#6d0776d81c5bd6a1780e6c39f32d7ef172900635" + integrity sha512-OsGX148sL+TqMK9YFaPFPoIaJKbFJJxFzkXZljIgA9hjMjdruKht6xDCEv1HLtlLNfkx3c5w2GLKhj7veBQizQ== + +"@next/swc-win32-arm64-msvc@15.5.6": + version "15.5.6" + resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.6.tgz#4b63d511b5c41278a48168fccb89cf00912411af" + integrity sha512-ONOMrqWxdzXDJNh2n60H6gGyKed42Ieu6UTVPZteXpuKbLZTH4G4eBMsr5qWgOBA+s7F+uB4OJbZnrkEDnZ5Fg== + +"@next/swc-win32-x64-msvc@15.5.6": + version "15.5.6" + resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.6.tgz#9ed5e84bd85009625dd35c444668d13061452a50" + integrity sha512-pxK4VIjFRx1MY92UycLOOw7dTdvccWsNETQ0kDHkBlcFH1GrTLUjSiHU1ohrznnux6TqRHgv5oflhfIWZwVROQ== "@next/third-parties@^15.3.1": - version "15.5.4" - resolved "https://registry.yarnpkg.com/@next/third-parties/-/third-parties-15.5.4.tgz#a6eb17336d707d92ca88709939f0439cf0841fa0" - integrity sha512-l3T1M/EA32phPzZx+gkQAWOF3E5iAULL1nX4Ej0JZQOXaBwwJzb/rd2uefr5TAshJj/+HjjwmdFu7olXudvgVg== + version "15.5.6" + resolved "https://registry.yarnpkg.com/@next/third-parties/-/third-parties-15.5.6.tgz#6b7f49702e969331b69c3105902c9f443d496c2c" + integrity sha512-B1BLvEi7edGERNN0njxpiqbqkp3zAZ69eJ5C0vwj/XINRzcC25b9MCqxbSHq094d306H65UnlhEkBv+a8c74iA== dependencies: third-party-capital "1.0.20" @@ -4018,12 +4018,12 @@ escape-string-regexp@^4.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== -eslint-config-next@15.5.4: - version "15.5.4" - resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-15.5.4.tgz#b15f15aa9030098fc5a48ea5bb3c2bba6a531f70" - integrity sha512-BzgVVuT3kfJes8i2GHenC1SRJ+W3BTML11lAOYFOOPzrk2xp66jBOAGEFRw+3LkYCln5UzvFsLhojrshb5Zfaw== +eslint-config-next@15.5.6: + version "15.5.6" + resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-15.5.6.tgz#e054f4f76d866b37848c1a142726e08c9a3a66c0" + integrity sha512-cGr3VQlPsZBEv8rtYp4BpG1KNXDqGvPo9VC1iaCgIA11OfziC/vczng+TnAS3WpRIR3Q5ye/6yl+CRUuZ1fPGg== dependencies: - "@next/eslint-plugin-next" "15.5.4" + "@next/eslint-plugin-next" "15.5.6" "@rushstack/eslint-patch" "^1.10.3" "@typescript-eslint/eslint-plugin" "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0" "@typescript-eslint/parser" "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0" @@ -5334,25 +5334,25 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== -next@15.5.4: - version "15.5.4" - resolved "https://registry.yarnpkg.com/next/-/next-15.5.4.tgz#e7412c805c0b686ceaf294de703b7c9be59a4081" - integrity sha512-xH4Yjhb82sFYQfY3vbkJfgSDgXvBB6a8xPs9i35k6oZJRoQRihZH+4s9Yo2qsWpzBmZ3lPXaJ2KPXLfkvW4LnA== +next@15.5.6: + version "15.5.6" + resolved "https://registry.yarnpkg.com/next/-/next-15.5.6.tgz#16d9d1e9ba2e8caf82ba15e060a12286cd25db30" + integrity sha512-zTxsnI3LQo3c9HSdSf91O1jMNsEzIXDShXd4wVdg9y5shwLqBXi4ZtUUJyB86KGVSJLZx0PFONvO54aheGX8QQ== dependencies: - "@next/env" "15.5.4" + "@next/env" "15.5.6" "@swc/helpers" "0.5.15" caniuse-lite "^1.0.30001579" postcss "8.4.31" styled-jsx "5.1.6" optionalDependencies: - "@next/swc-darwin-arm64" "15.5.4" - "@next/swc-darwin-x64" "15.5.4" - "@next/swc-linux-arm64-gnu" "15.5.4" - "@next/swc-linux-arm64-musl" "15.5.4" - "@next/swc-linux-x64-gnu" "15.5.4" - "@next/swc-linux-x64-musl" "15.5.4" - "@next/swc-win32-arm64-msvc" "15.5.4" - "@next/swc-win32-x64-msvc" "15.5.4" + "@next/swc-darwin-arm64" "15.5.6" + "@next/swc-darwin-x64" "15.5.6" + "@next/swc-linux-arm64-gnu" "15.5.6" + "@next/swc-linux-arm64-musl" "15.5.6" + "@next/swc-linux-x64-gnu" "15.5.6" + "@next/swc-linux-x64-musl" "15.5.6" + "@next/swc-win32-arm64-msvc" "15.5.6" + "@next/swc-win32-x64-msvc" "15.5.6" sharp "^0.34.3" no-case@^3.0.4: