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
68 changes: 66 additions & 2 deletions src/Pages/Events/CreateEventPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ export default function CreateEventPage() {
});
const [location, setLocation] = useState('');
const [description, setDescription] = useState('');
const [status, setStatus] = useState('draft');
const [visibility, setVisibility] = useState('public');
const [minimumVisibleRole, setMinimumVisibleRole] = useState('');
const [maxAttendees, setMaxAttendees] = useState(UNLIMITED_ATTENDEES);
const [questions, setQuestions] = useState(defaultQuestions);
const [submitError, setSubmitError] = useState('');
Expand Down Expand Up @@ -176,6 +179,10 @@ export default function CreateEventPage() {
setSubmitError('Could not resolve your user id.');
return;
}
if (visibility === 'private' && !minimumVisibleRole) {
setSubmitError('Please select a minimum visible role for private events.');
return;
}

const payload = {
id: eventId,
Expand All @@ -184,12 +191,14 @@ export default function CreateEventPage() {
time,
location: location.trim(),
description: description.trim(),
admins: [adminId],
admins: [adminId], // The event creator becomes the initial event admin in SCEvents
registration_form: toApiRegistrationForm(questions),
max_attendees:
maxAttendees === UNLIMITED_ATTENDEES ? UNLIMITED_ATTENDEES : Number(maxAttendees),
created_at: new Date().toISOString(),
status: 'draft',
status,
visibility,
minimum_visible_role: visibility === 'private' ? minimumVisibleRole : '',
};

setSubmitting(true);
Expand Down Expand Up @@ -336,6 +345,61 @@ export default function CreateEventPage() {
placeholder="No limit"
/>
</div>

<div className="grid gap-4 sm:grid-cols-2">
<label className="w-full form-control">
<div className="label">
<span className="label-text">Publish status</span>
</div>
<select
className="w-full select select-bordered"
value={status}
onChange={(e) => setStatus(e.target.value)}
>
<option value="draft">Draft</option>
<option value="published">Published</option>
<option value="closed">Closed</option>
</select>
</label>

<label className="w-full form-control">
<div className="label">
<span className="label-text">Visibility</span>
</div>
<select
className="w-full select select-bordered"
value={visibility}
onChange={(e) => {
const nextVisibility = e.target.value;
setVisibility(nextVisibility);
if (nextVisibility !== 'private') {
setMinimumVisibleRole('');
}
}}
>
<option value="public">Public</option>
<option value="private">Private</option>
</select>
</label>
</div>

{visibility === 'private' && (
<label className="w-full form-control">
<div className="label">
<span className="label-text">Minimum visible role</span>
</div>
<select
className="w-full max-w-xs select select-bordered"
value={minimumVisibleRole}
onChange={(e) => setMinimumVisibleRole(e.target.value)}
>
<option value="">Select role</option>
<option value="member">Member</option>
<option value="officer">Officer</option>
<option value="admin">Admin</option>
</select>
</label>
)}
</div>

<h2 className="mb-3 text-xl font-semibold text-gray-900 dark:text-white">
Expand Down
74 changes: 73 additions & 1 deletion src/Pages/Events/EditEventPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ export default function EditEventPage() {
const [time, setTime] = useState('');
const [location, setLocation] = useState('');
const [description, setDescription] = useState('');
const [status, setStatus] = useState('draft');
const [visibility, setVisibility] = useState('public');
const [minimumVisibleRole, setMinimumVisibleRole] = useState('');
const [maxAttendees, setMaxAttendees] = useState(UNLIMITED_ATTENDEES);
const [questions, setQuestions] = useState([]);
const [eventAdmins, setEventAdmins] = useState([]);
Expand Down Expand Up @@ -83,7 +86,12 @@ export default function EditEventPage() {
setTime(evt.time || '');
setLocation(evt.location || '');
setDescription(evt.description || '');
setMaxAttendees(evt.max_attendees || UNLIMITED_ATTENDEES);
setStatus(evt.status || 'draft');
setVisibility(evt.visibility || 'public');
setMinimumVisibleRole(evt.minimum_visible_role || '');
setMaxAttendees(
typeof evt.max_attendees === 'number' ? evt.max_attendees : UNLIMITED_ATTENDEES,
);
setQuestions(evt.registration_form || []);
setEventAdmins(evt.admins || []);
}
Expand Down Expand Up @@ -172,6 +180,11 @@ export default function EditEventPage() {
return;
}

if (visibility === 'private' && !minimumVisibleRole) {
setSubmitError('Please select a minimum visible role for private events.');
return;
}

const payload = {
name: eventName.trim(),
date,
Expand All @@ -181,6 +194,9 @@ export default function EditEventPage() {
registration_form: toApiRegistrationForm(questions),
max_attendees:
maxAttendees === UNLIMITED_ATTENDEES ? UNLIMITED_ATTENDEES : Number(maxAttendees),
status,
visibility,
minimum_visible_role: visibility === 'private' ? minimumVisibleRole : '',
};

setSubmitting(true);
Expand Down Expand Up @@ -249,6 +265,7 @@ export default function EditEventPage() {
);
}

// Edit access: only users listed in event.admins can update an event
const isEventAdmin = eventAdmins.includes(userId);
if (!isEventAdmin) {
return (
Expand Down Expand Up @@ -360,6 +377,61 @@ export default function EditEventPage() {
placeholder="No limit"
/>
</div>

<div className="grid gap-4 sm:grid-cols-2">
<label className="w-full form-control">
<div className="label">
<span className="label-text">Publish status</span>
</div>
<select
className="w-full select select-bordered"
value={status}
onChange={(e) => setStatus(e.target.value)}
>
<option value="draft">Draft</option>
<option value="published">Published</option>
<option value="closed">Closed</option>
</select>
</label>

<label className="w-full form-control">
<div className="label">
<span className="label-text">Visibility</span>
</div>
<select
className="w-full select select-bordered"
value={visibility}
onChange={(e) => {
const nextVisibility = e.target.value;
setVisibility(nextVisibility);
if (nextVisibility !== 'private') {
setMinimumVisibleRole('');
}
}}
>
<option value="public">Public</option>
<option value="private">Private</option>
</select>
</label>
</div>

{visibility === 'private' && (
<label className="w-full form-control">
<div className="label">
<span className="label-text">Minimum visible role</span>
</div>
<select
className="w-full max-w-xs select select-bordered"
value={minimumVisibleRole}
onChange={(e) => setMinimumVisibleRole(e.target.value)}
>
<option value="">Select role</option>
<option value="member">Member</option>
<option value="officer">Officer</option>
<option value="admin">Admin</option>
</select>
</label>
)}
</div>

<h2 className="mb-3 text-xl font-semibold text-gray-900 dark:text-white">
Expand Down
65 changes: 60 additions & 5 deletions src/Pages/Events/Events.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,16 +74,57 @@ function EditIcon() {
);
}

function getUserAccessLevel(user) {
return user?.accessLevel ?? membershipState.NON_MEMBER;
}

function canUserSeeEvent(event, user) {
const userId = user?._id != null ? String(user._id) : '';
const userAccess = getUserAccessLevel(user);

const isGlobalAdmin = userAccess >= membershipState.ADMIN;
const isEventAdmin = Array.isArray(event.admins) && userId
? event.admins.includes(userId)
: false;

const status = event.status || 'draft';
const visibility = event.visibility || 'public';
const minimumVisibleRole = event.minimum_visible_role || '';

// Draft events are only visible to global admins or users listed in event.admins
if (status === 'draft') {
return isGlobalAdmin || isEventAdmin;
}

// Public → everyone
if (visibility === 'public') {
return true;
}

// Private → compare access levels directly
if (visibility === 'private') {
const requiredLevel = membershipState[minimumVisibleRole?.toUpperCase()];
if (requiredLevel === undefined) return false;

return userAccess >= requiredLevel;
}

return false;
}

function EventCard({ event, user }) {
const isAdmin = event.admins && user?._id && event.admins.includes(String(user._id));
const isEventAdmin =
Array.isArray(event.admins) && user?._id
? event.admins.includes(String(user._id))
: false;

return (
<div className="group relative flex flex-col rounded-2xl border border-white/10 bg-white/5 p-6 shadow-md backdrop-blur-sm transition duration-300 hover:-translate-y-1 hover:border-white/20 hover:bg-white/[0.07]">
<div className="flex items-start justify-between">
<h2 className="mb-4 pr-4 text-2xl font-bold text-white">
{event.name || 'Untitled Event'}
</h2>
{isAdmin && (
{isEventAdmin && (
<Link
to={`/events/${event.id}/edit`}
className="rounded-lg p-2 text-gray-400 hover:bg-white/10 hover:text-emerald-400 transition-colors duration-200"
Expand All @@ -94,6 +135,19 @@ function EventCard({ event, user }) {
)}
</div>

<div className="mb-4 flex flex-wrap gap-2">
{event.status === 'draft' && (
<span className="rounded-full bg-yellow-500/15 px-3 py-1 text-xs font-medium text-yellow-200 border border-yellow-400/20">
Draft
</span>
)}
{event.visibility === 'private' && (
<span className="rounded-full bg-purple-500/15 px-3 py-1 text-xs font-medium text-purple-200 border border-purple-400/20">
Private
</span>
)}
</div>

<div className="mb-5 space-y-2 text-sm text-gray-300">
{(event.date || event.time) && (
<div className="flex items-center gap-2">
Expand Down Expand Up @@ -139,6 +193,7 @@ export default function EventsPage() {
const [hasError, setHasError] = useState(false);
const isSCEventsEnabled = config.SCEvents?.ENABLED;
const canCreateEvent = user?.accessLevel >= membershipState.OFFICER;
const visibleEvents = events.filter((event) => canUserSeeEvent(event, user));

useEffect(() => {
if (!isSCEventsEnabled) {
Expand Down Expand Up @@ -210,15 +265,15 @@ export default function EventsPage() {
</div>
)}

{!isLoading && !hasError && events.length === 0 && (
{!isLoading && !hasError && visibleEvents.length === 0 && (
<div className="rounded-2xl border border-white/10 bg-white/5 p-10 text-center text-lg text-gray-300">
No events available right now.
</div>
)}

{!isLoading && !hasError && events.length > 0 && (
{!isLoading && !hasError && visibleEvents.length > 0 && (
<div className="grid grid-cols-1 gap-8 md:grid-cols-2 xl:grid-cols-3">
{events.map((event) => (
{visibleEvents.map((event) => (
<EventCard key={event.id} event={event} user={user} />
))}
</div>
Expand Down
Loading