Skip to content
Merged
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
180 changes: 46 additions & 134 deletions src/Pages/Events/CalendarView.js

Large diffs are not rendered by default.

2 changes: 0 additions & 2 deletions src/Pages/Events/CreateEventFormQuestionBlock.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
/* eslint-disable camelcase -- SCEvents registration question shape uses snake_case */
import React from 'react';

export default function CreateEventFormQuestionBlock({
question,
index,
Expand Down
142 changes: 20 additions & 122 deletions src/Pages/Events/CreateEventPage.js
Original file line number Diff line number Diff line change
@@ -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 [
{
Expand All @@ -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();
Expand All @@ -91,84 +61,25 @@ 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);

const isOfficerOrAdmin = user?.accessLevel >= membershipState.OFFICER;

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()) {
Expand All @@ -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.');
Expand Down Expand Up @@ -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;
}

Expand Down
140 changes: 18 additions & 122 deletions src/Pages/Events/EditEventPage.js
Original file line number Diff line number Diff line change
@@ -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();
Expand All @@ -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);
Expand Down Expand Up @@ -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()) {
Expand Down Expand Up @@ -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;
}

Expand Down
2 changes: 1 addition & 1 deletion src/Pages/Events/EventAttendeesDashboard.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
Loading
Loading