Skip to content

Commit cbe6ac4

Browse files
committed
Add frontend waitlist support for events
1 parent fd94d75 commit cbe6ac4

4 files changed

Lines changed: 204 additions & 4 deletions

File tree

src/APIFunctions/SCEvents.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,3 +141,28 @@ export async function registerForSCEvent(eventId, token, payload) {
141141

142142
return status;
143143
}
144+
145+
export async function joinWaitlistForSCEvent(eventId, token) {
146+
const status = new ApiResponse();
147+
148+
try {
149+
const res = await fetch(`${SCEVENTS_API_URL}/events/${eventId}/waitlist`, {
150+
method: 'POST',
151+
headers: {
152+
Authorization: `Bearer ${token}`,
153+
},
154+
});
155+
156+
const body = await res.json();
157+
status.responseData = body;
158+
159+
if (!res.ok) {
160+
status.error = true;
161+
}
162+
} catch (err) {
163+
status.error = true;
164+
status.responseData = err;
165+
}
166+
167+
return status;
168+
}

src/Pages/Events/CalendarView.js

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, { useState, useMemo, useEffect, useRef } from 'react';
22
import { Link } from 'react-router-dom';
3-
import { getEventAttendanceSummary } from '../../APIFunctions/SCEvents';
3+
import { getEventAttendanceSummary, joinWaitlistForSCEvent } from '../../APIFunctions/SCEvents';
44

55
// ─── tiny helpers ────────────────────────────────────────────────────────────
66

