Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 74 additions & 10 deletions src/APIFunctions/SCEvents.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}
Expand All @@ -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;
}
Expand All @@ -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;
}
Expand All @@ -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',
Expand All @@ -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;
}

Expand Down
32 changes: 23 additions & 9 deletions src/Components/Navbar/AdminNavbar.js
Original file line number Diff line number Diff line change
@@ -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();
Expand Down Expand Up @@ -43,15 +44,28 @@ export default function UserNavBar(props) {
];

const sceventsAdminNavLinks = [];
sceventsAdminNavLinks.push({
title: 'Events',
route: '/events',
icon: (
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" className="w-6 h-6">
<path strokeLinecap="round" strokeLinejoin="round" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 0 1 2.25-2.25h13.5A2.25 2.25 0 0 1 21 7.5v11.25m-18 0A2.25 2.25 0 0 0 5.25 21h13.5a2.25 2.25 0 0 0 2.25-2.25m-18 0v-7.5A2.25 2.25 0 0 1 5.25 9h13.5a2.25 2.25 0 0 1 2.25 2.25v7.5" />
</svg>
),
});
if (config.SCEvents?.ENABLED) {
sceventsAdminNavLinks.push({
title: 'Events',
route: '/events',
icon: (
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" className="w-6 h-6">
<path strokeLinecap="round" strokeLinejoin="round" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 0 1 2.25-2.25h13.5A2.25 2.25 0 0 1 21 7.5v11.25m-18 0A2.25 2.25 0 0 0 5.25 21h13.5a2.25 2.25 0 0 0 2.25-2.25m-18 0v-7.5A2.25 2.25 0 0 1 5.25 9h13.5a2.25 2.25 0 0 1 2.25 2.25v7.5" />
</svg>
),
});
if (user?.accessLevel >= membershipState.OFFICER) {
sceventsAdminNavLinks.push({
title: 'Create event',
route: '/events/create',
icon: (
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" className="w-6 h-6">
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
),
});
}
}

const adminLinks = [
{
Expand Down
89 changes: 60 additions & 29 deletions src/Pages/Events/CalendarView.js
Original file line number Diff line number Diff line change
@@ -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 ────────────────────────────────────────────────────────────

Expand Down Expand Up @@ -275,19 +275,21 @@ 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);
const [attendanceLoaded, setAttendanceLoaded] = useState(false);
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 =
Expand All @@ -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 &&
Expand All @@ -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)) {
Expand Down Expand Up @@ -386,12 +410,11 @@ function EventPopup({ event, onClose, isAdminView, user }) {

setWaitlistMessage('Joined waitlist successfully.');
}

return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-slate-950/70 p-4">
<div className="fixed inset-0 z-50 flex items-center justify-center bg-slate-950/35 p-4">
<div
ref={popupRef}
className="relative w-full max-w-sm rounded-xl border border-slate-400/35 bg-slate-900 shadow-lg"
className="relative w-full max-w-sm rounded-xl border border-slate-400/35 bg-slate-900/95 shadow-lg"
role="dialog"
aria-modal="true"
aria-label={event.name}
Expand Down Expand Up @@ -465,16 +488,16 @@ function EventPopup({ event, onClose, isAdminView, user }) {
</div>

<div className="space-y-2 border-t border-slate-700/70 px-5 py-4">
{hasCapacityLimit && canEditEvent && typeof attendeeCount === 'number' && (
{event.max_attendees > 0 && (
<p className="text-center text-xs text-slate-400">
{attendeeCount} {attendeeCount === 1 ? 'person' : 'people'} registered
{event.max_attendees} spot{event.max_attendees !== 1 ? 's' : ''} available
{event.waitlist_enabled && (
<span className="ml-1 text-amber-300">· waitlist available</span>
)}
</p>
)}

{hasCapacityLimit && !canEditEvent && typeof remainingSpots === 'number' && (
{hasCapacityLimit && !canManageEvent && typeof remainingSpots === 'number' && (
<p className="text-center text-xs text-slate-400">
{remainingSpots} spot{remainingSpots !== 1 ? 's' : ''} left
{event.waitlist_enabled && (
Expand All @@ -483,41 +506,49 @@ function EventPopup({ event, onClose, isAdminView, user }) {
</p>
)}

{hasCapacityLimit && !canEditEvent && attendanceLoading && (
{hasCapacityLimit && !canManageEvent && attendanceLoading && (
<p className="text-center text-xs text-slate-400">
Loading live spots...
</p>
)}

{hasCapacityLimit && !canEditEvent && attendanceLoaded && typeof remainingSpots !== 'number' && (
{hasCapacityLimit && !canManageEvent && attendanceLoaded && typeof remainingSpots !== 'number' && (
<p className="text-center text-xs text-slate-400">
Unable to load live spots
</p>
)}

{canEditEvent && (
<Link
to={`/events/${event.id}/edit`}
className="inline-flex w-full items-center justify-center rounded-xl border border-slate-400/40 bg-slate-800 px-6 py-2.5 text-sm font-semibold text-white transition hover:border-slate-300 hover:bg-slate-700"
onClick={onClose}
>
Edit event
</Link>
{canManageEvent && (
<div className="grid grid-cols-1 gap-2">
<Link
to={`/events/${event.id}/admin/attendees`}
className="inline-flex w-full items-center justify-center rounded-xl bg-gradient-to-r from-emerald-500 to-teal-500 px-6 py-2.5 text-sm font-semibold text-white transition-all duration-200 hover:from-emerald-400 hover:to-teal-400"
onClick={onClose}
>
View attendees
</Link>
<Link
to={`/events/${event.id}/edit`}
className="inline-flex w-full items-center justify-center rounded-xl border border-slate-400/40 bg-slate-800 px-6 py-2.5 text-sm font-semibold text-white transition hover:border-slate-300 hover:bg-slate-700"
onClick={onClose}
>
Edit event
</Link>
</div>
)}

{!canEditEvent && event.status === 'closed' && (
{!canManageEvent && event.status === 'closed' && (
<div className="inline-flex w-full cursor-not-allowed items-center justify-center rounded-xl border border-slate-600/60 bg-slate-800/70 px-6 py-2.5 text-sm font-semibold text-slate-400">
Registration closed
</div>
)}

{!canEditEvent && event.status === 'draft' && (
{!canManageEvent && event.status === 'draft' && (
<div className="inline-flex w-full cursor-not-allowed items-center justify-center rounded-xl border border-yellow-400/20 bg-yellow-500/10 px-6 py-2.5 text-sm font-semibold text-yellow-300">
Not yet published
</div>
)}

{!canEditEvent && event.status === 'published' && registrationCta.disabled && (
{!canManageEvent && event.status === 'published' && registrationCta.disabled && (
<div
className={[
'inline-flex w-full cursor-not-allowed items-center justify-center rounded-xl px-6 py-2.5 text-sm font-semibold',
Expand All @@ -539,7 +570,7 @@ function EventPopup({ event, onClose, isAdminView, user }) {
</button>
)}

{!shouldShowWaitlistJoin && !canEditEvent && event.status === 'published' && !registrationCta.disabled && (
{!shouldShowWaitlistJoin && !canManageEvent && event.status === 'published' && !registrationCta.disabled && (
<Link
to={`/events/${event.id}/register`}
className="inline-flex w-full items-center justify-center rounded-xl bg-gradient-to-r from-sky-500 to-indigo-500 px-6 py-2.5 text-sm font-semibold text-white transition-all duration-200 hover:from-sky-400 hover:to-indigo-400 focus:outline-none focus:ring-2 focus:ring-sky-500"
Expand Down
Loading
Loading