mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-05-14 01:54:40 -04:00
Add events page
This commit is contained in:
@@ -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,
|
||||
|
||||
46
backend/api/src/cancel-event.ts
Normal file
46
backend/api/src/cancel-event.ts
Normal 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}
|
||||
}
|
||||
38
backend/api/src/cancel-rsvp.ts
Normal file
38
backend/api/src/cancel-rsvp.ts
Normal 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}
|
||||
}
|
||||
49
backend/api/src/create-event.ts
Normal file
49
backend/api/src/create-event.ts
Normal 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}
|
||||
}
|
||||
87
backend/api/src/get-events.ts
Normal file
87
backend/api/src/get-events.ts
Normal 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}
|
||||
}
|
||||
83
backend/api/src/rsvp-event.ts
Normal file
83
backend/api/src/rsvp-event.ts
Normal 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}
|
||||
}
|
||||
53
backend/api/src/update-event.ts
Normal file
53
backend/api/src/update-event.ts
Normal 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}
|
||||
}
|
||||
Reference in New Issue
Block a user