diff --git a/src/APIFunctions/SCEvents.js b/src/APIFunctions/SCEvents.js index 2d4ffafff..a8a83e556 100644 --- a/src/APIFunctions/SCEvents.js +++ b/src/APIFunctions/SCEvents.js @@ -1,7 +1,6 @@ import { ApiResponse } from './ApiResponses'; const SCEVENTS_API_URL = 'http://localhost:8002'; - export async function getAllSCEvents() { const status = new ApiResponse(); try { @@ -55,3 +54,25 @@ export async function createSCEvent(eventBody) { } return status; } + +export async function updateSCEvent(id, userId, eventUpdates) { + const status = new ApiResponse(); + try { + const res = await fetch(`${SCEVENTS_API_URL}/events/${id}?user_id=${userId}`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(eventUpdates), + }); + const body = await res.json(); + status.responseData = body; + if (!res.ok) { + status.error = true; + } + } catch (err) { + status.error = true; + status.responseData = err; + } + return status; +} diff --git a/src/Pages/Events/CreateEventPage.js b/src/Pages/Events/CreateEventPage.js index 6c65efe80..a76d04fb6 100644 --- a/src/Pages/Events/CreateEventPage.js +++ b/src/Pages/Events/CreateEventPage.js @@ -238,8 +238,8 @@ export default function CreateEventPage() { return (
- - ← Events + + ← Back to Events

Create event diff --git a/src/Pages/Events/EditEventPage.js b/src/Pages/Events/EditEventPage.js new file mode 100644 index 000000000..47859621c --- /dev/null +++ b/src/Pages/Events/EditEventPage.js @@ -0,0 +1,400 @@ +/* eslint-disable camelcase -- mirrors SCEvents JSON field names in state and payloads */ +import React, { useMemo, useState, useEffect } from 'react'; +import { Link, useHistory, useParams } from 'react-router-dom'; +import { useSCE } from '../../Components/context/SceContext.js'; +import { getEventByID, updateSCEvent } from '../../APIFunctions/SCEvents.js'; +import CreateEventFormQuestionBlock from './CreateEventFormQuestionBlock.js'; +import { membershipState } from '../../Enums'; + +/** 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(); + const history = useHistory(); + + const [isLoading, setIsLoading] = useState(true); + const [fetchError, setFetchError] = useState(''); + + const [eventName, setEventName] = useState(''); + const [date, setDate] = useState(''); + const [time, setTime] = useState(''); + const [location, setLocation] = useState(''); + const [description, setDescription] = useState(''); + const [maxAttendees, setMaxAttendees] = useState(UNLIMITED_ATTENDEES); + const [questions, setQuestions] = useState([]); + const [eventAdmins, setEventAdmins] = useState([]); + + const [submitError, setSubmitError] = useState(''); + const [submitting, setSubmitting] = useState(false); + + const isOfficerOrAdmin = user?.accessLevel >= membershipState.OFFICER; + const userId = useMemo(() => (user?._id != null ? String(user._id) : ''), [user]); + + useEffect(() => { + async function loadEvent() { + setIsLoading(true); + const result = await getEventByID(id); + setIsLoading(false); + + if (result.error) { + setFetchError('Failed to load event details.'); + return; + } + + const evt = result.responseData; + setEventName(evt.name || ''); + setDate(evt.date || ''); + setTime(evt.time || ''); + setLocation(evt.location || ''); + setDescription(evt.description || ''); + setMaxAttendees(evt.max_attendees || UNLIMITED_ATTENDEES); + setQuestions(evt.registration_form || []); + setEventAdmins(evt.admins || []); + } + 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()) { + setSubmitError('Please enter an event name.'); + return; + } + + const payload = { + name: eventName.trim(), + date, + time, + location: location.trim(), + description: description.trim(), + registration_form: toApiRegistrationForm(questions), + max_attendees: + maxAttendees === UNLIMITED_ATTENDEES ? UNLIMITED_ATTENDEES : Number(maxAttendees), + }; + + setSubmitting(true); + const result = await updateSCEvent(id, userId, payload); + 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); + return; + } + + history.push('/events'); + } + + if (!isOfficerOrAdmin) { + return ( +
+

+ Edit event +

+

+ Only officers and administrators can edit events. +

+ + Back to events + +
+ ); + } + + if (isLoading) { + return
Loading event details...
; + } + + if (fetchError) { + return ( +
+ {fetchError} +
+ + Back to events + +
+ ); + } + + const isEventAdmin = eventAdmins.includes(userId); + if (!isEventAdmin) { + return ( +
+

+ Edit event +

+

+ You are not an admin of this event. +

+ + Back to events + +
+ ); + } + + return ( +
+
+ + ← Back to Events + +

+ Edit event +

+

Event id: {id}

+
+ +

Event details

+
+ + +
+ + +
+ + + +