diff --git a/src/APIFunctions/SCEvents.js b/src/APIFunctions/SCEvents.js index 3e2cd82bd..f4b1a3a9d 100644 --- a/src/APIFunctions/SCEvents.js +++ b/src/APIFunctions/SCEvents.js @@ -20,7 +20,7 @@ export async function getAllSCEvents(token) { status.error = true; } } catch (err) { - status.responseData = err; + status.responseData = { error: err?.message || 'Failed to connect to SCEvents API' }; status.error = true; } return status; @@ -44,7 +44,7 @@ export async function getEventByID(id, token) { } } catch (err) { status.error = true; - status.responseData = err; + status.responseData = { error: err?.message || 'Failed to connect to SCEvents API' }; } return status; } @@ -64,7 +64,7 @@ export async function getEventAttendanceSummary(id, token) { } } catch (err) { status.error = true; - status.responseData = err; + status.responseData = { error: err?.message || 'Failed to connect to SCEvents API' }; } return status; } @@ -87,7 +87,7 @@ export async function createSCEvent(token, eventBody) { } } catch (err) { status.error = true; - status.responseData = err; + status.responseData = { error: err?.message || 'Failed to connect to SCEvents API' }; } return status; } @@ -110,14 +110,57 @@ export async function updateSCEvent(id, token, eventUpdates) { } } catch (err) { status.error = true; - status.responseData = err; + status.responseData = { error: err?.message || 'Failed to connect to SCEvents API' }; } return status; } -export async function registerForSCEvent(eventId, token, payload) { +export async function getEventRegistrations(eventId, token, { limit = 50, offset = 0 } = {}) { + const status = new ApiResponse(); + try { + const url = new URL(`${SCEVENTS_API_URL}/events/${eventId}/registrations`); + url.searchParams.set('limit', String(limit)); + url.searchParams.set('offset', String(offset)); + + const res = await fetch(url.href, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + const result = await res.json(); + status.responseData = result; + if (!res.ok) { + status.error = true; + } + } catch (err) { + status.error = true; + status.responseData = { error: err?.message || 'Failed to connect to SCEvents API' }; + } + return status; +} + +export async function getEventRegistrationByRequestId(eventId, requestId, token) { const status = new ApiResponse(); + try { + const res = await fetch(`${SCEVENTS_API_URL}/events/${eventId}/registrations/${requestId}`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + const result = await res.json(); + status.responseData = result; + if (!res.ok) { + status.error = true; + } + } catch (err) { + status.error = true; + status.responseData = { error: err?.message || 'Failed to connect to SCEvents API' }; + } + return status; +} +export async function registerForEvent(eventId, token, payload) { + const status = new ApiResponse(); try { const res = await fetch(`${SCEVENTS_API_URL}/events/${eventId}/register`, { method: 'POST', @@ -127,18 +170,39 @@ export async function registerForSCEvent(eventId, token, payload) { }, body: JSON.stringify(payload), }); + const result = await res.json(); + status.responseData = result; + if (!res.ok) { + status.error = true; + } + } catch (err) { + status.error = true; + status.responseData = { error: err?.message || 'Failed to connect to SCEvents API' }; + } + return status; +} - const body = await res.json(); - status.responseData = body; +export async function registerForSCEvent(eventId, token, payload) { + return registerForEvent(eventId, token, payload); +} +export async function getMyEventRegistrationState(eventId, token) { + const status = new ApiResponse(); + try { + const res = await fetch(`${SCEVENTS_API_URL}/events/${eventId}/registration/me`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + const result = await res.json(); + status.responseData = result; if (!res.ok) { status.error = true; } } catch (err) { status.error = true; - status.responseData = err; + status.responseData = { error: err?.message || 'Failed to connect to SCEvents API' }; } - return status; } diff --git a/src/Components/Navbar/AdminNavbar.js b/src/Components/Navbar/AdminNavbar.js index 679c54e9e..3b15bd7d5 100644 --- a/src/Components/Navbar/AdminNavbar.js +++ b/src/Components/Navbar/AdminNavbar.js @@ -1,6 +1,7 @@ import React from 'react'; import { useSCE } from '../context/SceContext'; import { membershipState } from '../../Enums'; +import config from '../../config/config.json'; export default function UserNavBar(props) { const { user, setAuthenticated } = useSCE(); @@ -43,15 +44,28 @@ export default function UserNavBar(props) { ]; const sceventsAdminNavLinks = []; - sceventsAdminNavLinks.push({ - title: 'Events', - route: '/events', - icon: ( - - ), - }); + if (config.SCEvents?.ENABLED) { + sceventsAdminNavLinks.push({ + title: 'Events', + route: '/events', + icon: ( + + ), + }); + if (user?.accessLevel >= membershipState.OFFICER) { + sceventsAdminNavLinks.push({ + title: 'Create event', + route: '/events/create', + icon: ( + + ), + }); + } + } const adminLinks = [ { diff --git a/src/Pages/Events/CalendarView.js b/src/Pages/Events/CalendarView.js index 69dc9fc21..058f53dc4 100644 --- a/src/Pages/Events/CalendarView.js +++ b/src/Pages/Events/CalendarView.js @@ -1,6 +1,6 @@ import React, { useState, useMemo, useEffect, useRef } from 'react'; import { Link } from 'react-router-dom'; -import { getEventAttendanceSummary, joinWaitlistForSCEvent } from '../../APIFunctions/SCEvents'; +import { getEventAttendanceSummary, getMyEventRegistrationState, joinWaitlistForSCEvent } from '../../APIFunctions/SCEvents'; // ─── tiny helpers ──────────────────────────────────────────────────────────── @@ -275,8 +275,6 @@ function EventPopup({ event, onClose, isAdminView, user }) { const popupRef = useRef(null); const colors = pillColors(event, isAdminView); const badgeText = getBadgeText(event, isAdminView); - const eventId = event.id || event._id; - const authToken = user?.token || window.localStorage.getItem('jwtToken'); const userId = user?._id != null ? String(user._id) : ''; const [attendeeCount, setAttendeeCount] = useState(null); const [attendanceLoading, setAttendanceLoading] = useState(false); @@ -284,10 +282,14 @@ function EventPopup({ event, onClose, isAdminView, user }) { const [waitlistSubmitting, setWaitlistSubmitting] = useState(false); const [waitlistMessage, setWaitlistMessage] = useState(''); const [waitlistError, setWaitlistError] = useState(''); - const canEditEvent = - Array.isArray(event.admins) && - userId && - event.admins.includes(userId); + const eventAdmins = Array.isArray(event.admins) ? event.admins.map((id) => String(id)) : []; + const canManageEvent = + (eventAdmins.length > 0 && userId && eventAdmins.includes(userId)) + || (eventAdmins.length === 0 && (user?.accessLevel ?? 0) >= 4); + const [hasRegistered, setHasRegistered] = useState(false); + const [isCheckingRegistration, setIsCheckingRegistration] = useState(false); + const eventId = event?.id || event?._id; + const authToken = user?.token; const maxAttendees = Number(event.max_attendees); const hasCapacityLimit = Number.isFinite(maxAttendees) && maxAttendees > 0; const remainingSpots = @@ -297,7 +299,7 @@ function EventPopup({ event, onClose, isAdminView, user }) { const registrationCta = getRegistrationCta(event); const isFull = hasCapacityLimit && typeof remainingSpots === 'number' && remainingSpots <= 0; const shouldShowWaitlistJoin = - !canEditEvent && + !canManageEvent && event.status === 'published' && !registrationCta.disabled && isFull && @@ -311,6 +313,28 @@ function EventPopup({ event, onClose, isAdminView, user }) { return () => document.removeEventListener('keydown', onKey); }, [onClose]); + useEffect(() => { + async function fetchMyRegistrationState() { + if (!userId || canManageEvent || !user?.token || event.status !== 'published') { + setHasRegistered(false); + setIsCheckingRegistration(false); + return; + } + + setIsCheckingRegistration(true); + const eventID = event?.id || event?._id; + const response = await getMyEventRegistrationState(eventID, user.token); + if (!response.error) { + setHasRegistered(Boolean(response.responseData?.registered)); + } else { + setHasRegistered(false); + } + setIsCheckingRegistration(false); + } + + fetchMyRegistrationState(); + }, [canManageEvent, event, user?.token, userId]); + useEffect(() => { function onClickOutside(e) { if (popupRef.current && !popupRef.current.contains(e.target)) { @@ -386,12 +410,11 @@ function EventPopup({ event, onClose, isAdminView, user }) { setWaitlistMessage('Joined waitlist successfully.'); } - return ( -
- {attendeeCount} {attendeeCount === 1 ? 'person' : 'people'} registered + {event.max_attendees} spot{event.max_attendees !== 1 ? 's' : ''} available {event.waitlist_enabled && ( · waitlist available )}
)} - {hasCapacityLimit && !canEditEvent && typeof remainingSpots === 'number' && ( + {hasCapacityLimit && !canManageEvent && typeof remainingSpots === 'number' && ({remainingSpots} spot{remainingSpots !== 1 ? 's' : ''} left {event.waitlist_enabled && ( @@ -483,41 +506,49 @@ function EventPopup({ event, onClose, isAdminView, user }) {
)} - {hasCapacityLimit && !canEditEvent && attendanceLoading && ( + {hasCapacityLimit && !canManageEvent && attendanceLoading && (Loading live spots...
)} - {hasCapacityLimit && !canEditEvent && attendanceLoaded && typeof remainingSpots !== 'number' && ( + {hasCapacityLimit && !canManageEvent && attendanceLoaded && typeof remainingSpots !== 'number' && (Unable to load live spots
)} - - {canEditEvent && ( - - Edit event - + {canManageEvent && ( +{label}
+{value}
+Event ID: {id}
+Loading attendees...
} + {!isLoadingList && listError &&{listError}
} + + {!isLoadingList && !listError && ( +Click an attendee to open details
+No attendees found for this event yet.
+ ) : ( +Select an attendee to view answers.
} + {isLoadingDetail &&Loading attendee detail...
} + {!isLoadingDetail && detailError &&{detailError}
} + {!isLoadingDetail && !detailError && selectedAttendee && ( +Name: {selectedAttendee.registrant?.name || 'N/A'}
+Email: {selectedAttendee.registrant?.email || 'N/A'}
+Status: {selectedAttendee.status || 'N/A'}
+Submitted: {formatDateTime(selectedAttendee.created_at)}
+No answers submitted.
+ ) : ( + selectedAnswers.map(([fieldKey, value]) => ( +{fieldKey}
+