Skip to content

Commit 9b1817d

Browse files
committed
temp
1 parent f5f85fe commit 9b1817d

File tree

10 files changed

+2684
-5
lines changed

10 files changed

+2684
-5
lines changed

eventstack/apps/backend/src/routes/bookmarks.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,39 @@
1+
/**
2+
* Bookmark routes module
3+
* Handles all HTTP endpoints related to user bookmarks, including:
4+
* - Retrieving a user's bookmarked events
5+
* - Adding events to user's bookmarks
6+
* - Removing events from user's bookmarks
7+
* All endpoints require user authentication
8+
*/
19
import { FastifyInstance } from "fastify";
210
import { z } from "zod";
311
import { query } from "../db.js";
412
import { cuid } from "../utils.js";
513
import { requireUser } from "../auth.js";
614

15+
/**
16+
* Registers all bookmark-related routes with the Fastify app instance
17+
* @param app - Fastify application instance
18+
*/
719
export default async function bookmarkRoutes(app: FastifyInstance) {
20+
/**
21+
* Schema for validating bookmark creation requests
22+
* Requires an eventId field to identify which event to bookmark
23+
*/
824
const addSchema = z.object({ eventId: z.string() });
925

26+
/**
27+
* GET /me/bookmarks
28+
* Retrieves all bookmarked events for the authenticated user
29+
* Returns events in reverse chronological order (most recently bookmarked first)
30+
* Includes event details and the first cover image URL
31+
* @returns Array of event objects with id, title, summary, dates, coverUrl, and venue
32+
*/
1033
app.get("/me/bookmarks", { preHandler: requireUser }, async (req) => {
1134
const user = (req as any).user as { id: string };
35+
// Query joins bookmarks with events to get full event details
36+
// Gets the first cover image ordered by display order
1237
const { rows } = await query(
1338
`SELECT b.event_id as id, e.title, e.summary, e.starts_at, e.ends_at,
1439
(SELECT url FROM event_images i WHERE i.event_id=e.id ORDER BY ord ASC LIMIT 1) as cover_url,
@@ -19,6 +44,7 @@ export default async function bookmarkRoutes(app: FastifyInstance) {
1944
ORDER BY b.created_at DESC`,
2045
[user.id]
2146
);
47+
// Transform database rows to API response format with ISO date strings
2248
return rows.map((r: any) => ({
2349
id: r.id,
2450
title: r.title,
@@ -30,22 +56,38 @@ export default async function bookmarkRoutes(app: FastifyInstance) {
3056
}));
3157
});
3258

59+
/**
60+
* POST /me/bookmarks
61+
* Adds an event to the authenticated user's bookmarks
62+
* If the event is already bookmarked, the request silently succeeds (idempotent)
63+
* @param body.eventId - The ID of the event to bookmark
64+
* @returns 201 status with success confirmation
65+
*/
3366
app.post("/me/bookmarks", { preHandler: requireUser }, async (req, reply) => {
3467
const body = addSchema.parse(req.body);
3568
const user = (req as any).user as { id: string };
3669
try {
70+
// Create a new bookmark with a generated CUID
3771
await query(
3872
`INSERT INTO bookmarks (id, user_id, event_id) VALUES ($1,$2,$3)`,
3973
[cuid(), user.id, body.eventId]
4074
);
4175
} catch (e: any) {
42-
// likely duplicate
76+
// If bookmark already exists (duplicate key), silently ignore the error
77+
// This makes the endpoint idempotent - calling it multiple times is safe
4378
}
4479
reply.code(201).send({ ok: true });
4580
});
4681

82+
/**
83+
* DELETE /me/bookmarks/:eventId
84+
* Removes an event from the authenticated user's bookmarks
85+
* @param eventId - The ID of the event to remove from bookmarks (URL parameter)
86+
* @returns Success confirmation object
87+
*/
4788
app.delete<{ Params: { eventId: string } }>("/me/bookmarks/:eventId", { preHandler: requireUser }, async (req) => {
4889
const user = (req as any).user as { id: string };
90+
// Delete the bookmark matching both user and event IDs
4991
await query(`DELETE FROM bookmarks WHERE user_id=$1 AND event_id=$2`, [user.id, req.params.eventId]);
5092
return { ok: true };
5193
});

eventstack/apps/backend/src/routes/events.ts

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,26 @@
1+
/**
2+
* Event routes module
3+
* Handles all HTTP endpoints related to events, including:
4+
* - Retrieving event lists with filtering capabilities
5+
* - Getting individual event details with images
6+
* - Creating, updating, and deleting events (admin/organizer only)
7+
* - Managing event images (admin/organizer only)
8+
*/
19
import { FastifyInstance } from "fastify";
210
import { z } from "zod";
311
import { query } from "../db.js";
412
import { cuid, iso } from "../utils.js";
513
import { requireOrganizerOrAdmin } from "../auth.js";
614

15+
/**
16+
* Registers all event-related routes with the Fastify app instance
17+
* @param app - Fastify application instance
18+
*/
719
export default async function eventRoutes(app: FastifyInstance) {
20+
/**
21+
* Base schema for event creation and updates
22+
* Validates common fields required for event operations
23+
*/
824
const baseSchema = z.object({
925
title: z.string().min(1),
1026
summary: z.string().optional().nullable(),
@@ -14,10 +30,22 @@ export default async function eventRoutes(app: FastifyInstance) {
1430
categoriesCsv: z.string().optional().default("")
1531
});
1632

33+
/**
34+
* GET /events
35+
* Retrieves a list of events with optional filtering
36+
* Query parameters:
37+
* - from: Filter events starting from this date (ISO string)
38+
* - to: Filter events ending before this date (ISO string)
39+
* - category: Filter events by category name
40+
* @returns Array of event objects with cover image URL
41+
*/
1742
app.get("/events", async (req) => {
43+
// Extract and parse query parameters for filtering
1844
const q = (req.query || {}) as Record<string, string | undefined>;
1945
const where: string[] = [];
2046
const params: any[] = [];
47+
48+
// Build WHERE clause dynamically based on query parameters
2149
if (q.from) {
2250
params.push(q.from);
2351
where.push(`starts_at >= $${params.length}`);
@@ -28,9 +56,12 @@ export default async function eventRoutes(app: FastifyInstance) {
2856
}
2957
if (q.category) {
3058
params.push(q.category);
59+
// Use PostgreSQL ANY() operator to check if category exists in categories array
3160
where.push(`$${params.length} = ANY(categories)`);
3261
}
3362
const whereSql = where.length ? `WHERE ${where.join(" AND ")}` : "";
63+
64+
// Fetch events with their cover image (first image ordered by ord)
3465
const { rows } = await query(
3566
`SELECT e.*,
3667
(SELECT url FROM event_images i WHERE i.event_id=e.id ORDER BY ord ASC LIMIT 1) as cover_url
@@ -39,20 +70,31 @@ export default async function eventRoutes(app: FastifyInstance) {
3970
ORDER BY starts_at ASC`,
4071
params
4172
);
73+
74+
// Transform database rows to API response format
4275
return rows.map((r: any) => ({
4376
id: r.id,
4477
title: r.title,
4578
summary: r.summary,
46-
startsAt: iso(r.starts_at),
79+
startsAt: iso(r.starts_at), // Convert PostgreSQL timestamp to ISO string
4780
endsAt: iso(r.ends_at),
4881
categories: r.categories || [],
4982
coverUrl: r.cover_url || undefined,
5083
venue: { name: r.venue_name }
5184
}));
5285
});
5386

87+
/**
88+
* GET /events/:id
89+
* Retrieves detailed information about a specific event
90+
* Includes all event images ordered by their display order
91+
* @param id - Event ID from URL parameter
92+
* @returns Event object with all images, or 404 if not found
93+
*/
5494
app.get<{ Params: { id: string } }>("/events/:id", async (req, reply) => {
5595
const { id } = req.params;
96+
97+
// Fetch event with cover image (first image)
5698
const { rows } = await query(
5799
`SELECT e.* , e.cover_url FROM (
58100
SELECT e.*, (SELECT url FROM event_images i WHERE i.event_id=e.id ORDER BY ord ASC LIMIT 1) as cover_url FROM events e
@@ -64,7 +106,10 @@ export default async function eventRoutes(app: FastifyInstance) {
64106
reply.code(404).send({ error: "Not found" });
65107
return;
66108
}
109+
110+
// Fetch all images for this event, ordered by display order
67111
const { rows: img } = await query(`SELECT * FROM event_images WHERE event_id=$1 ORDER BY ord ASC`, [id]);
112+
68113
return {
69114
id: r.id,
70115
title: r.title,
@@ -77,35 +122,61 @@ export default async function eventRoutes(app: FastifyInstance) {
77122
};
78123
});
79124

125+
/**
126+
* POST /admin/events
127+
* Creates a new event (admin/organizer only)
128+
* Requires authentication with organizer or admin role
129+
* @param body - Event data validated against baseSchema
130+
* @returns Object with the created event ID
131+
*/
80132
app.post("/admin/events", { preHandler: requireOrganizerOrAdmin }, async (req) => {
81133
const body = baseSchema.parse(req.body);
82-
const id = cuid();
134+
const id = cuid(); // Generate unique ID for the event
135+
136+
// Parse comma-separated categories into array, trimming whitespace
83137
const categories = body.categoriesCsv ? body.categoriesCsv.split(",").map((s) => s.trim()).filter(Boolean) : [];
84138
const venueName = body.venueName || '';
139+
140+
// Create searchable text by combining all searchable fields (for full-text search)
85141
const searchable = [body.title, body.summary ?? "", venueName, categories.join(" ")].join(" ").toLowerCase();
142+
86143
await query(
87144
`INSERT INTO events (id,title,summary,starts_at,ends_at,venue_name,categories,searchable) VALUES ($1,$2,$3,$4,$5,$6,$7,$8)`,
88145
[id, body.title, body.summary ?? null, body.startsAt, body.endsAt, venueName, categories, searchable]
89146
);
90147
return { id };
91148
});
92149

150+
/**
151+
* POST /admin/events/:id/images
152+
* Adds an image to an event (admin/organizer only)
153+
* Images are automatically ordered based on existing images
154+
* @param id - Event ID from URL parameter
155+
* @param body - Image data (url, optional width/height)
156+
* @returns Object with image ID, URL, and display order
157+
*/
93158
app.post<{ Params: { id: string } }>("/admin/events/:id/images", { preHandler: requireOrganizerOrAdmin }, async (req, reply) => {
94159
const { id: eventId } = req.params;
160+
161+
// Schema for image validation
95162
const imageSchema = z.object({
96163
url: z.string().min(1),
97164
width: z.number().int().positive().optional(),
98165
height: z.number().int().positive().optional()
99166
});
100167
const body = imageSchema.parse(req.body);
101168

169+
// Verify that the event exists
102170
const { rows: existing } = await query("SELECT id FROM events WHERE id=$1", [eventId]);
103171
if (!existing.length) {
104172
reply.code(404).send({ error: "Event not found" });
105173
return;
106174
}
107175

108176
const imageId = cuid();
177+
178+
// Get the maximum order value for existing images, defaulting to -1 if none exist
179+
// This ensures the new image is appended to the end
109180
const { rows: ordRows } = await query<{ max: number }>("SELECT COALESCE(MAX(ord), -1) as max FROM event_images WHERE event_id=$1", [eventId]);
110181
const nextOrd = (ordRows[0]?.max ?? -1) + 1;
111182

@@ -117,19 +188,39 @@ export default async function eventRoutes(app: FastifyInstance) {
117188
return { id: imageId, url: body.url, ord: nextOrd };
118189
});
119190

191+
/**
192+
* PUT /admin/events/:id
193+
* Updates an existing event (admin/organizer only)
194+
* Updates the updated_at timestamp automatically
195+
* @param id - Event ID from URL parameter
196+
* @param body - Event data validated against baseSchema
197+
* @returns Object with the updated event ID
198+
*/
120199
app.put<{ Params: { id: string } }>("/admin/events/:id", { preHandler: requireOrganizerOrAdmin }, async (req) => {
121200
const body = baseSchema.parse(req.body);
122201
const { id } = req.params;
202+
203+
// Parse comma-separated categories into array, trimming whitespace
123204
const categories = body.categoriesCsv ? body.categoriesCsv.split(",").map((s) => s.trim()).filter(Boolean) : [];
124205
const venueName = body.venueName || '';
206+
207+
// Update searchable text by combining all searchable fields
125208
const searchable = [body.title, body.summary ?? "", venueName, categories.join(" ")].join(" ").toLowerCase();
209+
126210
await query(
127211
`UPDATE events SET title=$2,summary=$3,starts_at=$4,ends_at=$5,venue_name=$6,categories=$7,searchable=$8,updated_at=now() WHERE id=$1`,
128212
[id, body.title, body.summary ?? null, body.startsAt, body.endsAt, venueName, categories, searchable]
129213
);
130214
return { id };
131215
});
132216

217+
/**
218+
* DELETE /admin/events/:id
219+
* Deletes an event (admin/organizer only)
220+
* Cascades to delete associated images and other related data
221+
* @param id - Event ID from URL parameter
222+
* @returns Object with the deleted event ID
223+
*/
133224
app.delete<{ Params: { id: string } }>("/admin/events/:id", { preHandler: requireOrganizerOrAdmin }, async (req) => {
134225
const { id } = req.params;
135226
await query(`DELETE FROM events WHERE id=$1`, [id]);

eventstack/apps/frontend/src/lib/api/events.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
1+
/**
2+
* Events API client module
3+
* Provides functions to interact with the events API endpoints
4+
*/
15
import { http } from "../http"
26

7+
/**
8+
* Retrieves a list of events with optional filtering
9+
* @param params - Optional filter parameters
10+
* @param params.from - Filter events starting from this date (ISO string)
11+
* @param params.to - Filter events ending before this date (ISO string)
12+
* @param params.category - Filter events by category name
13+
* @returns Promise resolving to an array of event objects
14+
*/
315
export async function listEvents(params?: { from?: string; to?: string; category?: string }) {
416
const qs = new URLSearchParams()
517
if (params?.from) qs.set('from', params.from)
@@ -9,22 +21,63 @@ export async function listEvents(params?: { from?: string; to?: string; category
921
return http<any[]>(`/events${q ? `?${q}` : ''}`)
1022
}
1123

24+
/**
25+
* Retrieves detailed information about a specific event
26+
* @param id - Event ID
27+
* @returns Promise resolving to an event object with all images
28+
*/
1229
export async function getEvent(id: string) { return http<any>(`/events/${id}`) }
1330

31+
/**
32+
* Creates a new event (admin/organizer only)
33+
* Requires authentication with organizer or admin role
34+
* @param e - Event data object
35+
* @param e.title - Event title (required)
36+
* @param e.summary - Event summary (optional)
37+
* @param e.startsAt - Event start date/time (ISO string)
38+
* @param e.endsAt - Event end date/time (ISO string)
39+
* @param e.venueName - Venue name (optional)
40+
* @param e.categoriesCsv - Comma-separated list of categories (optional)
41+
* @returns Promise resolving to an object with the created event ID
42+
*/
1443
export async function createEvent(e: { title: string; summary?: string; startsAt: string; endsAt: string; venueName?: string; categoriesCsv?: string }) {
1544
return http<{ id: string }>(
1645
'/admin/events', { method: 'POST', body: JSON.stringify(e) }
1746
)
1847
}
1948

49+
/**
50+
* Updates an existing event (admin/organizer only)
51+
* Requires authentication with organizer or admin role
52+
* @param id - Event ID to update
53+
* @param e - Event data object with fields to update
54+
* @returns Promise resolving to an object with the updated event ID
55+
*/
2056
export async function updateEvent(id: string, e: { title: string; summary?: string; startsAt: string; endsAt: string; venueName?: string; categoriesCsv?: string }) {
2157
return http<{ id: string }>(
2258
`/admin/events/${id}`, { method: 'PUT', body: JSON.stringify(e) }
2359
)
2460
}
2561

62+
/**
63+
* Deletes an event (admin/organizer only)
64+
* Requires authentication with organizer or admin role
65+
* @param id - Event ID to delete
66+
* @returns Promise resolving to an object with the deleted event ID
67+
*/
2668
export async function deleteEvent(id: string) { return http<{ id: string }>(`/admin/events/${id}`, { method: 'DELETE' }) }
2769

70+
/**
71+
* Adds an image to an event (admin/organizer only)
72+
* Requires authentication with organizer or admin role
73+
* Images are automatically ordered based on existing images
74+
* @param id - Event ID
75+
* @param image - Image data object
76+
* @param image.url - Image URL (required)
77+
* @param image.width - Image width in pixels (optional)
78+
* @param image.height - Image height in pixels (optional)
79+
* @returns Promise resolving to an object with image ID, URL, and display order
80+
*/
2881
export async function addEventImage(id: string, image: { url: string; width?: number; height?: number }) {
2982
return http<{ id: string; url: string; ord: number }>(
3083
`/admin/events/${id}/images`,

eventstack/apps/frontend/src/routes/EventDetail.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,6 @@ export default function EventDetail() {
109109
}
110110
}
111111

112-
// Directions removed per create-flow simplification request
113112

114113
// Handle sharing the event
115114
const handleShare = async () => {
@@ -220,7 +219,7 @@ export default function EventDetail() {
220219
<button type="button" className="btn btn-primary" onClick={handleBookmark}>
221220
{bookmarked ? 'Remove bookmark' : 'Bookmark'}
222221
</button>
223-
{/* Directions removed */}
222+
224223
<button type="button" className="btn btn-outline" onClick={handleShare}>
225224
Share
226225
</button>

0 commit comments

Comments
 (0)