From 0b947388f17892af6ebafbd2b453d066d6cf75ce Mon Sep 17 00:00:00 2001 From: Steven Le Date: Fri, 1 May 2026 08:48:51 -0700 Subject: [PATCH 1/4] make event HC clearer, refactor shared functions, remove unused code --- src/Pages/Events/CalendarView.js | 179 ++++-------- .../Events/CreateEventFormQuestionBlock.js | 2 - src/Pages/Events/CreateEventPage.js | 142 ++-------- src/Pages/Events/EditEventPage.js | 140 ++-------- src/Pages/Events/EventAttendeesDashboard.js | 2 +- src/Pages/Events/EventIcons.js | 108 +++++++ src/Pages/Events/Events.js | 263 +----------------- src/Pages/Events/EventsRegistration.js | 49 +--- src/Pages/Events/eventUtils.js | 58 ++++ src/Pages/Events/useEventQuestions.js | 118 ++++++++ 10 files changed, 385 insertions(+), 676 deletions(-) create mode 100644 src/Pages/Events/EventIcons.js create mode 100644 src/Pages/Events/eventUtils.js create mode 100644 src/Pages/Events/useEventQuestions.js diff --git a/src/Pages/Events/CalendarView.js b/src/Pages/Events/CalendarView.js index ef977b6ec..77a6ef570 100644 --- a/src/Pages/Events/CalendarView.js +++ b/src/Pages/Events/CalendarView.js @@ -1,9 +1,8 @@ -import React, { useState, useMemo, useEffect, useRef } from 'react'; +import { useState, useMemo, useEffect, useRef } from 'react'; import { Link } from 'react-router-dom'; import { getEventAttendanceSummary, getMyEventRegistrationState, joinWaitlistForSCEvent } from '../../APIFunctions/SCEvents'; import { membershipState } from '../../Enums'; - -// ─── tiny helpers ──────────────────────────────────────────────────────────── +import { calculateEventCapacity, getApiErrorMessage, toDateKey } from './eventUtils'; const DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; const MONTHS = [ @@ -13,13 +12,6 @@ const MONTHS = [ const currentYear = new Date().getFullYear(); const YEAR_RANGE = Array.from({ length: 10 }, (_, i) => currentYear + i); -function toDateKey(date) { - const y = date.getFullYear(); - const m = String(date.getMonth() + 1).padStart(2, '0'); - const d = String(date.getDate()).padStart(2, '0'); - return `${y}-${m}-${d}`; -} - function eventDateKey(event) { if (!event.date) return null; if (/^\d{4}-\d{2}-\d{2}$/.test(event.date)) return event.date; @@ -159,12 +151,8 @@ function getBadgeText(event, isAdminView) { return ''; } -function getRegistrationStatus(event) { - return event?.registration_status || 'none'; -} - function getRegistrationCta(event) { - const registrationStatus = getRegistrationStatus(event); + const registrationStatus = event?.registration_status || 'none'; switch (registrationStatus) { case 'registered': @@ -225,73 +213,15 @@ function canUserManageEvent(event, user) { // ─── icons ──────────────────────────────────────────────────────────────────── -function ChevronLeft() { - return ( - - ); -} - -function ChevronRight() { - return ( - - ); -} - -function CalendarIcon() { - return ( - - ); -} - -function PinIcon() { - return ( - - ); -} - -function ClockIcon() { - return ( - - ); -} - -function XIcon() { - return ( - - ); -} - -function PlusIcon() { - return ( - - ); -} +import { + ChevronLeft, + ChevronRight, + CalendarIcon, + PinIcon, + ClockIcon, + XIcon, + PlusIcon +} from './EventIcons'; // ─── Event popup ────────────────────────────────────────────────────────────── @@ -311,14 +241,10 @@ function EventPopup({ event, onClose, isAdminView, user }) { 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 = - hasCapacityLimit && typeof attendeeCount === 'number' - ? Math.max(maxAttendees - attendeeCount, 0) - : null; + + const { maxAttendees, hasCapacityLimit, remainingSpots, isFull } = calculateEventCapacity(event, attendeeCount); + const registrationCta = getRegistrationCta(event); - const isFull = hasCapacityLimit && typeof remainingSpots === 'number' && remainingSpots <= 0; const shouldShowWaitlistJoin = !canManageEvent && event.status === 'published' && @@ -416,33 +342,24 @@ function EventPopup({ event, onClose, isAdminView, user }) { setWaitlistSubmitting(false); if (response.error) { - let msg = ''; - const data = response.responseData; - - if (data && typeof data === 'object' && data.error) { - msg = String(data.error); - } else if (typeof data === 'string' && data.trim()) { - msg = data.trim(); - } - - setWaitlistError(msg || 'Failed to join waitlist.'); + setWaitlistError(getApiErrorMessage(response, { fallback: 'Failed to join waitlist.' })); return; } setWaitlistMessage('Joined waitlist successfully.'); } return ( -
+
-
+
@@ -451,7 +368,7 @@ function EventPopup({ event, onClose, isAdminView, user }) {
-
+
{event.date && (
@@ -508,18 +425,18 @@ function EventPopup({ event, onClose, isAdminView, user }) { )}
-
+
{event.max_attendees > 0 && ( -

- {event.max_attendees} spot{event.max_attendees !== 1 ? 's' : ''} available - {event.waitlist_enabled && ( +

+ {event.max_attendees} total spot{event.max_attendees !== 1 ? 's' : ''} + {canManageEvent && event.waitlist_enabled && ( · waitlist available )}

)} {hasCapacityLimit && !canManageEvent && typeof remainingSpots === 'number' && ( -

+

{remainingSpots} spot{remainingSpots !== 1 ? 's' : ''} left {event.waitlist_enabled && ( · waitlist available @@ -528,13 +445,13 @@ function EventPopup({ event, onClose, isAdminView, user }) { )} {hasCapacityLimit && !canManageEvent && attendanceLoading && ( -

+

Loading live spots...

)} {hasCapacityLimit && !canManageEvent && attendanceLoaded && typeof remainingSpots !== 'number' && ( -

+

Unable to load live spots

)} @@ -602,11 +519,11 @@ function EventPopup({ event, onClose, isAdminView, user }) { )} {waitlistError && ( -

{waitlistError}

+

{waitlistError}

)} {waitlistMessage && ( -

{waitlistMessage}

+

{waitlistMessage}

)}
@@ -636,7 +553,7 @@ function EventRow({ event, onSelect, isAdminView }) { title={event.name} > -
+
{timeLabel && ( {timeLabel} @@ -688,7 +605,7 @@ function CalCell({ date, isCurrentMonth, isToday, events, onSelectEvent, isAdmin isToday ? 'bg-cyan-500/[0.12]' : 'hover:bg-slate-700/35', ].join(' ')} > -
+
+
No events scheduled this month.
); @@ -749,7 +666,7 @@ function MobileMonthAgenda({ monthEvents, onSelectEvent, isAdminView }) { }, []); return ( -
+
{groupedEvents.map(({ dateKey, events }) => { const [year, month, day] = dateKey.split('-'); const date = new Date(Number(year), Number(month) - 1, Number(day)); @@ -786,8 +703,8 @@ function MobileMonthAgenda({ monthEvents, onSelectEvent, isAdminView }) { colors.border, ].join(' ')} > -
-
+
+
{event.name || 'Untitled Event'}
@@ -796,7 +713,7 @@ function MobileMonthAgenda({ monthEvents, onSelectEvent, isAdminView }) {
-
+
{event.location || '\u00A0'}
@@ -899,7 +816,7 @@ export default function CalendarView({ )}
-
+
@@ -916,8 +833,8 @@ export default function CalendarView({ ))} -
- +
+
@@ -937,8 +854,8 @@ export default function CalendarView({ ))} -
- +
+
@@ -962,7 +879,7 @@ export default function CalendarView({ @@ -971,21 +888,21 @@ export default function CalendarView({
-
+
{isAdminView ? ( -
+
{[ { label: 'Published', dot: 'bg-cyan-300' }, { label: 'Private', dot: 'bg-violet-300' }, @@ -1023,7 +940,7 @@ export default function CalendarView({ ))}
) : ( -
+
{[ { label: 'Open', dot: 'bg-cyan-300' }, { label: 'Members only', dot: 'bg-violet-300' }, diff --git a/src/Pages/Events/CreateEventFormQuestionBlock.js b/src/Pages/Events/CreateEventFormQuestionBlock.js index 808b27b63..1b12fb90e 100644 --- a/src/Pages/Events/CreateEventFormQuestionBlock.js +++ b/src/Pages/Events/CreateEventFormQuestionBlock.js @@ -1,6 +1,4 @@ /* eslint-disable camelcase -- SCEvents registration question shape uses snake_case */ -import React from 'react'; - export default function CreateEventFormQuestionBlock({ question, index, diff --git a/src/Pages/Events/CreateEventPage.js b/src/Pages/Events/CreateEventPage.js index 790340f99..a8b9027fe 100644 --- a/src/Pages/Events/CreateEventPage.js +++ b/src/Pages/Events/CreateEventPage.js @@ -1,24 +1,16 @@ /* eslint-disable camelcase -- mirrors SCEvents JSON field names in state and payloads */ -import React, { useMemo, useState } from 'react'; +import { useMemo, useState } from 'react'; import { Link, useHistory } from 'react-router-dom'; import { useSCE } from '../../Components/context/SceContext'; import { createSCEvent } from '../../APIFunctions/SCEvents'; import CreateEventFormQuestionBlock from './CreateEventFormQuestionBlock'; import { membershipState } from '../../Enums'; +import { useEventQuestions, toApiRegistrationForm } from './useEventQuestions'; +import { getApiErrorMessage } from './eventUtils'; /** Matches SCEvents `max_attendees` when there is no cap. */ const UNLIMITED_ATTENDEES = -1; -function newQuestionTemplate() { - return { - id: crypto.randomUUID(), - type: 'textbox', - question: '', - required: false, - answer_details: { max_chars: 200 }, - }; -} - function defaultQuestions() { return [ { @@ -45,28 +37,6 @@ function defaultQuestions() { ]; } -function toApiRegistrationForm(questions) { - return questions.map((q) => { - const base = { - id: q.id, - type: q.type, - question: q.question, - required: !!q.required, - }; - if (q.type === 'textbox' && q.answer_details?.max_chars) { - base.answer_details = { max_chars: q.answer_details.max_chars }; - } - if ( - (q.type === 'multiple_choice' || q.type === 'dropdown' || q.type === 'checkbox') && - q.answer_options && - q.answer_options.length - ) { - base.answer_options = q.answer_options; - } - return base; - }); -} - export default function CreateEventPage() { const { user } = useSCE(); const history = useHistory(); @@ -91,7 +61,18 @@ export default function CreateEventPage() { const [maxAttendees, setMaxAttendees] = useState(UNLIMITED_ATTENDEES); const [waitlistEnabled, setWaitlistEnabled] = useState(false); const [waitlistSize, setWaitlistSize] = useState(10); - const [questions, setQuestions] = useState(defaultQuestions); + + const { + questions, + addQuestion, + removeQuestion, + updateQuestion, + updateQuestionType, + updateAnswerOption, + addAnswerOption, + removeAnswerOption + } = useEventQuestions(defaultQuestions()); + const [submitError, setSubmitError] = useState(''); const [submitting, setSubmitting] = useState(false); @@ -99,76 +80,6 @@ export default function CreateEventPage() { const adminId = useMemo(() => (user?._id != null ? String(user._id) : ''), [user]); - function addQuestion() { - setQuestions((prev) => [...prev, newQuestionTemplate()]); - } - - function removeQuestion(id) { - setQuestions((prev) => prev.filter((q) => q.id !== id)); - } - - function updateQuestion(id, field, value) { - setQuestions((prev) => - prev.map((q) => (q.id === id ? { ...q, [field]: value } : q)), - ); - } - - function updateQuestionType(id, newType) { - setQuestions((prev) => - prev.map((q) => { - if (q.id !== id) return q; - const base = { - id: q.id, - type: newType, - question: q.question, - required: q.required, - }; - if (newType === 'textbox') { - return { ...base, answer_details: { max_chars: 200 } }; - } - if (newType === 'multiple_choice' || newType === 'dropdown' || newType === 'checkbox') { - return { ...base, answer_options: ['Option 1', 'Option 2'] }; - } - return base; - }), - ); - } - - function updateAnswerOption(questionId, optionIndex, value) { - setQuestions((prev) => - prev.map((q) => { - if (q.id !== questionId) return q; - const next = [...(q.answer_options || [])]; - next[optionIndex] = value; - return { ...q, answer_options: next }; - }), - ); - } - - function addAnswerOption(questionId) { - setQuestions((prev) => - prev.map((q) => { - if (q.id !== questionId) return q; - return { - ...q, - answer_options: [...(q.answer_options || []), 'New option'], - }; - }), - ); - } - - function removeAnswerOption(questionId, optionIndex) { - setQuestions((prev) => - prev.map((q) => { - if (q.id !== questionId) return q; - return { - ...q, - answer_options: (q.answer_options || []).filter((_, i) => i !== optionIndex), - }; - }), - ); - } - async function handleCreateEvent() { setSubmitError(''); if (!eventName.trim()) { @@ -185,6 +96,7 @@ export default function CreateEventPage() { } if (maxAttendees !== UNLIMITED_ATTENDEES && (maxAttendees === '' || maxAttendees <= 0)) { setSubmitError('Please enter a valid max attendees, or check "No limit".'); + return; } if (waitlistEnabled && (!waitlistSize || Number(waitlistSize) <= 0)) { setSubmitError('Please enter a valid waitlist size.'); @@ -216,24 +128,10 @@ export default function CreateEventPage() { setSubmitting(false); if (result.error) { - let msg = ''; - const data = result.responseData; - if (data && typeof data === 'object' && data.error) { - msg = String(data.error); - } else if (typeof data === 'string' && data.trim()) { - msg = data.trim(); - } - if (!msg && result.statusCode) { - msg = `HTTP ${result.statusCode}`; - } - if (result.networkError) { - msg = - (msg || 'Network error') + - '. Is the SCEvents API running (e.g. Docker on port 8002)?'; - } else if (!msg) { - msg = 'SCEvents returned an error.'; - } - setSubmitError(msg); + setSubmitError(getApiErrorMessage(result, { + fallback: 'SCEvents returned an error.', + networkHint: 'Is the SCEvents API running (e.g. Docker on port 8002)?', + })); return; } diff --git a/src/Pages/Events/EditEventPage.js b/src/Pages/Events/EditEventPage.js index ff8c9881a..fbdd609af 100644 --- a/src/Pages/Events/EditEventPage.js +++ b/src/Pages/Events/EditEventPage.js @@ -1,46 +1,16 @@ /* eslint-disable camelcase -- mirrors SCEvents JSON field names in state and payloads */ -import React, { useMemo, useState, useEffect } from 'react'; +import { useMemo, useState, useEffect } from 'react'; import { Link, useHistory, useParams } from 'react-router-dom'; import { useSCE } from '../../Components/context/SceContext'; import { getEventByID, updateSCEvent } from '../../APIFunctions/SCEvents'; import CreateEventFormQuestionBlock from './CreateEventFormQuestionBlock'; import { membershipState } from '../../Enums'; +import { toApiRegistrationForm, useEventQuestions } from './useEventQuestions'; +import { getApiErrorMessage } from './eventUtils'; /** Matches SCEvents `max_attendees` when there is no cap. */ const UNLIMITED_ATTENDEES = -1; -function newQuestionTemplate() { - return { - id: crypto.randomUUID(), - type: 'textbox', - question: '', - required: false, - answer_details: { max_chars: 200 }, - }; -} - -function toApiRegistrationForm(questions) { - return questions.map((q) => { - const base = { - id: q.id, - type: q.type, - question: q.question, - required: !!q.required, - }; - if (q.type === 'textbox' && q.answer_details?.max_chars) { - base.answer_details = { max_chars: q.answer_details.max_chars }; - } - if ( - (q.type === 'multiple_choice' || q.type === 'dropdown') && - q.answer_options && - q.answer_options.length - ) { - base.answer_options = q.answer_options; - } - return base; - }); -} - export default function EditEventPage() { const { id } = useParams(); const { user } = useSCE(); @@ -60,8 +30,18 @@ export default function EditEventPage() { const [maxAttendees, setMaxAttendees] = useState(UNLIMITED_ATTENDEES); const [waitlistEnabled, setWaitlistEnabled] = useState(false); const [waitlistSize, setWaitlistSize] = useState(10); - const [questions, setQuestions] = useState([]); const [eventAdmins, setEventAdmins] = useState([]); + const { + questions, + setQuestions, + addQuestion, + removeQuestion, + updateQuestion, + updateQuestionType, + updateAnswerOption, + addAnswerOption, + removeAnswerOption + } = useEventQuestions([]); const [submitError, setSubmitError] = useState(''); const [submitting, setSubmitting] = useState(false); @@ -106,76 +86,6 @@ export default function EditEventPage() { loadEvent(); }, [id]); - function addQuestion() { - setQuestions((prev) => [...prev, newQuestionTemplate()]); - } - - function removeQuestion(qId) { - setQuestions((prev) => prev.filter((q) => q.id !== qId)); - } - - function updateQuestion(qId, field, value) { - setQuestions((prev) => - prev.map((q) => (q.id === qId ? { ...q, [field]: value } : q)), - ); - } - - function updateQuestionType(qId, newType) { - setQuestions((prev) => - prev.map((q) => { - if (q.id !== qId) return q; - const base = { - id: q.id, - type: newType, - question: q.question, - required: q.required, - }; - if (newType === 'textbox') { - return { ...base, answer_details: { max_chars: 200 } }; - } - if (newType === 'multiple_choice' || newType === 'dropdown') { - return { ...base, answer_options: ['Option 1', 'Option 2'] }; - } - return base; - }), - ); - } - - function updateAnswerOption(questionId, optionIndex, value) { - setQuestions((prev) => - prev.map((q) => { - if (q.id !== questionId) return q; - const next = [...(q.answer_options || [])]; - next[optionIndex] = value; - return { ...q, answer_options: next }; - }), - ); - } - - function addAnswerOption(questionId) { - setQuestions((prev) => - prev.map((q) => { - if (q.id !== questionId) return q; - return { - ...q, - answer_options: [...(q.answer_options || []), 'New option'], - }; - }), - ); - } - - function removeAnswerOption(questionId, optionIndex) { - setQuestions((prev) => - prev.map((q) => { - if (q.id !== questionId) return q; - return { - ...q, - answer_options: (q.answer_options || []).filter((_, i) => i !== optionIndex), - }; - }), - ); - } - async function handleUpdateEvent() { setSubmitError(''); if (!eventName.trim()) { @@ -215,24 +125,10 @@ export default function EditEventPage() { setSubmitting(false); if (result.error) { - let msg = ''; - const data = result.responseData; - if (data && typeof data === 'object' && data.error) { - msg = String(data.error); - } else if (typeof data === 'string' && data.trim()) { - msg = data.trim(); - } - if (!msg && result.statusCode) { - msg = `HTTP ${result.statusCode}`; - } - if (result.networkError) { - msg = - (msg || 'Network error') + - '. Is the SCEvents API running (e.g. Docker on port 8002)?'; - } else if (!msg) { - msg = 'SCEvents returned an error.'; - } - setSubmitError(msg); + setSubmitError(getApiErrorMessage(result, { + fallback: 'SCEvents returned an error.', + networkHint: 'Is the SCEvents API running (e.g. Docker on port 8002)?', + })); return; } diff --git a/src/Pages/Events/EventAttendeesDashboard.js b/src/Pages/Events/EventAttendeesDashboard.js index e5b7e13ca..ff578afe1 100644 --- a/src/Pages/Events/EventAttendeesDashboard.js +++ b/src/Pages/Events/EventAttendeesDashboard.js @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { Link, Redirect, useParams } from 'react-router-dom'; import { getEventRegistrationByRequestId, getEventRegistrations } from '../../APIFunctions/SCEvents'; import { useSCE } from '../../Components/context/SceContext'; diff --git a/src/Pages/Events/EventIcons.js b/src/Pages/Events/EventIcons.js new file mode 100644 index 000000000..ca7a167cc --- /dev/null +++ b/src/Pages/Events/EventIcons.js @@ -0,0 +1,108 @@ +export function ChevronLeft() { + return ( + + ); +} + +export function ChevronRight() { + return ( + + ); +} + +export function CalendarIcon() { + return ( + + ); +} + +export function PinIcon() { + return ( + + ); +} + +export function ClockIcon() { + return ( + + ); +} + +export function XIcon() { + return ( + + ); +} + +export function PlusIcon() { + return ( + + ); +} + +export function ArrowLeftIcon() { + return ( + + ); +} + +export function PeopleIcon() { + return ( + + + + ); +} + +export function EditIcon() { + return ( + + ); +} diff --git a/src/Pages/Events/Events.js b/src/Pages/Events/Events.js index 945de1bcc..12948aa3f 100644 --- a/src/Pages/Events/Events.js +++ b/src/Pages/Events/Events.js @@ -1,147 +1,13 @@ -import React, { useEffect, useState } from 'react'; -import { Link } from 'react-router-dom'; -import { getAllSCEvents, getEventAttendanceSummary } from '../../APIFunctions/SCEvents'; +import { useEffect, useState } from 'react'; +import { getAllSCEvents } from '../../APIFunctions/SCEvents'; import { useSCE } from '../../Components/context/SceContext'; import { membershipState } from '../../Enums'; import CalendarView from './CalendarView'; - -function CalendarIcon() { - return ( - - ); -} - -function PinIcon() { - return ( - - ); -} - -function PeopleIcon() { - return ( - - ); -} - -function PlusIcon() { - return ( - - ); -} - -function EditIcon() { - return ( - - ); -} - -function getUserAccessLevel(user) { - return user?.accessLevel ?? membershipState.NON_MEMBER; -} - -function formatEventDate(date) { - if (!date) { - return ''; - } - - const [year, month, day] = date.split('-').map(Number); - if (!year || !month || !day) { - return date; - } - - return new Date(year, month - 1, day).toLocaleDateString('en-US', { - month: 'long', - day: 'numeric', - year: 'numeric', - }); -} - -function formatEventTime(time) { - if (!time) { - return ''; - } - - const [hour, minute] = time.split(':').map(Number); - if (Number.isNaN(hour) || Number.isNaN(minute)) { - return time; - } - - return new Date(2000, 0, 1, hour, minute).toLocaleTimeString('en-US', { - hour: 'numeric', - minute: '2-digit', - hour12: true, - }); -} - -function formatDateParam(date) { - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, '0'); - const day = String(date.getDate()).padStart(2, '0'); - return `${year}-${month}-${day}`; -} +import { toDateKey } from './eventUtils'; function canUserSeeEvent(event, user) { const userId = user?._id != null ? String(user._id) : ''; - const userAccess = getUserAccessLevel(user); + const userAccess = user?.accessLevel ?? membershipState.NON_MEMBER; const isGlobalAdmin = userAccess >= membershipState.ADMIN; const isEventAdmin = Array.isArray(event.admins) && userId @@ -169,118 +35,6 @@ function canUserSeeEvent(event, user) { return false; } -function EventCard({ event, user }) { - const [attendeeCount, setAttendeeCount] = useState(0); - const userAccess = getUserAccessLevel(user); - const isEventAdmin = - Array.isArray(event.admins) && user?._id - ? event.admins.includes(String(user._id)) - : false; - const canViewAttendance = - isEventAdmin || - ((!Array.isArray(event.admins) || event.admins.length === 0) && - userAccess >= membershipState.ADMIN); - - useEffect(() => { - let isCurrent = true; - setAttendeeCount(0); - - async function fetchAttendanceSummary() { - const response = await getEventAttendanceSummary(event.id, user?.token); - if (isCurrent && !response.error && typeof response.responseData?.attendee_count === 'number') { - setAttendeeCount(response.responseData.attendee_count); - } - } - - if (canViewAttendance && event.id && user?.token) { - fetchAttendanceSummary(); - } - - return () => { - isCurrent = false; - }; - }, [canViewAttendance, event.id, user?.token]); - - return ( -
-
-

- {event.name || 'Untitled Event'} -

- {isEventAdmin && ( - - - - )} -
- -
- {event.status === 'draft' && ( - - Draft - - )} - {event.visibility === 'private' && ( - - Private - - )} -
- -
- {(event.date || event.time) && ( -
- - - - {[formatEventDate(event.date), formatEventTime(event.time)].filter(Boolean).join(' · ')} -
- )} - - {event.location && ( -
- - - - {event.location} -
- )} - - {canViewAttendance && ( -
- - - - - {attendeeCount} - {' people going'} - -
- )} -
- - {event.description && ( -

- {event.description} -

- )} - -
- - Register - -
-
- ); -} - export default function EventsPage() { const { user } = useSCE(); const [events, setEvents] = useState([]); @@ -291,9 +45,8 @@ export default function EventsPage() { return new Date(today.getFullYear(), today.getMonth(), 1); }); - const canCreateEvent = user?.accessLevel >= membershipState.OFFICER; + const isAdminView = user?.accessLevel >= membershipState.OFFICER; const visibleEvents = events.filter((event) => canUserSeeEvent(event, user)); - const isAdminView = canCreateEvent; const pageContainerClass = isAdminView ? 'relative h-dvh overflow-hidden bg-gradient-to-r from-gray-800 to-gray-600 text-white' : 'relative h-[calc(100dvh-4rem)] overflow-hidden bg-gradient-to-r from-gray-800 to-gray-600 text-white'; @@ -307,8 +60,8 @@ export default function EventsPage() { setHasError(false); const token = window.localStorage.getItem('jwtToken'); - const startDate = formatDateParam(new Date(cursor.getFullYear(), cursor.getMonth(), 1)); - const endDate = formatDateParam(new Date(cursor.getFullYear(), cursor.getMonth() + 1, 0)); + const startDate = toDateKey(new Date(cursor.getFullYear(), cursor.getMonth(), 1)); + const endDate = toDateKey(new Date(cursor.getFullYear(), cursor.getMonth() + 1, 0)); const response = await getAllSCEvents(token, { startDate, endDate }); if (!response.error) { @@ -350,7 +103,7 @@ export default function EventsPage() { events={visibleEvents} isAdminView={isAdminView} user={user} - canCreateEvent={canCreateEvent} + canCreateEvent={isAdminView} cursor={cursor} setCursor={setCursor} /> diff --git a/src/Pages/Events/EventsRegistration.js b/src/Pages/Events/EventsRegistration.js index 90a31d816..75d3a438f 100644 --- a/src/Pages/Events/EventsRegistration.js +++ b/src/Pages/Events/EventsRegistration.js @@ -1,27 +1,10 @@ /* eslint-disable camelcase -- mirrors SCEvents JSON field names in state and payloads */ -import React, { useState, useEffect } from 'react'; +import { useState, useEffect } from 'react'; import { useParams, useHistory } from 'react-router-dom'; import { useSCE } from '../../Components/context/SceContext'; import { getEventByID, getEventAttendanceSummary, registerForEvent } from '../../APIFunctions/SCEvents'; - -function ArrowLeftIcon() { - return ( - - ); -} - -function getRegistrationStatus(event) { - return event?.registration_status || 'none'; -} +import { calculateEventCapacity, getApiErrorMessage } from './eventUtils'; +import { ArrowLeftIcon } from './EventIcons'; function StatusPanel({ title, message, borderClass, textClass, onBack }) { return ( @@ -64,7 +47,7 @@ export default function EventRegistration() { const [submitting, setSubmitting] = useState(false); const [attendeeCount, setAttendeeCount] = useState(null); const [attendanceLoading, setAttendanceLoading] = useState(false); - const registrationStatus = getRegistrationStatus(event); + const registrationStatus = event?.registration_status || 'none'; const goBackToEvents = () => history.push('/events'); useEffect(() => { @@ -166,27 +149,12 @@ export default function EventRegistration() { setSubmitting(false); if (result.error) { - let msg = ''; - const data = result.responseData; - - if (data && typeof data === 'object' && data.error) { - msg = String(data.error); - } else if (typeof data === 'string' && data.trim()) { - msg = data.trim(); - } + let msg = getApiErrorMessage(result, { fallback: 'Failed to submit registration.' }); if (msg.toLowerCase().includes('registration is closed')) { msg = 'This event is no longer accepting sign-ups, but more events are on the way 👀'; } - if (!msg && result.statusCode) { - msg = `HTTP ${result.statusCode}`; - } - - if (!msg) { - msg = 'Failed to submit registration.'; - } - setSubmitError(msg); return; } @@ -280,12 +248,7 @@ export default function EventRegistration() { ); } - const maxAttendees = Number(event.max_attendees); - const hasCapacityLimit = Number.isFinite(maxAttendees) && maxAttendees > 0; - const remainingSpots = - hasCapacityLimit && typeof attendeeCount === 'number' - ? Math.max(maxAttendees - attendeeCount, 0) - : null; + const { remainingSpots, hasCapacityLimit } = calculateEventCapacity(event, attendeeCount); return (
diff --git a/src/Pages/Events/eventUtils.js b/src/Pages/Events/eventUtils.js new file mode 100644 index 000000000..e41b4fe5a --- /dev/null +++ b/src/Pages/Events/eventUtils.js @@ -0,0 +1,58 @@ +export function calculateEventCapacity(event, attendeeCount) { + const maxAttendees = Number(event?.max_attendees); + const hasCapacityLimit = Number.isFinite(maxAttendees) && maxAttendees > 0; + + const remainingSpots = + hasCapacityLimit && typeof attendeeCount === 'number' + ? Math.max(maxAttendees - attendeeCount, 0) + : null; + + const isFull = hasCapacityLimit && typeof remainingSpots === 'number' && remainingSpots <= 0; + + return { + maxAttendees, + hasCapacityLimit, + remainingSpots, + isFull + }; +} + +export function toDateKey(date) { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; +} + +export function getApiErrorMessage(result, options = {}) { + const { + fallback = 'Request failed.', + networkHint = '', + } = options; + + let msg = ''; + const data = result?.responseData; + + if (data && typeof data === 'object') { + if (data.error) { + msg = String(data.error); + } else if (data.message) { + msg = String(data.message); + } + } else if (typeof data === 'string' && data.trim()) { + msg = data.trim(); + } + + if (!msg && result?.statusCode) { + msg = `HTTP ${result.statusCode}`; + } + + if (result?.networkError) { + msg = msg || 'Network error'; + if (networkHint) { + msg = `${msg}. ${networkHint}`; + } + } + + return msg || fallback; +} diff --git a/src/Pages/Events/useEventQuestions.js b/src/Pages/Events/useEventQuestions.js new file mode 100644 index 000000000..4636f4399 --- /dev/null +++ b/src/Pages/Events/useEventQuestions.js @@ -0,0 +1,118 @@ +import { useState } from 'react'; + +export function newQuestionTemplate() { + return { + id: crypto.randomUUID(), + type: 'textbox', + question: '', + required: false, + answer_details: { max_chars: 200 }, + }; +} + +export function toApiRegistrationForm(questions) { + return questions.map((q) => { + const base = { + id: q.id, + type: q.type, + question: q.question, + required: !!q.required, + }; + + if (q.type === 'textbox' && q.answer_details?.max_chars) { + base.answer_details = { max_chars: q.answer_details.max_chars }; + } + + if (['multiple_choice', 'dropdown', 'checkbox'].includes(q.type)) { + base.answer_options = Array.isArray(q.answer_options) + ? q.answer_options.map((opt) => String(opt).trim()).filter(Boolean) + : []; + } + + return base; + }); +} + +export function useEventQuestions(initialQuestions = []) { + const [questions, setQuestions] = useState(initialQuestions); + + function addQuestion() { + setQuestions((prev) => [...prev, newQuestionTemplate()]); + } + + function removeQuestion(id) { + setQuestions((prev) => prev.filter((q) => q.id !== id)); + } + + function updateQuestion(id, field, value) { + setQuestions((prev) => + prev.map((q) => (q.id === id ? { ...q, [field]: value } : q)) + ); + } + + function updateQuestionType(id, newType) { + setQuestions((prev) => + prev.map((q) => { + if (q.id !== id) return q; + + const updated = { ...q, type: newType }; + + if (newType === 'textbox') { + updated.answer_details = { max_chars: 200 }; + delete updated.answer_options; + } else if (['multiple_choice', 'dropdown', 'checkbox'].includes(newType)) { + if (!updated.answer_options) { + updated.answer_options = ['Option 1', 'Option 2']; + } + delete updated.answer_details; + } + return updated; + }) + ); + } + + function updateAnswerOption(questionId, optionIndex, value) { + setQuestions((prev) => + prev.map((q) => { + if (q.id !== questionId) return q; + const newOptions = [...(q.answer_options || [])]; + newOptions[optionIndex] = value; + return { ...q, answer_options: newOptions }; + }) + ); + } + + function addAnswerOption(questionId) { + setQuestions((prev) => + prev.map((q) => { + if (q.id !== questionId) return q; + const newOptions = [...(q.answer_options || [])]; + newOptions.push(`Option ${newOptions.length + 1}`); + return { ...q, answer_options: newOptions }; + }) + ); + } + + function removeAnswerOption(questionId, optionIndex) { + setQuestions((prev) => + prev.map((q) => { + if (q.id !== questionId) return q; + const newOptions = [...(q.answer_options || [])]; + newOptions.splice(optionIndex, 1); + return { ...q, answer_options: newOptions }; + }) + ); + } + + return { + questions, + setQuestions, + addQuestion, + removeQuestion, + updateQuestion, + updateQuestionType, + updateAnswerOption, + addAnswerOption, + removeAnswerOption + }; +} From 35254e41a2f6ace115e12275cf282c70cd947be7 Mon Sep 17 00:00:00 2001 From: Steven Le Date: Fri, 1 May 2026 09:18:28 -0700 Subject: [PATCH 2/4] refactor CalendarView.js --- src/Pages/Events/Calendar/CalCell.js | 57 ++ src/Pages/Events/Calendar/CalendarView.js | 234 +++++ src/Pages/Events/Calendar/DayLabels.js | 16 + src/Pages/Events/Calendar/EventPopup.js | 296 ++++++ src/Pages/Events/Calendar/EventRow.js | 40 + .../Events/Calendar/MobileMonthAgenda.js | 86 ++ .../Events/Calendar/calendarViewUtils.js | 201 ++++ src/Pages/Events/CalendarView.js | 959 ------------------ src/Pages/Events/Events.js | 2 +- 9 files changed, 931 insertions(+), 960 deletions(-) create mode 100644 src/Pages/Events/Calendar/CalCell.js create mode 100644 src/Pages/Events/Calendar/CalendarView.js create mode 100644 src/Pages/Events/Calendar/DayLabels.js create mode 100644 src/Pages/Events/Calendar/EventPopup.js create mode 100644 src/Pages/Events/Calendar/EventRow.js create mode 100644 src/Pages/Events/Calendar/MobileMonthAgenda.js create mode 100644 src/Pages/Events/Calendar/calendarViewUtils.js delete mode 100644 src/Pages/Events/CalendarView.js diff --git a/src/Pages/Events/Calendar/CalCell.js b/src/Pages/Events/Calendar/CalCell.js new file mode 100644 index 000000000..d434200b0 --- /dev/null +++ b/src/Pages/Events/Calendar/CalCell.js @@ -0,0 +1,57 @@ +import EventRow from './EventRow'; +import { sortEventsForDay } from './calendarViewUtils'; + +export default function CalCell({ date, isCurrentMonth, isToday, events, onSelectEvent, isAdminView }) { + const sortedEvents = sortEventsForDay(events); + const mobileRows = sortedEvents.slice(0, 2); + const desktopOnlyRows = sortedEvents.slice(2, 4); + const hiddenCount = Math.max(sortedEvents.length - 4, 0); + + return ( +
+
+ + {date.getDate()} + +
+ +
+ {mobileRows.map((ev) => ( + + ))} + + {desktopOnlyRows.map((ev) => ( +
+ +
+ ))} + + {hiddenCount > 0 && ( + + +{hiddenCount} more + + )} +
+
+ ); +} diff --git a/src/Pages/Events/Calendar/CalendarView.js b/src/Pages/Events/Calendar/CalendarView.js new file mode 100644 index 000000000..29423b3f7 --- /dev/null +++ b/src/Pages/Events/Calendar/CalendarView.js @@ -0,0 +1,234 @@ +import { useMemo, useState } from 'react'; +import { Link } from 'react-router-dom'; +import { toDateKey } from '../eventUtils'; +import { ChevronLeft, ChevronRight, PlusIcon } from '../EventIcons'; +import { eventDateKey, MONTHS, sortEventsForDay, YEAR_RANGE } from './calendarViewUtils'; +import EventPopup from './EventPopup'; +import DayLabels from './DayLabels'; +import CalCell from './CalCell'; +import MobileMonthAgenda from './MobileMonthAgenda'; + +export default function CalendarView({ + events, + isAdminView = false, + user, + canCreateEvent = false, + cursor, + setCursor, +}) { + const today = useMemo(() => new Date(), []); + const [selectedEvent, setSelectedEvent] = useState(null); + + const eventsByDate = useMemo(() => { + const map = {}; + events.forEach((ev) => { + const key = eventDateKey(ev); + if (!key) return; + if (!map[key]) map[key] = []; + map[key].push(ev); + }); + Object.keys(map).forEach((key) => { + map[key] = sortEventsForDay(map[key]); + }); + return map; + }, [events]); + + const year = cursor.getFullYear(); + const month = cursor.getMonth(); + + function handleMonthChange(e) { + const nextMonth = Number(e.target.value); + setCursor(new Date(year, nextMonth, 1)); + } + + function handleYearChange(e) { + const nextYear = Number(e.target.value); + setCursor(new Date(nextYear, month, 1)); + } + + const firstDayOfMonth = new Date(year, month, 1).getDay(); + const daysInMonth = new Date(year, month + 1, 0).getDate(); + const totalCells = Math.ceil((firstDayOfMonth + daysInMonth) / 7) * 7; + const todayKey = toDateKey(today); + + const cells = Array.from({ length: totalCells }, (_, i) => { + const dayOffset = i - firstDayOfMonth; + const date = new Date(year, month, dayOffset + 1); + const key = toDateKey(date); + return { + date, + key, + isCurrentMonth: date.getMonth() === month, + isToday: key === todayKey, + events: eventsByDate[key] || [], + }; + }); + + const monthEventCount = cells + .filter((c) => c.isCurrentMonth) + .reduce((sum, c) => sum + c.events.length, 0); + + const monthEvents = useMemo(() => { + return cells + .filter((c) => c.isCurrentMonth && c.events.length > 0) + .flatMap((c) => + c.events.map((event) => ({ + event, + dateKey: c.key, + })) + ); + }, [cells]); + + return ( + <> + {selectedEvent && ( + setSelectedEvent(null)} + isAdminView={isAdminView} + user={user} + /> + )} + +
+
+
+
+
+ + +
+ + + +
+
+ +
+ + +
+ + + +
+
+ + +
+ +

+ {monthEventCount} event{monthEventCount !== 1 ? 's' : ''} this month +

+
+ +
+ {canCreateEvent && ( + + + + )} + + + +
+
+ +
+ +
+ +
+ + +
+ {cells.map((cell) => ( + + ))} +
+
+ + {isAdminView ? ( +
+ {[ + { label: 'Published', dot: 'bg-cyan-300' }, + { label: 'Private', dot: 'bg-violet-300' }, + { label: 'Draft', dot: 'bg-amber-300' }, + { label: 'Closed', dot: 'bg-rose-300' }, + ].map(({ label, dot }) => ( + + + {label} + + ))} +
+ ) : ( +
+ {[ + { label: 'Open', dot: 'bg-cyan-300' }, + { label: 'Members only', dot: 'bg-violet-300' }, + { label: 'Closed', dot: 'bg-rose-300' }, + ].map(({ label, dot }) => ( + + + {label} + + ))} +
+ )} +
+ + ); +} diff --git a/src/Pages/Events/Calendar/DayLabels.js b/src/Pages/Events/Calendar/DayLabels.js new file mode 100644 index 000000000..6b6e83b19 --- /dev/null +++ b/src/Pages/Events/Calendar/DayLabels.js @@ -0,0 +1,16 @@ +import { DAYS } from './calendarViewUtils'; + +export default function DayLabels() { + return ( +
+ {DAYS.map((dayName) => ( +
+ {dayName} +
+ ))} +
+ ); +} diff --git a/src/Pages/Events/Calendar/EventPopup.js b/src/Pages/Events/Calendar/EventPopup.js new file mode 100644 index 000000000..152296582 --- /dev/null +++ b/src/Pages/Events/Calendar/EventPopup.js @@ -0,0 +1,296 @@ +import { useState, useEffect, useRef } from 'react'; +import { Link } from 'react-router-dom'; +import { getEventAttendanceSummary, joinWaitlistForSCEvent } from '../../../APIFunctions/SCEvents'; +import { calculateEventCapacity, getApiErrorMessage } from '../eventUtils'; +import { CalendarIcon, PinIcon, ClockIcon, XIcon } from '../EventIcons'; +import { + canUserManageEvent, + formatDate, + formatTime, + getBadgeText, + getRegistrationCta, + pillColors, +} from './calendarViewUtils'; + +export default function EventPopup({ event, onClose, isAdminView, user }) { + const popupRef = useRef(null); + const colors = pillColors(event, isAdminView); + const badgeText = getBadgeText(event, isAdminView); + 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 canManageEvent = canUserManageEvent(event, user); + const eventId = event?.id || event?._id; + const authToken = user?.token; + + const { hasCapacityLimit, remainingSpots, isFull } = calculateEventCapacity(event, attendeeCount); + + const registrationCta = getRegistrationCta(event); + const shouldShowWaitlistJoin = + !canManageEvent && + event.status === 'published' && + !registrationCta.disabled && + isFull && + !!event.waitlist_enabled; + + useEffect(() => { + function onKey(e) { + if (e.key === 'Escape') onClose(); + } + document.addEventListener('keydown', onKey); + return () => document.removeEventListener('keydown', onKey); + }, [onClose]); + + useEffect(() => { + function onClickOutside(e) { + if (popupRef.current && !popupRef.current.contains(e.target)) { + onClose(); + } + } + const timer = setTimeout(() => document.addEventListener('mousedown', onClickOutside), 0); + return () => { + clearTimeout(timer); + document.removeEventListener('mousedown', onClickOutside); + }; + }, [onClose]); + + useEffect(() => { + let isCurrent = true; + setAttendeeCount(null); + setAttendanceLoaded(false); + setAttendanceLoading(false); + + async function fetchAttendanceSummary() { + if (!eventId || !authToken) { + return; + } + setAttendanceLoading(true); + const response = await getEventAttendanceSummary(eventId, authToken); + if (isCurrent && !response.error && typeof response.responseData?.attendee_count === 'number') { + setAttendeeCount(response.responseData.attendee_count); + } + if (isCurrent) { + setAttendanceLoaded(true); + setAttendanceLoading(false); + } + } + + fetchAttendanceSummary(); + + return () => { + isCurrent = false; + }; + }, [eventId, authToken]); + + async function handleJoinWaitlist() { + setWaitlistError(''); + setWaitlistMessage(''); + + if (!authToken) { + setWaitlistError('You must be logged in to join the waitlist.'); + return; + } + + if (!eventId) { + setWaitlistError('Missing event id.'); + return; + } + + setWaitlistSubmitting(true); + const response = await joinWaitlistForSCEvent(eventId, authToken); + setWaitlistSubmitting(false); + + if (response.error) { + setWaitlistError(getApiErrorMessage(response, { fallback: 'Failed to join waitlist.' })); + return; + } + + setWaitlistMessage('Joined waitlist successfully.'); + } + + return ( +
+
+
+ +
+
+ + + {event.category || event.type || 'Event'} + +
+ +
+ +
+

+ {event.name || 'Untitled Event'} +

+ +
+ {badgeText && ( + + {badgeText} + + )} +
+
+ +
+ {event.date && ( +
+ + {formatDate(event.date)} +
+ )} + + {event.time && ( +
+ + {formatTime(event.time)} +
+ )} + + {event.location && ( +
+ + {event.location} +
+ )} + + {event.description && ( +

+ {event.description} +

+ )} +
+ +
+ {event.max_attendees > 0 && ( +

+ {event.max_attendees} total spot{event.max_attendees !== 1 ? 's' : ''} + {canManageEvent && event.waitlist_enabled && ( + · waitlist available + )} +

+ )} + + {hasCapacityLimit && !canManageEvent && typeof remainingSpots === 'number' && ( +

+ {remainingSpots} spot{remainingSpots !== 1 ? 's' : ''} left + {event.waitlist_enabled && ( + · waitlist available + )} +

+ )} + + {hasCapacityLimit && !canManageEvent && attendanceLoading && ( +

+ Loading live spots... +

+ )} + + {hasCapacityLimit && !canManageEvent && attendanceLoaded && typeof remainingSpots !== 'number' && ( +

+ Unable to load live spots +

+ )} + + {canManageEvent && ( +
+ + View attendees + + + Edit event + +
+ )} + + {!canManageEvent && event.status === 'closed' && ( +
+ Registration closed +
+ )} + + {!canManageEvent && event.status === 'draft' && ( +
+ Not yet published +
+ )} + + {!canManageEvent && event.status === 'published' && registrationCta.disabled && ( +
+ {registrationCta.label} +
+ )} + + {shouldShowWaitlistJoin && ( + + )} + + {!shouldShowWaitlistJoin && !canManageEvent && event.status === 'published' && !registrationCta.disabled && ( + + {registrationCta.label} + + )} + + {waitlistError && ( +

{waitlistError}

+ )} + + {waitlistMessage && ( +

{waitlistMessage}

+ )} +
+
+
+ ); +} diff --git a/src/Pages/Events/Calendar/EventRow.js b/src/Pages/Events/Calendar/EventRow.js new file mode 100644 index 000000000..1589d2b06 --- /dev/null +++ b/src/Pages/Events/Calendar/EventRow.js @@ -0,0 +1,40 @@ +import { formatTime, getBadgeText, pillColors } from './calendarViewUtils'; + +export default function EventRow({ event, onSelect, isAdminView }) { + const colors = pillColors(event, isAdminView); + const badgeText = getBadgeText(event, isAdminView); + const timeLabel = formatTime(event.time); + + return ( + + ); +} diff --git a/src/Pages/Events/Calendar/MobileMonthAgenda.js b/src/Pages/Events/Calendar/MobileMonthAgenda.js new file mode 100644 index 000000000..228d524b2 --- /dev/null +++ b/src/Pages/Events/Calendar/MobileMonthAgenda.js @@ -0,0 +1,86 @@ +import { formatTime, getBadgeText, pillColors } from './calendarViewUtils'; + +export default function MobileMonthAgenda({ monthEvents, onSelectEvent, isAdminView }) { + if (monthEvents.length === 0) { + return ( +
+ No events scheduled this month. +
+ ); + } + + const groupedEvents = monthEvents.reduce((groups, item) => { + const lastGroup = groups[groups.length - 1]; + if (!lastGroup || lastGroup.dateKey !== item.dateKey) { + groups.push({ dateKey: item.dateKey, events: [item.event] }); + return groups; + } + lastGroup.events.push(item.event); + return groups; + }, []); + + return ( +
+ {groupedEvents.map(({ dateKey, events }) => { + const [year, month, day] = dateKey.split('-'); + const date = new Date(Number(year), Number(month) - 1, Number(day)); + const weekday = date.toLocaleDateString('en-US', { weekday: 'short' }).toUpperCase(); + const dayOfMonth = String(date.getDate()); + + return ( +
+
+
+ {weekday} +
+
+ {dayOfMonth} +
+
+ +
+ {events.map((event) => { + const colors = pillColors(event, isAdminView); + const badgeText = getBadgeText(event, isAdminView); + const timeLabel = formatTime(event.time) || 'All day'; + + return ( + + ); + })} +
+
+ ); + })} +
+ ); +} diff --git a/src/Pages/Events/Calendar/calendarViewUtils.js b/src/Pages/Events/Calendar/calendarViewUtils.js new file mode 100644 index 000000000..342ef0330 --- /dev/null +++ b/src/Pages/Events/Calendar/calendarViewUtils.js @@ -0,0 +1,201 @@ +import { membershipState } from '../../../Enums'; +import { toDateKey } from '../eventUtils'; + +export const DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; +export const MONTHS = [ + 'January', 'February', 'March', 'April', 'May', 'June', + 'July', 'August', 'September', 'October', 'November', 'December', +]; +const currentYear = new Date().getFullYear(); +export const YEAR_RANGE = Array.from({ length: 10 }, (_, i) => currentYear + i); + +export function eventDateKey(event) { + if (!event.date) return null; + if (/^\d{4}-\d{2}-\d{2}$/.test(event.date)) return event.date; + const d = new Date(event.date); + if (isNaN(d)) return null; + return toDateKey(d); +} + +function toTimeSortValue(event) { + if (!event?.time) return Number.POSITIVE_INFINITY; + const [hour, minute] = String(event.time).split(':').map(Number); + if (Number.isNaN(hour) || Number.isNaN(minute)) return Number.POSITIVE_INFINITY; + return hour * 60 + minute; +} + +export function sortEventsForDay(events) { + return [...events].sort((a, b) => { + const timeDelta = toTimeSortValue(a) - toTimeSortValue(b); + if (timeDelta !== 0) return timeDelta; + return String(a.name || '').localeCompare(String(b.name || '')); + }); +} + +export function formatDate(dateStr) { + if (!dateStr) return ''; + const d = new Date(`${dateStr}T12:00:00`); + if (isNaN(d)) return dateStr; + return d.toLocaleDateString('en-US', { + weekday: 'long', + month: 'long', + day: 'numeric', + year: 'numeric', + }); +} + +export function formatTime(timeStr) { + if (!timeStr) return ''; + const [hours, minutes] = String(timeStr).split(':'); + if (hours == null || minutes == null) return timeStr; + + const h = Number(hours); + const m = Number(minutes); + if (Number.isNaN(h) || Number.isNaN(m)) return timeStr; + + return new Date(2000, 0, 1, h, m).toLocaleTimeString('en-US', { + hour: 'numeric', + minute: '2-digit', + }); +} + +export function pillColors(event, isAdminView) { + const status = event.status || 'draft'; + const visibility = event.visibility || 'public'; + + if (isAdminView) { + if (status === 'draft') { + return { + bg: 'bg-amber-500/12', + text: 'text-amber-200', + border: 'border-amber-400/40', + dot: 'bg-amber-300', + accent: 'text-amber-300', + }; + } + + if (status === 'closed') { + return { + bg: 'bg-rose-500/12', + text: 'text-rose-200', + border: 'border-rose-400/40', + dot: 'bg-rose-300', + accent: 'text-rose-300', + }; + } + + if (visibility === 'private') { + return { + bg: 'bg-violet-500/12', + text: 'text-violet-200', + border: 'border-violet-400/40', + dot: 'bg-violet-300', + accent: 'text-violet-300', + }; + } + + return { + bg: 'bg-cyan-500/12', + text: 'text-cyan-100', + border: 'border-cyan-400/40', + dot: 'bg-cyan-300', + accent: 'text-cyan-300', + }; + } + + if (status === 'closed') { + return { + bg: 'bg-rose-500/16', + text: 'text-rose-200', + border: 'border-rose-400/30', + dot: 'bg-rose-300', + accent: 'text-rose-300', + }; + } + + if (visibility === 'private') { + return { + bg: 'bg-violet-500/16', + text: 'text-violet-200', + border: 'border-violet-400/30', + dot: 'bg-violet-300', + accent: 'text-violet-300', + }; + } + + return { + bg: 'bg-cyan-500/16', + text: 'text-cyan-100', + border: 'border-cyan-400/30', + dot: 'bg-cyan-300', + accent: 'text-cyan-300', + }; +} + +export function getBadgeText(event, isAdminView) { + const status = event.status || 'draft'; + const visibility = event.visibility || 'public'; + + if (isAdminView) { + if (status === 'draft') return 'Draft'; + if (status === 'closed') return 'Closed'; + if (visibility === 'private') return 'Private'; + return 'Published'; + } + + if (status === 'closed') return 'Closed'; + if (visibility === 'private') return 'Members only'; + return ''; +} + +export function getRegistrationCta(event) { + const registrationStatus = event?.registration_status || 'none'; + + switch (registrationStatus) { + case 'registered': + return { + label: 'Registered', + disabled: true, + className: 'border border-emerald-400/30 bg-emerald-500/10 text-emerald-200', + }; + case 'pending': + return { + label: 'Pending', + disabled: true, + className: 'border border-amber-400/30 bg-amber-500/10 text-amber-200', + }; + case 'waitlisted': + return { + label: 'On waitlist', + disabled: true, + className: 'border border-violet-400/30 bg-violet-500/10 text-violet-200', + }; + case 'rejected': + return { + label: 'Unavailable', + disabled: true, + className: 'border border-slate-500/40 bg-slate-700/40 text-slate-300', + }; + default: + return { + label: 'Register', + disabled: false, + className: '', + }; + } +} + +export function canUserManageEvent(event, user) { + const userId = user?._id != null ? String(user._id) : ''; + const eventAdmins = Array.isArray(event.admins) ? event.admins.map((id) => String(id)) : []; + + if (eventAdmins.length > 0 && userId && eventAdmins.includes(userId)) { + return true; + } + + if (eventAdmins.length === 0 && (user?.accessLevel ?? 0) >= membershipState.ADMIN) { + return true; + } + + return false; +} diff --git a/src/Pages/Events/CalendarView.js b/src/Pages/Events/CalendarView.js deleted file mode 100644 index 77a6ef570..000000000 --- a/src/Pages/Events/CalendarView.js +++ /dev/null @@ -1,959 +0,0 @@ -import { useState, useMemo, useEffect, useRef } from 'react'; -import { Link } from 'react-router-dom'; -import { getEventAttendanceSummary, getMyEventRegistrationState, joinWaitlistForSCEvent } from '../../APIFunctions/SCEvents'; -import { membershipState } from '../../Enums'; -import { calculateEventCapacity, getApiErrorMessage, toDateKey } from './eventUtils'; - -const DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; -const MONTHS = [ - 'January', 'February', 'March', 'April', 'May', 'June', - 'July', 'August', 'September', 'October', 'November', 'December', -]; -const currentYear = new Date().getFullYear(); -const YEAR_RANGE = Array.from({ length: 10 }, (_, i) => currentYear + i); - -function eventDateKey(event) { - if (!event.date) return null; - if (/^\d{4}-\d{2}-\d{2}$/.test(event.date)) return event.date; - const d = new Date(event.date); - if (isNaN(d)) return null; - return toDateKey(d); -} - -function toTimeSortValue(event) { - if (!event?.time) return Number.POSITIVE_INFINITY; - const [hour, minute] = String(event.time).split(':').map(Number); - if (Number.isNaN(hour) || Number.isNaN(minute)) return Number.POSITIVE_INFINITY; - return hour * 60 + minute; -} - -function sortEventsForDay(events) { - return [...events].sort((a, b) => { - const timeDelta = toTimeSortValue(a) - toTimeSortValue(b); - if (timeDelta !== 0) return timeDelta; - return String(a.name || '').localeCompare(String(b.name || '')); - }); -} - -function formatDate(dateStr) { - if (!dateStr) return ''; - const d = new Date(`${dateStr}T12:00:00`); - if (isNaN(d)) return dateStr; - return d.toLocaleDateString('en-US', { - weekday: 'long', - month: 'long', - day: 'numeric', - year: 'numeric', - }); -} - -function formatTime(timeStr) { - if (!timeStr) return ''; - const [hours, minutes] = String(timeStr).split(':'); - if (hours == null || minutes == null) return timeStr; - - const h = Number(hours); - const m = Number(minutes); - if (Number.isNaN(h) || Number.isNaN(m)) return timeStr; - - return new Date(2000, 0, 1, h, m).toLocaleTimeString('en-US', { - hour: 'numeric', - minute: '2-digit', - }); -} - -function pillColors(event, isAdminView) { - const status = event.status || 'draft'; - const visibility = event.visibility || 'public'; - - if (isAdminView) { - if (status === 'draft') { - return { - bg: 'bg-amber-500/12', - text: 'text-amber-200', - border: 'border-amber-400/40', - dot: 'bg-amber-300', - accent: 'text-amber-300', - }; - } - - if (status === 'closed') { - return { - bg: 'bg-rose-500/12', - text: 'text-rose-200', - border: 'border-rose-400/40', - dot: 'bg-rose-300', - accent: 'text-rose-300', - }; - } - - if (visibility === 'private') { - return { - bg: 'bg-violet-500/12', - text: 'text-violet-200', - border: 'border-violet-400/40', - dot: 'bg-violet-300', - accent: 'text-violet-300', - }; - } - - return { - bg: 'bg-cyan-500/12', - text: 'text-cyan-100', - border: 'border-cyan-400/40', - dot: 'bg-cyan-300', - accent: 'text-cyan-300', - }; - } - - if (status === 'closed') { - return { - bg: 'bg-rose-500/16', - text: 'text-rose-200', - border: 'border-rose-400/30', - dot: 'bg-rose-300', - accent: 'text-rose-300', - }; - } - - if (visibility === 'private') { - return { - bg: 'bg-violet-500/16', - text: 'text-violet-200', - border: 'border-violet-400/30', - dot: 'bg-violet-300', - accent: 'text-violet-300', - }; - } - - return { - bg: 'bg-cyan-500/16', - text: 'text-cyan-100', - border: 'border-cyan-400/30', - dot: 'bg-cyan-300', - accent: 'text-cyan-300', - }; -} - -function getBadgeText(event, isAdminView) { - const status = event.status || 'draft'; - const visibility = event.visibility || 'public'; - - if (isAdminView) { - if (status === 'draft') return 'Draft'; - if (status === 'closed') return 'Closed'; - if (visibility === 'private') return 'Private'; - return 'Published'; - } - - if (status === 'closed') return 'Closed'; - if (visibility === 'private') return 'Members only'; - return ''; -} - -function getRegistrationCta(event) { - const registrationStatus = event?.registration_status || 'none'; - - switch (registrationStatus) { - case 'registered': - return { - label: 'Registered', - disabled: true, - className: 'border border-emerald-400/30 bg-emerald-500/10 text-emerald-200', - }; - case 'pending': - return { - label: 'Pending', - disabled: true, - className: 'border border-amber-400/30 bg-amber-500/10 text-amber-200', - }; - case 'waitlisted': - return { - label: 'On waitlist', - disabled: true, - className: 'border border-violet-400/30 bg-violet-500/10 text-violet-200', - }; - case 'rejected': - return { - label: 'Unavailable', - disabled: true, - className: 'border border-slate-500/40 bg-slate-700/40 text-slate-300', - }; - default: - return { - label: 'Register', - disabled: false, - className: '', - }; - } -} - -/** - * Checks if a user has permission to manage an event. - * @param {Object} event The event object. - * @param {Object} user The user object. - * @returns {boolean} True if the user can manage the event. - */ -function canUserManageEvent(event, user) { - const userId = user?._id != null ? String(user._id) : ''; - const eventAdmins = Array.isArray(event.admins) ? event.admins.map((id) => String(id)) : []; - - // 1. If user is explicitly listed as an admin for this event - if (eventAdmins.length > 0 && userId && eventAdmins.includes(userId)) { - return true; - } - - // 2. If the event has no admins, allow users with level 3 (ADMIN) or higher to manage it - if (eventAdmins.length === 0 && (user?.accessLevel ?? 0) >= membershipState.ADMIN) { - return true; - } - - return false; -} - -// ─── icons ──────────────────────────────────────────────────────────────────── - -import { - ChevronLeft, - ChevronRight, - CalendarIcon, - PinIcon, - ClockIcon, - XIcon, - PlusIcon -} from './EventIcons'; - -// ─── Event popup ────────────────────────────────────────────────────────────── - -function EventPopup({ event, onClose, isAdminView, user }) { - const popupRef = useRef(null); - const colors = pillColors(event, isAdminView); - const badgeText = getBadgeText(event, isAdminView); - 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 canManageEvent = canUserManageEvent(event, user); - const [hasRegistered, setHasRegistered] = useState(false); - const [isCheckingRegistration, setIsCheckingRegistration] = useState(false); - const eventId = event?.id || event?._id; - const authToken = user?.token; - - const { maxAttendees, hasCapacityLimit, remainingSpots, isFull } = calculateEventCapacity(event, attendeeCount); - - const registrationCta = getRegistrationCta(event); - const shouldShowWaitlistJoin = - !canManageEvent && - event.status === 'published' && - !registrationCta.disabled && - isFull && - !!event.waitlist_enabled; - - useEffect(() => { - function onKey(e) { - if (e.key === 'Escape') onClose(); - } - document.addEventListener('keydown', onKey); - 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)) { - onClose(); - } - } - const timer = setTimeout(() => document.addEventListener('mousedown', onClickOutside), 0); - return () => { - clearTimeout(timer); - document.removeEventListener('mousedown', onClickOutside); - }; - }, [onClose]); - - useEffect(() => { - let isCurrent = true; - setAttendeeCount(null); - setAttendanceLoaded(false); - setAttendanceLoading(false); - - async function fetchAttendanceSummary() { - if (!eventId || !authToken) { - return; - } - setAttendanceLoading(true); - const response = await getEventAttendanceSummary(eventId, authToken); - if (isCurrent && !response.error && typeof response.responseData?.attendee_count === 'number') { - setAttendeeCount(response.responseData.attendee_count); - } - if (isCurrent) { - setAttendanceLoaded(true); - setAttendanceLoading(false); - } - } - - fetchAttendanceSummary(); - - return () => { - isCurrent = false; - }; - }, [eventId, authToken]); - - async function handleJoinWaitlist() { - setWaitlistError(''); - setWaitlistMessage(''); - - if (!authToken) { - setWaitlistError('You must be logged in to join the waitlist.'); - return; - } - - if (!eventId) { - setWaitlistError('Missing event id.'); - return; - } - - setWaitlistSubmitting(true); - const response = await joinWaitlistForSCEvent(eventId, authToken); - setWaitlistSubmitting(false); - - if (response.error) { - setWaitlistError(getApiErrorMessage(response, { fallback: 'Failed to join waitlist.' })); - return; - } - - setWaitlistMessage('Joined waitlist successfully.'); - } - return ( -
-
-
- -
-
- - - {event.category || event.type || 'Event'} - -
- -
- -
-

- {event.name || 'Untitled Event'} -

- -
- {badgeText && ( - - {badgeText} - - )} -
-
- -
- {event.date && ( -
- - {formatDate(event.date)} -
- )} - - {event.time && ( -
- - {formatTime(event.time)} -
- )} - - {event.location && ( -
- - {event.location} -
- )} - - {event.description && ( -

- {event.description} -

- )} -
- -
- {event.max_attendees > 0 && ( -

- {event.max_attendees} total spot{event.max_attendees !== 1 ? 's' : ''} - {canManageEvent && event.waitlist_enabled && ( - · waitlist available - )} -

- )} - - {hasCapacityLimit && !canManageEvent && typeof remainingSpots === 'number' && ( -

- {remainingSpots} spot{remainingSpots !== 1 ? 's' : ''} left - {event.waitlist_enabled && ( - · waitlist available - )} -

- )} - - {hasCapacityLimit && !canManageEvent && attendanceLoading && ( -

- Loading live spots... -

- )} - - {hasCapacityLimit && !canManageEvent && attendanceLoaded && typeof remainingSpots !== 'number' && ( -

- Unable to load live spots -

- )} - {canManageEvent && ( -
- - View attendees - - - Edit event - -
- )} - - {!canManageEvent && event.status === 'closed' && ( -
- Registration closed -
- )} - - {!canManageEvent && event.status === 'draft' && ( -
- Not yet published -
- )} - - {!canManageEvent && event.status === 'published' && registrationCta.disabled && ( -
- {registrationCta.label} -
- )} - - {shouldShowWaitlistJoin && ( - - )} - - {!shouldShowWaitlistJoin && !canManageEvent && event.status === 'published' && !registrationCta.disabled && ( - - {registrationCta.label} - - )} - - {waitlistError && ( -

{waitlistError}

- )} - - {waitlistMessage && ( -

{waitlistMessage}

- )} -
-
-
- ); -} - -// ─── Event pill ─────────────────────────────────────────────────────────────── - -function EventRow({ event, onSelect, isAdminView }) { - const colors = pillColors(event, isAdminView); - const badgeText = getBadgeText(event, isAdminView); - const timeLabel = formatTime(event.time); - - return ( - - ); -} - -// ─── Day labels ─────────────────────────────────────────────────────────────── - -function DayLabels() { - return ( -
- {DAYS.map((d) => ( -
- {d} -
- ))} -
- ); -} - -// ─── Calendar cell ──────────────────────────────────────────────────────────── - -function CalCell({ date, isCurrentMonth, isToday, events, onSelectEvent, isAdminView }) { - const sortedEvents = sortEventsForDay(events); - const mobileRows = sortedEvents.slice(0, 2); - const desktopOnlyRows = sortedEvents.slice(2, 4); - const hiddenCount = Math.max(sortedEvents.length - 4, 0); - - return ( -
-
- - {date.getDate()} - -
- -
- {mobileRows.map((ev) => ( - - ))} - - {desktopOnlyRows.map((ev) => ( -
- -
- ))} - - {hiddenCount > 0 && ( - - +{hiddenCount} more - - )} -
-
- ); -} - -function MobileMonthAgenda({ monthEvents, onSelectEvent, isAdminView }) { - if (monthEvents.length === 0) { - return ( -
- No events scheduled this month. -
- ); - } - - const groupedEvents = monthEvents.reduce((groups, item) => { - const lastGroup = groups[groups.length - 1]; - if (!lastGroup || lastGroup.dateKey !== item.dateKey) { - groups.push({ dateKey: item.dateKey, events: [item.event] }); - return groups; - } - lastGroup.events.push(item.event); - return groups; - }, []); - - return ( -
- {groupedEvents.map(({ dateKey, events }) => { - const [year, month, day] = dateKey.split('-'); - const date = new Date(Number(year), Number(month) - 1, Number(day)); - const weekday = date.toLocaleDateString('en-US', { weekday: 'short' }).toUpperCase(); - const dayOfMonth = String(date.getDate()); - - return ( -
-
-
- {weekday} -
-
- {dayOfMonth} -
-
- -
- {events.map((event) => { - const colors = pillColors(event, isAdminView); - const badgeText = getBadgeText(event, isAdminView); - const timeLabel = formatTime(event.time) || 'All day'; - - return ( - - ); - })} -
-
- ); - })} -
- ); -} - -// ─── Main export ────────────────────────────────────────────────────────────── - -export default function CalendarView({ - events, - isAdminView = false, - user, - canCreateEvent = false, - cursor, - setCursor, -}) { - const today = useMemo(() => new Date(), []); - const [selectedEvent, setSelectedEvent] = useState(null); - - const eventsByDate = useMemo(() => { - const map = {}; - events.forEach((ev) => { - const key = eventDateKey(ev); - if (!key) return; - if (!map[key]) map[key] = []; - map[key].push(ev); - }); - Object.keys(map).forEach((key) => { - map[key] = sortEventsForDay(map[key]); - }); - return map; - }, [events]); - - const year = cursor.getFullYear(); - const month = cursor.getMonth(); - - function handleMonthChange(e) { - const nextMonth = Number(e.target.value); - setCursor(new Date(year, nextMonth, 1)); - } - - function handleYearChange(e) { - const nextYear = Number(e.target.value); - setCursor(new Date(nextYear, month, 1)); - } - - const firstDayOfMonth = new Date(year, month, 1).getDay(); - const daysInMonth = new Date(year, month + 1, 0).getDate(); - const totalCells = Math.ceil((firstDayOfMonth + daysInMonth) / 7) * 7; - const todayKey = toDateKey(today); - - const cells = Array.from({ length: totalCells }, (_, i) => { - const dayOffset = i - firstDayOfMonth; - const date = new Date(year, month, dayOffset + 1); - const key = toDateKey(date); - return { - date, - key, - isCurrentMonth: date.getMonth() === month, - isToday: key === todayKey, - events: eventsByDate[key] || [], - }; - }); - - const monthEventCount = cells - .filter((c) => c.isCurrentMonth) - .reduce((sum, c) => sum + c.events.length, 0); - - const monthEvents = useMemo(() => { - return cells - .filter((c) => c.isCurrentMonth && c.events.length > 0) - .flatMap((c) => - c.events.map((event) => ({ - event, - dateKey: c.key, - })) - ); - }, [cells]); - - return ( - <> - {selectedEvent && ( - setSelectedEvent(null)} - isAdminView={isAdminView} - user={user} - /> - )} - -
-
-
-
-
- - -
- - - -
-
- -
- - -
- - - -
-
- - -
- -

- {monthEventCount} event{monthEventCount !== 1 ? 's' : ''} this month -

-
- -
- {canCreateEvent && ( - - - - )} - - - -
-
- -
- -
- -
- - -
- {cells.map((cell) => ( - - ))} -
-
- - {isAdminView ? ( -
- {[ - { label: 'Published', dot: 'bg-cyan-300' }, - { label: 'Private', dot: 'bg-violet-300' }, - { label: 'Draft', dot: 'bg-amber-300' }, - { label: 'Closed', dot: 'bg-rose-300' }, - ].map(({ label, dot }) => ( - - - {label} - - ))} -
- ) : ( -
- {[ - { label: 'Open', dot: 'bg-cyan-300' }, - { label: 'Members only', dot: 'bg-violet-300' }, - { label: 'Closed', dot: 'bg-rose-300' }, - ].map(({ label, dot }) => ( - - - {label} - - ))} -
- )} -
- - ); -} diff --git a/src/Pages/Events/Events.js b/src/Pages/Events/Events.js index 12948aa3f..f22368fff 100644 --- a/src/Pages/Events/Events.js +++ b/src/Pages/Events/Events.js @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'; import { getAllSCEvents } from '../../APIFunctions/SCEvents'; import { useSCE } from '../../Components/context/SceContext'; import { membershipState } from '../../Enums'; -import CalendarView from './CalendarView'; +import CalendarView from './Calendar/CalendarView'; import { toDateKey } from './eventUtils'; function canUserSeeEvent(event, user) { From 794c6f196ffa9bdbf4b13503f577671dc6aa1ad2 Mon Sep 17 00:00:00 2001 From: Steven Le Date: Fri, 1 May 2026 09:24:13 -0700 Subject: [PATCH 3/4] share code between edit + create event forms --- src/Pages/Events/CreateEventPage.js | 312 ++++---------------------- src/Pages/Events/EditEventPage.js | 291 ++++-------------------- src/Pages/Events/EventEditorForm.js | 336 ++++++++++++++++++++++++++++ 3 files changed, 434 insertions(+), 505 deletions(-) create mode 100644 src/Pages/Events/EventEditorForm.js diff --git a/src/Pages/Events/CreateEventPage.js b/src/Pages/Events/CreateEventPage.js index a8b9027fe..58f055ee1 100644 --- a/src/Pages/Events/CreateEventPage.js +++ b/src/Pages/Events/CreateEventPage.js @@ -3,10 +3,10 @@ import { useMemo, useState } from 'react'; import { Link, useHistory } from 'react-router-dom'; import { useSCE } from '../../Components/context/SceContext'; import { createSCEvent } from '../../APIFunctions/SCEvents'; -import CreateEventFormQuestionBlock from './CreateEventFormQuestionBlock'; import { membershipState } from '../../Enums'; import { useEventQuestions, toApiRegistrationForm } from './useEventQuestions'; import { getApiErrorMessage } from './eventUtils'; +import EventEditorForm from './EventEditorForm'; /** Matches SCEvents `max_attendees` when there is no cap. */ const UNLIMITED_ATTENDEES = -1; @@ -155,267 +155,53 @@ export default function CreateEventPage() { } return ( -
-
- - ← Back to Events - -

- Create event -

-

Event id: {eventId}

-
- -

Event details

-
- - -
- - -
- - - -