@@ -281,6 +281,9 @@ function EventPopup({ event, onClose, isAdminView, user }) {
281281
const [attendeeCount, setAttendeeCount] = useState(null);
282282
const [attendanceLoading, setAttendanceLoading] = useState(false);
283283
const [attendanceLoaded, setAttendanceLoaded] = useState(false);
284+
const [waitlistSubmitting, setWaitlistSubmitting] = useState(false);
285+
const [waitlistMessage, setWaitlistMessage] = useState('');
286+
const [waitlistError, setWaitlistError] = useState('');
284287
const canEditEvent =
285288
Array.isArray(event.admins) &&
286289
userId &&
@@ -292,6 +295,13 @@ function EventPopup({ event, onClose, isAdminView, user }) {
292295
? Math.max(maxAttendees - attendeeCount, 0)
293296
: null;
294297
const registrationCta = getRegistrationCta(event);
298+
const isFull = hasCapacityLimit && typeof remainingSpots === 'number' && remainingSpots <= 0;
299+
const shouldShowWaitlistJoin =
300+
!canEditEvent &&
301+
event.status === 'published' &&
302+
!registrationCta.disabled &&
303+
isFull &&
304+
!!event.waitlist_enabled;
295305

296306
useEffect(() => {
297307
function onKey(e) {
@@ -342,6 +352,41 @@ function EventPopup({ event, onClose, isAdminView, user }) {
342352
};
343353
}, [eventId, authToken]);
344354

355+
async function handleJoinWaitlist() {
356+
setWaitlistError('');
357+
setWaitlistMessage('');
358+
359+
if (!authToken) {
360+
setWaitlistError('You must be logged in to join the waitlist.');
361+
return;
362+
}
363+
364+
if (!eventId) {
365+
setWaitlistError('Missing event id.');
366+
return;
367+
}
368+
369+
setWaitlistSubmitting(true);
370+
const response = await joinWaitlistForSCEvent(eventId, authToken);
371+
setWaitlistSubmitting(false);
372+
373+
if (response.error) {
374+
let msg = '';
375+
const data = response.responseData;
376+
377+
if (data && typeof data === 'object' && data.error) {
378+
msg = String(data.error);
379+
} else if (typeof data === 'string' && data.trim()) {
380+
msg = data.trim();
381+
}
382+
383+
setWaitlistError(msg || 'Failed to join waitlist.');
384+
return;
385+
}
386+
387+
setWaitlistMessage('Joined waitlist successfully.');
388+
}
389+
345390
return (
346391
<div className="fixed inset-0 z-50 flex items-center justify-center bg-slate-950/70 p-4">
347392
<div
@@ -424,7 +469,7 @@ function EventPopup({ event, onClose, isAdminView, user }) {
424469
<p className="text-center text-xs text-slate-400">
425470
{attendeeCount} {attendeeCount === 1 ? 'person' : 'people'} registered
426471
{event.waitlist_enabled && (
427-
<span className="ml-1 text-amber-300">· waitlist enabled</span>
472+
<span className="ml-1 text-amber-300">· waitlist available</span>
428473
)}
429474
</p>
430475
)}
@@ -433,7 +478,7 @@ function EventPopup({ event, onClose, isAdminView, user }) {
433478
<p className="text-center text-xs text-slate-400">
434479
{remainingSpots} spot{remainingSpots !== 1 ? 's' : ''} left
435480
{event.waitlist_enabled && (
436-
<span className="ml-1 text-amber-300">· waitlist enabled</span>
481+
<span className="ml-1 text-amber-300">· waitlist available</span>
437482
)}
438483
</p>
439484
)}
@@ -483,7 +528,18 @@ function EventPopup({ event, onClose, isAdminView, user }) {
483528
</div>
484529
)}
485530

486-
{!canEditEvent && event.status === 'published' && !registrationCta.disabled && (
531+
{shouldShowWaitlistJoin && (
532+
<button
533+
type="button"
534+
onClick={handleJoinWaitlist}
535+
disabled={waitlistSubmitting}
536+
className="inline-flex w-full items-center justify-center rounded-xl bg-gradient-to-r from-amber-500 to-orange-500 px-6 py-2.5 text-sm font-semibold text-white transition-all duration-200 hover:from-amber-400 hover:to-orange-400 disabled:cursor-not-allowed disabled:opacity-70"
537+
>
538+
{waitlistSubmitting ? 'Joining waitlist...' : 'Join waitlist'}
539+
</button>
540+
)}
541+
542+
{!shouldShowWaitlistJoin && !canEditEvent && event.status === 'published' && !registrationCta.disabled && (
487543
<Link
488544
to={`/events/${event.id}/register`}
489545
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"
@@ -492,6 +548,14 @@ function EventPopup({ event, onClose, isAdminView, user }) {
492548
{registrationCta.label}
493549
</Link>
494550
)}
551+
552+
{waitlistError && (
553+
<p className="text-center text-xs text-red-300">{waitlistError}</p>
554+
)}
555+
556+
{waitlistMessage && (
557+
<p className="text-center text-xs text-emerald-300">{waitlistMessage}</p>
558+
)}
495559
</div>
496560
</div>
497561
</div>

src/Pages/Events/CreateEventPage.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ export default function CreateEventPage() {
8989
const [visibility, setVisibility] = useState('public');
9090
const [minimumVisibleRole, setMinimumVisibleRole] = useState('');
9191
const [maxAttendees, setMaxAttendees] = useState(UNLIMITED_ATTENDEES);
92+
const [waitlistEnabled, setWaitlistEnabled] = useState(false);
93+
const [waitlistSize, setWaitlistSize] = useState(10);
9294
const [questions, setQuestions] = useState(defaultQuestions);
9395
const [submitError, setSubmitError] = useState('');
9496
const [submitting, setSubmitting] = useState(false);
@@ -184,6 +186,10 @@ export default function CreateEventPage() {
184186
if (maxAttendees !== UNLIMITED_ATTENDEES && (maxAttendees === '' || maxAttendees <= 0)) {
185187
setSubmitError('Please enter a valid max attendees, or check "No limit".');
186188
}
189+
if (waitlistEnabled && (!waitlistSize || Number(waitlistSize) <= 0)) {
190+
setSubmitError('Please enter a valid waitlist size.');
191+
return;
192+
}
187193

188194
const payload = {
189195
id: eventId,
@@ -200,6 +206,8 @@ export default function CreateEventPage() {
200206
status,
201207
visibility,
202208
minimum_visible_role: visibility === 'private' ? minimumVisibleRole : '',
209+
waitlist_enabled: waitlistEnabled,
210+
waitlist_size: waitlistEnabled ? Number(waitlistSize) : 0,
203211
};
204212

205213
setSubmitting(true);
@@ -364,6 +372,51 @@ export default function CreateEventPage() {
364372
</div>
365373
</div>
366374

375+
<div>
376+
<div className="label">
377+
<span className="label-text">Waitlist</span>
378+
</div>
379+
380+
<div className="flex items-center gap-4">
381+
<div className="max-w-xs">
382+
{waitlistEnabled ? (
383+
<input
384+
type="number"
385+
min="1"
386+
className="w-full input input-bordered"
387+
value={waitlistSize}
388+
onChange={(e) => setWaitlistSize(e.target.value ? parseInt(e.target.value, 10) : '')}
389+
placeholder="e.g. 20"
390+
/>
391+
) : (
392+
<input
393+
type="text"
394+
className="w-full input input-bordered"
395+
value=""
396+
disabled
397+
placeholder="e.g. 20"
398+
/>
399+
)}
400+
</div>
401+
402+
<label className="flex gap-2 items-center text-sm cursor-pointer label">
403+
<input
404+
type="checkbox"
405+
className="checkbox checkbox-sm"
406+
checked={waitlistEnabled}
407+
onChange={(e) => setWaitlistEnabled(e.target.checked)}
408+
/>
409+
<span className="label-text font-medium">Enable waitlist</span>
410+
</label>
411+
</div>
412+
413+
{waitlistEnabled && (
414+
<p className="mt-2 text-xs text-gray-500 dark:text-gray-400">
415+
Maximum number of users allowed on the waitlist.
416+
</p>
417+
)}
418+
</div>
419+
367420
<div className="grid gap-4 sm:grid-cols-2">
368421
<label className="w-full form-control">
369422
<div className="label">

src/Pages/Events/EditEventPage.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ export default function EditEventPage() {
5858
const [visibility, setVisibility] = useState('public');
5959
const [minimumVisibleRole, setMinimumVisibleRole] = useState('');
6060
const [maxAttendees, setMaxAttendees] = useState(UNLIMITED_ATTENDEES);
61+
const [waitlistEnabled, setWaitlistEnabled] = useState(false);
62+
const [waitlistSize, setWaitlistSize] = useState(10);
6163
const [questions, setQuestions] = useState([]);
6264
const [eventAdmins, setEventAdmins] = useState([]);
6365

@@ -93,6 +95,10 @@ export default function EditEventPage() {
9395
setMaxAttendees(
9496
typeof evt.max_attendees === 'number' ? evt.max_attendees : UNLIMITED_ATTENDEES,
9597
);
98+
setWaitlistEnabled(!!evt.waitlist_enabled);
99+
setWaitlistSize(
100+
typeof evt.waitlist_size === 'number' && evt.waitlist_size > 0 ? evt.waitlist_size : 10,
101+
);
96102
setQuestions(evt.registration_form || []);
97103
setEventAdmins(evt.admins || []);
98104
}
@@ -182,6 +188,11 @@ export default function EditEventPage() {
182188
return;
183189
}
184190

191+
if (waitlistEnabled && (!waitlistSize || Number(waitlistSize) <= 0)) {
192+
setSubmitError('Please enter a valid waitlist size.');
193+
return;
194+
}
195+
185196
const payload = {
186197
name: eventName.trim(),
187198
date,
@@ -194,6 +205,8 @@ export default function EditEventPage() {
194205
status,
195206
visibility,
196207
minimum_visible_role: visibility === 'private' ? minimumVisibleRole : '',
208+
waitlist_enabled: waitlistEnabled,
209+
waitlist_size: waitlistEnabled ? Number(waitlistSize) : 0,
197210
};
198211

199212
setSubmitting(true);
@@ -371,6 +384,51 @@ export default function EditEventPage() {
371384
/>
372385
</div>
373386

387+
<div>
388+
<div className="label">
389+
<span className="label-text">Waitlist</span>
390+
</div>
391+
392+
<div className="flex items-center gap-4">
393+
<div className="max-w-xs">
394+
{waitlistEnabled ? (
395+
<input
396+
type="number"
397+
min="1"
398+
className="w-full input input-bordered"
399+
value={waitlistSize}
400+
onChange={(e) => setWaitlistSize(e.target.value ? parseInt(e.target.value, 10) : '')}
401+
placeholder="e.g. 20"
402+
/>
403+
) : (
404+
<input
405+
type="text"
406+
className="w-full input input-bordered"
407+
value=""
408+
disabled
409+
placeholder="e.g. 20"
410+
/>
411+
)}
412+
</div>
413+
414+
<label className="flex gap-2 items-center text-sm cursor-pointer label">
415+
<input
416+
type="checkbox"
417+
className="checkbox checkbox-sm"
418+
checked={waitlistEnabled}
419+
onChange={(e) => setWaitlistEnabled(e.target.checked)}
420+
/>
421+
<span className="label-text font-medium">Enable waitlist</span>
422+
</label>
423+
</div>
424+
425+
{waitlistEnabled && (
426+
<p className="mt-2 text-xs text-gray-500 dark:text-gray-400">
427+
Maximum number of users allowed on the waitlist.
428+
</p>
429+
)}
430+
</div>
431+
374432
<div className="grid gap-4 sm:grid-cols-2">
375433
<label className="w-full form-control">
376434
<div className="label">

0 commit comments

Comments
 (0)