Add events page

This commit is contained in:
MartinBraquet
2026-02-18 18:34:18 +01:00
parent cad1fd72e3
commit 46082c5f64
30 changed files with 1798 additions and 25 deletions

View File

@@ -80,6 +80,12 @@ import {hideProfile} from "api/hide-profile";
import {unhideProfile} from "api/unhide-profile";
import {getHiddenProfiles} from "api/get-hidden-profiles";
import {getUserDataExport} from "./get-user-data-export";
import {getEvents} from "./get-events";
import {createEvent} from "./create-event";
import {rsvpEvent} from "./rsvp-event";
import {cancelRsvp} from "./cancel-rsvp";
import {cancelEvent} from "./cancel-event";
import {updateEvent} from "./update-event";
// const corsOptions: CorsOptions = {
// origin: ['*'], // Only allow requests from this domain
@@ -376,6 +382,12 @@ const handlers: { [k in APIPath]: APIHandler<k> } = {
// 'user/:username': getUser,
// 'user/:username/lite': getDisplayUser,
// 'user/by-id/:id/lite': getDisplayUser,
'cancel-event': cancelEvent,
'cancel-rsvp': cancelRsvp,
'create-event': createEvent,
'get-events': getEvents,
'rsvp-event': rsvpEvent,
'update-event': updateEvent,
health: health,
me: getMe,
report: report,

View File

@@ -0,0 +1,46 @@
import {APIError, APIHandler} from 'api/helpers/endpoint'
import {createSupabaseDirectClient} from 'shared/supabase/init'
import {update} from 'shared/supabase/utils'
import {tryCatch} from 'common/util/try-catch'
export const cancelEvent: APIHandler<'cancel-event'> = async (body, auth) => {
const pg = createSupabaseDirectClient()
// Check if event exists and user is the creator
const event = await pg.oneOrNone<{
id: string
creator_id: string
status: string
}>(
`SELECT id, creator_id, status
FROM events
WHERE id = $1`,
[body.eventId]
)
if (!event) {
throw new APIError(404, 'Event not found')
}
if (event.creator_id !== auth.uid) {
throw new APIError(403, 'Only the event creator can cancel this event')
}
if (event.status === 'cancelled') {
throw new APIError(400, 'Event is already cancelled')
}
// Update event status to cancelled
const {error} = await tryCatch(
update(pg, 'events', 'id', {
status: 'cancelled',
id: body.eventId,
})
)
if (error) {
throw new APIError(500, 'Failed to cancel event: ' + error.message)
}
return {success: true}
}

View File

@@ -0,0 +1,38 @@
import {APIError, APIHandler} from 'api/helpers/endpoint'
import {createSupabaseDirectClient} from 'shared/supabase/init'
import {tryCatch} from 'common/util/try-catch'
export const cancelRsvp: APIHandler<'cancel-rsvp'> = async (body, auth) => {
const pg = createSupabaseDirectClient()
// Check if RSVP exists
const rsvp = await pg.oneOrNone<{
id: string
}>(
`SELECT id
FROM events_participants
WHERE event_id = $1
AND user_id = $2`,
[body.eventId, auth.uid]
)
if (!rsvp) {
throw new APIError(404, 'RSVP not found')
}
// Delete the RSVP
const {error} = await tryCatch(
pg.none(
`DELETE
FROM events_participants
WHERE id = $1`,
[rsvp.id]
)
)
if (error) {
throw new APIError(500, 'Failed to cancel RSVP: ' + error.message)
}
return {success: true}
}

View File

@@ -0,0 +1,49 @@
import {APIError, APIHandler} from 'api/helpers/endpoint'
import {createSupabaseDirectClient} from 'shared/supabase/init'
import {insert} from 'shared/supabase/utils'
import {tryCatch} from 'common/util/try-catch'
export const createEvent: APIHandler<'create-event'> = async (body, auth) => {
const pg = createSupabaseDirectClient()
// Validate location
if (body.locationType === 'in_person' && !body.locationAddress) {
throw new APIError(400, 'In-person events require a location address')
}
if (body.locationType === 'online' && !body.locationUrl) {
throw new APIError(400, 'Online events require a location URL')
}
// Validate dates
const startTime = new Date(body.eventStartTime)
if (startTime < new Date()) {
throw new APIError(400, 'Event start time must be in the future')
}
if (body.eventEndTime) {
const endTime = new Date(body.eventEndTime)
if (endTime <= startTime) {
throw new APIError(400, 'Event end time must be after start time')
}
}
const {data, error} = await tryCatch(
insert(pg, 'events', {
creator_id: auth.uid,
title: body.title,
description: body.description,
location_type: body.locationType,
location_address: body.locationAddress,
location_url: body.locationUrl,
event_start_time: body.eventStartTime,
event_end_time: body.eventEndTime,
max_participants: body.maxParticipants,
})
)
if (error) {
throw new APIError(500, 'Failed to create event: ' + error.message)
}
return {success: true, event: data}
}

View File

@@ -0,0 +1,87 @@
import {APIHandler} from 'api/helpers/endpoint'
import {createSupabaseDirectClient} from 'shared/supabase/init'
export const getEvents: APIHandler<'get-events'> = async () => {
const pg = createSupabaseDirectClient()
const events = await pg.manyOrNone<{
id: string
created_time: string
creator_id: string
title: string
description: string | null
location_type: 'in_person' | 'online'
location_address: string | null
location_url: string | null
event_start_time: object
event_end_time: object | null
is_public: boolean
max_participants: number | null
status: 'active' | 'cancelled' | 'completed'
}>(
`SELECT *
FROM events
WHERE is_public = true
AND status = 'active'
ORDER BY event_start_time`
)
// Get participants for each event
const eventIds = events.map(e => e.id)
const participants = eventIds.length > 0
? await pg.manyOrNone<{
event_id: string
user_id: string
status: 'going' | 'maybe' | 'not_going'
}>(
`SELECT event_id, user_id, status
FROM events_participants
WHERE event_id = ANY ($1)`,
[eventIds]
)
: []
// Get creator info for each event
const creatorIds = [...new Set(events.map(e => e.creator_id))]
const creators = creatorIds.length > 0
? await pg.manyOrNone<{
id: string
name: string
username: string
avatar_url: string | null
}>(
`SELECT id, name, username, data ->> 'avatarUrl' as avatar_url
FROM users
WHERE id = ANY ($1)`,
[creatorIds]
)
: []
const now = new Date()
const eventsWithDetails = events.map(event => ({
...event,
participants: participants
.filter(p => p.event_id === event.id && p.status === 'going')
.map(p => p.user_id),
maybe: participants
.filter(p => p.event_id === event.id && p.status === 'maybe')
.map(p => p.user_id),
creator: creators.find(c => c.id === event.creator_id),
}))
const upcoming: typeof eventsWithDetails = []
const past: typeof eventsWithDetails = []
for (const e of eventsWithDetails) {
if ((e.event_end_time ?? e.event_start_time) > now) {
upcoming.push(e)
} else {
past.push(e)
}
}
// console.debug({events, eventsWithDetails, upcoming, past, now})
return {upcoming, past}
}

View File

@@ -0,0 +1,83 @@
import {APIError, APIHandler} from 'api/helpers/endpoint'
import {createSupabaseDirectClient} from 'shared/supabase/init'
import {insert, update} from 'shared/supabase/utils'
import {tryCatch} from 'common/util/try-catch'
export const rsvpEvent: APIHandler<'rsvp-event'> = async (body, auth) => {
const pg = createSupabaseDirectClient()
// Check if event exists and is active
const event = await pg.oneOrNone<{
id: string
status: string
max_participants: number | null
}>(
`SELECT id, status, max_participants
FROM events
WHERE id = $1`,
[body.eventId]
)
if (!event) {
throw new APIError(404, 'Event not found')
}
if (event.status !== 'active') {
throw new APIError(400, 'Cannot RSVP to a cancelled or completed event')
}
// Check if already RSVPed
const existingRsvp = await pg.oneOrNone<{
id: string
}>(
`SELECT id
FROM events_participants
WHERE event_id = $1
AND user_id = $2`,
[body.eventId, auth.uid]
)
if (existingRsvp) {
// Update existing RSVP
const {error} = await tryCatch(
update(pg, 'events_participants', 'id', {
status: body.status,
id: existingRsvp.id,
})
)
if (error) {
throw new APIError(500, 'Failed to update RSVP: ' + error.message)
}
} else {
// Check max participants limit
if (event.max_participants && body.status === 'going') {
const count = await pg.one<{ count: number }>(
`SELECT COUNT(*)
FROM events_participants
WHERE event_id = $1
AND status = 'going'`,
[body.eventId]
)
if (Number(count.count) >= event.max_participants) {
throw new APIError(400, 'Event is at maximum capacity')
}
}
// Create new RSVP
const {error} = await tryCatch(
insert(pg, 'events_participants', {
event_id: body.eventId,
user_id: auth.uid,
status: body.status,
})
)
if (error) {
throw new APIError(500, 'Failed to RSVP: ' + error.message)
}
}
return {success: true}
}

View File

@@ -0,0 +1,53 @@
import {APIError, APIHandler} from 'api/helpers/endpoint'
import {createSupabaseDirectClient} from 'shared/supabase/init'
import {update} from 'shared/supabase/utils'
import {tryCatch} from 'common/util/try-catch'
export const updateEvent: APIHandler<'update-event'> = async (body, auth) => {
const pg = createSupabaseDirectClient()
// Check if event exists and user is the creator
const event = await pg.oneOrNone<{
id: string
creator_id: string
status: string
}>(
`SELECT id, creator_id, status
FROM events
WHERE id = $1`,
[body.eventId]
)
if (!event) {
throw new APIError(404, 'Event not found')
}
if (event.creator_id !== auth.uid) {
throw new APIError(403, 'Only the event creator can edit this event')
}
if (event.status !== 'active') {
throw new APIError(400, 'Cannot edit a cancelled or completed event')
}
// Update event
const {error} = await tryCatch(
update(pg, 'events', 'id', {
title: body.title,
description: body.description,
location_type: body.locationType,
location_address: body.locationAddress,
location_url: body.locationUrl,
event_start_time: body.eventStartTime,
event_end_time: body.eventEndTime,
max_participants: body.maxParticipants,
id: body.eventId,
})
)
if (error) {
throw new APIError(500, 'Failed to update event: ' + error.message)
}
return {success: true}
}