@commonpub/layer 0.11.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/EventCard.vue +121 -0
- package/components/PollDisplay.vue +108 -0
- package/components/PostVoteButtons.vue +108 -0
- package/components/contest/ContestJudgeManager.vue +110 -0
- package/components/nav/MobileNavRenderer.vue +94 -0
- package/components/nav/NavDropdown.vue +101 -0
- package/components/nav/NavLink.vue +40 -0
- package/components/nav/NavRenderer.vue +51 -0
- package/composables/useFeatures.ts +2 -0
- package/layouts/admin.vue +1 -0
- package/layouts/default.vue +22 -86
- package/middleware/feature-gate.global.ts +1 -0
- package/package.json +7 -7
- package/pages/admin/navigation.vue +350 -0
- package/pages/events/[slug]/edit.vue +182 -0
- package/pages/events/[slug]/index.vue +249 -0
- package/pages/events/create.vue +140 -0
- package/pages/events/index.vue +47 -0
- package/server/api/admin/navigation/items.get.ts +11 -0
- package/server/api/admin/navigation/items.put.ts +51 -0
- package/server/api/contests/[slug]/entries/[entryId]/vote.delete.ts +20 -0
- package/server/api/contests/[slug]/entries/[entryId]/vote.post.ts +20 -0
- package/server/api/contests/[slug]/judge.post.ts +5 -7
- package/server/api/contests/[slug]/judges/[userId].delete.ts +26 -0
- package/server/api/contests/[slug]/judges/accept.post.ts +21 -0
- package/server/api/contests/[slug]/judges/index.get.ts +17 -0
- package/server/api/contests/[slug]/judges/index.post.ts +36 -0
- package/server/api/events/[slug]/attendees.get.ts +23 -0
- package/server/api/events/[slug]/rsvp.delete.ts +23 -0
- package/server/api/events/[slug]/rsvp.post.ts +23 -0
- package/server/api/events/[slug].delete.ts +22 -0
- package/server/api/events/[slug].get.ts +17 -0
- package/server/api/events/[slug].put.ts +38 -0
- package/server/api/events/index.get.ts +21 -0
- package/server/api/events/index.post.ts +40 -0
- package/server/api/hubs/[slug]/posts/[postId]/poll-options.get.ts +18 -0
- package/server/api/hubs/[slug]/posts/[postId]/poll-vote.post.ts +27 -0
- package/server/api/hubs/[slug]/posts/[postId]/vote.post.ts +21 -0
- package/server/api/navigation/items.get.ts +10 -0
- package/server/middleware/features.ts +1 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { NavItem } from '@commonpub/server';
|
|
2
|
+
import { setNavItems } from '@commonpub/server';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
const navItemSchema: z.ZodType<NavItem> = z.lazy(() =>
|
|
6
|
+
z.object({
|
|
7
|
+
id: z.string().min(1).max(64),
|
|
8
|
+
type: z.enum(['link', 'dropdown', 'external']),
|
|
9
|
+
label: z.string().min(1).max(128),
|
|
10
|
+
icon: z.string().max(128).optional(),
|
|
11
|
+
route: z.string().max(255).optional(),
|
|
12
|
+
href: z.string().url().max(1024).optional(),
|
|
13
|
+
featureGate: z.string().max(64).optional(),
|
|
14
|
+
children: z.array(navItemSchema).max(20).optional(),
|
|
15
|
+
visibleTo: z.enum(['all', 'authenticated', 'admin']).optional(),
|
|
16
|
+
disabled: z.boolean().optional(),
|
|
17
|
+
}),
|
|
18
|
+
) as z.ZodType<NavItem>;
|
|
19
|
+
|
|
20
|
+
const updateNavSchema = z.object({
|
|
21
|
+
items: z.array(navItemSchema).min(1).max(30),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* PUT /api/admin/navigation/items
|
|
26
|
+
* Save navigation item configuration.
|
|
27
|
+
*/
|
|
28
|
+
export default defineEventHandler(async (event) => {
|
|
29
|
+
const user = requireAdmin(event);
|
|
30
|
+
const db = useDB();
|
|
31
|
+
const body = await parseBody(event, updateNavSchema);
|
|
32
|
+
|
|
33
|
+
// Validate unique IDs (flatten to check children too)
|
|
34
|
+
const ids = new Set<string>();
|
|
35
|
+
function collectIds(items: NavItem[]): void {
|
|
36
|
+
for (const item of items) {
|
|
37
|
+
if (ids.has(item.id)) {
|
|
38
|
+
throw createError({ statusCode: 400, statusMessage: `Duplicate nav item ID: ${item.id}` });
|
|
39
|
+
}
|
|
40
|
+
ids.add(item.id);
|
|
41
|
+
if (item.children) {
|
|
42
|
+
collectIds(item.children);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
collectIds(body.items);
|
|
47
|
+
|
|
48
|
+
await setNavItems(db, body.items, user.id, getRequestIP(event) ?? undefined);
|
|
49
|
+
|
|
50
|
+
return { items: body.items, message: 'Navigation updated' };
|
|
51
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { removeContestEntryVote } from '@commonpub/server';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* DELETE /api/contests/:slug/entries/:entryId/vote
|
|
5
|
+
* Remove community vote from a contest entry.
|
|
6
|
+
*/
|
|
7
|
+
export default defineEventHandler(async (event) => {
|
|
8
|
+
requireFeature('contests');
|
|
9
|
+
const user = requireAuth(event);
|
|
10
|
+
const db = useDB();
|
|
11
|
+
const entryId = getRouterParam(event, 'entryId');
|
|
12
|
+
if (!entryId) throw createError({ statusCode: 400, statusMessage: 'Missing entryId' });
|
|
13
|
+
|
|
14
|
+
const removed = await removeContestEntryVote(db, entryId, user.id);
|
|
15
|
+
if (!removed) {
|
|
16
|
+
throw createError({ statusCode: 400, statusMessage: 'No vote found' });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return { removed: true };
|
|
20
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { voteOnContestEntry } from '@commonpub/server';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* POST /api/contests/:slug/entries/:entryId/vote
|
|
5
|
+
* Vote on a contest entry (community voting).
|
|
6
|
+
*/
|
|
7
|
+
export default defineEventHandler(async (event) => {
|
|
8
|
+
requireFeature('contests');
|
|
9
|
+
const user = requireAuth(event);
|
|
10
|
+
const db = useDB();
|
|
11
|
+
const entryId = getRouterParam(event, 'entryId');
|
|
12
|
+
if (!entryId) throw createError({ statusCode: 400, statusMessage: 'Missing entryId' });
|
|
13
|
+
|
|
14
|
+
const result = await voteOnContestEntry(db, entryId, user.id);
|
|
15
|
+
if (!result.voted) {
|
|
16
|
+
throw createError({ statusCode: 400, statusMessage: result.error ?? 'Vote failed' });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return { voted: true };
|
|
20
|
+
});
|
|
@@ -1,18 +1,16 @@
|
|
|
1
|
-
import { judgeContestEntry
|
|
1
|
+
import { judgeContestEntry } from '@commonpub/server';
|
|
2
2
|
import { judgeEntrySchema } from '@commonpub/schema';
|
|
3
3
|
|
|
4
4
|
export default defineEventHandler(async (event): Promise<{ success: boolean }> => {
|
|
5
5
|
requireFeature('contests');
|
|
6
6
|
const user = requireAuth(event);
|
|
7
7
|
const db = useDB();
|
|
8
|
-
const { slug } = parseParams(event, { slug: 'string' });
|
|
9
8
|
const input = await parseBody(event, judgeEntrySchema);
|
|
10
9
|
|
|
11
|
-
const
|
|
12
|
-
if (!
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
const result = await judgeContestEntry(db, input.entryId, input.score, user.id, input.feedback);
|
|
11
|
+
if (!result.judged) {
|
|
12
|
+
throw createError({ statusCode: 403, message: result.error ?? 'Judging failed' });
|
|
13
|
+
}
|
|
15
14
|
|
|
16
|
-
await judgeContestEntry(db, input.entryId, input.score, user.id, input.feedback);
|
|
17
15
|
return { success: true };
|
|
18
16
|
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { getContestBySlug, removeContestJudge } from '@commonpub/server';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* DELETE /api/contests/:slug/judges/:userId
|
|
5
|
+
* Remove a judge from a contest (contest owner or admin only).
|
|
6
|
+
*/
|
|
7
|
+
export default defineEventHandler(async (event) => {
|
|
8
|
+
requireFeature('contests');
|
|
9
|
+
const user = requireAuth(event);
|
|
10
|
+
const db = useDB();
|
|
11
|
+
const slug = getRouterParam(event, 'slug');
|
|
12
|
+
const targetUserId = getRouterParam(event, 'userId');
|
|
13
|
+
if (!slug || !targetUserId) throw createError({ statusCode: 400, statusMessage: 'Missing parameters' });
|
|
14
|
+
|
|
15
|
+
const contest = await getContestBySlug(db, slug);
|
|
16
|
+
if (!contest) throw createError({ statusCode: 404, statusMessage: 'Contest not found' });
|
|
17
|
+
|
|
18
|
+
if (contest.createdById !== user.id && user.role !== 'admin') {
|
|
19
|
+
throw createError({ statusCode: 403, statusMessage: 'Only contest owner or admin can manage judges' });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const removed = await removeContestJudge(db, contest.id, targetUserId);
|
|
23
|
+
if (!removed) throw createError({ statusCode: 404, statusMessage: 'Judge not found' });
|
|
24
|
+
|
|
25
|
+
return { removed: true };
|
|
26
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { getContestBySlug, acceptJudgeInvite } from '@commonpub/server';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* POST /api/contests/:slug/judges/accept
|
|
5
|
+
* Accept a judge invitation (authenticated user).
|
|
6
|
+
*/
|
|
7
|
+
export default defineEventHandler(async (event) => {
|
|
8
|
+
requireFeature('contests');
|
|
9
|
+
const user = requireAuth(event);
|
|
10
|
+
const db = useDB();
|
|
11
|
+
const slug = getRouterParam(event, 'slug');
|
|
12
|
+
if (!slug) throw createError({ statusCode: 400, statusMessage: 'Missing slug' });
|
|
13
|
+
|
|
14
|
+
const contest = await getContestBySlug(db, slug);
|
|
15
|
+
if (!contest) throw createError({ statusCode: 404, statusMessage: 'Contest not found' });
|
|
16
|
+
|
|
17
|
+
const accepted = await acceptJudgeInvite(db, contest.id, user.id);
|
|
18
|
+
if (!accepted) throw createError({ statusCode: 400, statusMessage: 'No pending invitation found' });
|
|
19
|
+
|
|
20
|
+
return { accepted: true };
|
|
21
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { getContestBySlug, listContestJudges } from '@commonpub/server';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* GET /api/contests/:slug/judges
|
|
5
|
+
* List judges for a contest.
|
|
6
|
+
*/
|
|
7
|
+
export default defineEventHandler(async (event) => {
|
|
8
|
+
requireFeature('contests');
|
|
9
|
+
const db = useDB();
|
|
10
|
+
const slug = getRouterParam(event, 'slug');
|
|
11
|
+
if (!slug) throw createError({ statusCode: 400, statusMessage: 'Missing slug' });
|
|
12
|
+
|
|
13
|
+
const contest = await getContestBySlug(db, slug);
|
|
14
|
+
if (!contest) throw createError({ statusCode: 404, statusMessage: 'Contest not found' });
|
|
15
|
+
|
|
16
|
+
return listContestJudges(db, contest.id);
|
|
17
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { getContestBySlug, addContestJudge } from '@commonpub/server';
|
|
2
|
+
import type { JudgeRole } from '@commonpub/server';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
const addJudgeSchema = z.object({
|
|
6
|
+
userId: z.string().uuid(),
|
|
7
|
+
role: z.enum(['lead', 'judge', 'guest']).optional(),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* POST /api/contests/:slug/judges
|
|
12
|
+
* Add a judge to a contest (contest owner or admin only).
|
|
13
|
+
*/
|
|
14
|
+
export default defineEventHandler(async (event) => {
|
|
15
|
+
requireFeature('contests');
|
|
16
|
+
const user = requireAuth(event);
|
|
17
|
+
const db = useDB();
|
|
18
|
+
const slug = getRouterParam(event, 'slug');
|
|
19
|
+
if (!slug) throw createError({ statusCode: 400, statusMessage: 'Missing slug' });
|
|
20
|
+
|
|
21
|
+
const contest = await getContestBySlug(db, slug);
|
|
22
|
+
if (!contest) throw createError({ statusCode: 404, statusMessage: 'Contest not found' });
|
|
23
|
+
|
|
24
|
+
if (contest.createdById !== user.id && user.role !== 'admin') {
|
|
25
|
+
throw createError({ statusCode: 403, statusMessage: 'Only contest owner or admin can manage judges' });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const body = await parseBody(event, addJudgeSchema);
|
|
29
|
+
const result = await addContestJudge(db, contest.id, body.userId, (body.role ?? 'judge') as JudgeRole);
|
|
30
|
+
|
|
31
|
+
if (!result.added) {
|
|
32
|
+
throw createError({ statusCode: 400, statusMessage: result.error ?? 'Failed to add judge' });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return { added: true };
|
|
36
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { getEventBySlug, listEventAttendees } from '@commonpub/server';
|
|
2
|
+
import type { AttendeeStatus } from '@commonpub/server';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* GET /api/events/:slug/attendees
|
|
6
|
+
* List attendees for an event (public).
|
|
7
|
+
*/
|
|
8
|
+
export default defineEventHandler(async (event) => {
|
|
9
|
+
requireFeature('events');
|
|
10
|
+
const db = useDB();
|
|
11
|
+
const slug = getRouterParam(event, 'slug');
|
|
12
|
+
if (!slug) throw createError({ statusCode: 400, statusMessage: 'Missing slug' });
|
|
13
|
+
|
|
14
|
+
const existing = await getEventBySlug(db, slug);
|
|
15
|
+
if (!existing) throw createError({ statusCode: 404, statusMessage: 'Event not found' });
|
|
16
|
+
|
|
17
|
+
const query = getQuery(event);
|
|
18
|
+
return listEventAttendees(db, existing.id, {
|
|
19
|
+
status: (query.status as AttendeeStatus) || undefined,
|
|
20
|
+
limit: query.limit ? Number(query.limit) : undefined,
|
|
21
|
+
offset: query.offset ? Number(query.offset) : undefined,
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { getEventBySlug, cancelRsvp } from '@commonpub/server';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* DELETE /api/events/:slug/rsvp
|
|
5
|
+
* Cancel RSVP for an event (authenticated).
|
|
6
|
+
*/
|
|
7
|
+
export default defineEventHandler(async (event) => {
|
|
8
|
+
requireFeature('events');
|
|
9
|
+
const user = requireAuth(event);
|
|
10
|
+
const db = useDB();
|
|
11
|
+
const slug = getRouterParam(event, 'slug');
|
|
12
|
+
if (!slug) throw createError({ statusCode: 400, statusMessage: 'Missing slug' });
|
|
13
|
+
|
|
14
|
+
const existing = await getEventBySlug(db, slug);
|
|
15
|
+
if (!existing) throw createError({ statusCode: 404, statusMessage: 'Event not found' });
|
|
16
|
+
|
|
17
|
+
const cancelled = await cancelRsvp(db, existing.id, user.id);
|
|
18
|
+
if (!cancelled) {
|
|
19
|
+
throw createError({ statusCode: 400, statusMessage: 'No RSVP found' });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return { cancelled: true };
|
|
23
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { getEventBySlug, rsvpEvent } from '@commonpub/server';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* POST /api/events/:slug/rsvp
|
|
5
|
+
* RSVP to an event (authenticated).
|
|
6
|
+
*/
|
|
7
|
+
export default defineEventHandler(async (event) => {
|
|
8
|
+
requireFeature('events');
|
|
9
|
+
const user = requireAuth(event);
|
|
10
|
+
const db = useDB();
|
|
11
|
+
const slug = getRouterParam(event, 'slug');
|
|
12
|
+
if (!slug) throw createError({ statusCode: 400, statusMessage: 'Missing slug' });
|
|
13
|
+
|
|
14
|
+
const existing = await getEventBySlug(db, slug);
|
|
15
|
+
if (!existing) throw createError({ statusCode: 404, statusMessage: 'Event not found' });
|
|
16
|
+
|
|
17
|
+
const result = await rsvpEvent(db, existing.id, user.id);
|
|
18
|
+
if (!result.success) {
|
|
19
|
+
throw createError({ statusCode: 400, statusMessage: result.error ?? 'RSVP failed' });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return { status: result.status };
|
|
23
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { deleteEvent, getEventBySlug } from '@commonpub/server';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* DELETE /api/events/:slug
|
|
5
|
+
* Delete an event (owner or admin).
|
|
6
|
+
*/
|
|
7
|
+
export default defineEventHandler(async (event) => {
|
|
8
|
+
requireFeature('events');
|
|
9
|
+
const user = requireAuth(event);
|
|
10
|
+
const db = useDB();
|
|
11
|
+
const slug = getRouterParam(event, 'slug');
|
|
12
|
+
if (!slug) throw createError({ statusCode: 400, statusMessage: 'Missing slug' });
|
|
13
|
+
|
|
14
|
+
const existing = await getEventBySlug(db, slug);
|
|
15
|
+
if (!existing) throw createError({ statusCode: 404, statusMessage: 'Event not found' });
|
|
16
|
+
|
|
17
|
+
const isAdmin = user.role === 'admin';
|
|
18
|
+
const deleted = await deleteEvent(db, existing.id, user.id, isAdmin);
|
|
19
|
+
if (!deleted) throw createError({ statusCode: 403, statusMessage: 'Unauthorized' });
|
|
20
|
+
|
|
21
|
+
return { deleted: true };
|
|
22
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { getEventBySlug } from '@commonpub/server';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* GET /api/events/:slug
|
|
5
|
+
* Get event details by slug (public).
|
|
6
|
+
*/
|
|
7
|
+
export default defineEventHandler(async (event) => {
|
|
8
|
+
requireFeature('events');
|
|
9
|
+
const db = useDB();
|
|
10
|
+
const slug = getRouterParam(event, 'slug');
|
|
11
|
+
if (!slug) throw createError({ statusCode: 400, statusMessage: 'Missing slug' });
|
|
12
|
+
|
|
13
|
+
const result = await getEventBySlug(db, slug);
|
|
14
|
+
if (!result) throw createError({ statusCode: 404, statusMessage: 'Event not found' });
|
|
15
|
+
|
|
16
|
+
return result;
|
|
17
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { updateEvent } from '@commonpub/server';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
const updateEventSchema = z.object({
|
|
5
|
+
title: z.string().min(1).max(255).optional(),
|
|
6
|
+
description: z.string().max(10000).optional(),
|
|
7
|
+
coverImage: z.string().max(500).optional(),
|
|
8
|
+
eventType: z.enum(['in-person', 'online', 'hybrid']).optional(),
|
|
9
|
+
status: z.enum(['draft', 'published', 'active', 'completed', 'cancelled']).optional(),
|
|
10
|
+
startDate: z.string().datetime().optional(),
|
|
11
|
+
endDate: z.string().datetime().optional(),
|
|
12
|
+
timezone: z.string().max(64).optional(),
|
|
13
|
+
location: z.string().max(500).optional(),
|
|
14
|
+
locationUrl: z.string().url().max(500).optional(),
|
|
15
|
+
onlineUrl: z.string().url().max(500).optional(),
|
|
16
|
+
capacity: z.number().int().min(1).max(100000).optional(),
|
|
17
|
+
isFeatured: z.boolean().optional(),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* PUT /api/events/:slug
|
|
22
|
+
* Update an event (owner or admin).
|
|
23
|
+
*/
|
|
24
|
+
export default defineEventHandler(async (event) => {
|
|
25
|
+
requireFeature('events');
|
|
26
|
+
const user = requireAuth(event);
|
|
27
|
+
const db = useDB();
|
|
28
|
+
const slug = getRouterParam(event, 'slug');
|
|
29
|
+
if (!slug) throw createError({ statusCode: 400, statusMessage: 'Missing slug' });
|
|
30
|
+
|
|
31
|
+
const body = await parseBody(event, updateEventSchema);
|
|
32
|
+
const isAdmin = user.role === 'admin';
|
|
33
|
+
|
|
34
|
+
const result = await updateEvent(db, slug, user.id, body, isAdmin);
|
|
35
|
+
if (!result) throw createError({ statusCode: 404, statusMessage: 'Event not found or unauthorized' });
|
|
36
|
+
|
|
37
|
+
return result;
|
|
38
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { listEvents } from '@commonpub/server';
|
|
2
|
+
import type { EventStatus } from '@commonpub/server';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* GET /api/events
|
|
6
|
+
* List published/active events (public).
|
|
7
|
+
*/
|
|
8
|
+
export default defineEventHandler(async (event) => {
|
|
9
|
+
requireFeature('events');
|
|
10
|
+
const db = useDB();
|
|
11
|
+
const query = getQuery(event);
|
|
12
|
+
|
|
13
|
+
return listEvents(db, {
|
|
14
|
+
status: (query.status as EventStatus) || undefined,
|
|
15
|
+
hubId: (query.hubId as string) || undefined,
|
|
16
|
+
upcoming: query.upcoming === 'true',
|
|
17
|
+
featured: query.featured === 'true',
|
|
18
|
+
limit: query.limit ? Number(query.limit) : undefined,
|
|
19
|
+
offset: query.offset ? Number(query.offset) : undefined,
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { createEvent } from '@commonpub/server';
|
|
2
|
+
import { generateSlug } from '@commonpub/server';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
const createEventSchema = z.object({
|
|
6
|
+
title: z.string().min(1).max(255),
|
|
7
|
+
description: z.string().max(10000).optional(),
|
|
8
|
+
coverImage: z.string().max(500).optional(),
|
|
9
|
+
eventType: z.enum(['in-person', 'online', 'hybrid']).optional(),
|
|
10
|
+
startDate: z.string().datetime(),
|
|
11
|
+
endDate: z.string().datetime(),
|
|
12
|
+
timezone: z.string().max(64).optional(),
|
|
13
|
+
location: z.string().max(500).optional(),
|
|
14
|
+
locationUrl: z.string().url().max(500).optional(),
|
|
15
|
+
onlineUrl: z.string().url().max(500).optional(),
|
|
16
|
+
capacity: z.number().int().min(1).max(100000).optional(),
|
|
17
|
+
hubId: z.string().uuid().optional(),
|
|
18
|
+
}).refine(d => new Date(d.endDate) > new Date(d.startDate), {
|
|
19
|
+
message: 'End date must be after start date',
|
|
20
|
+
path: ['endDate'],
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* POST /api/events
|
|
25
|
+
* Create a new event (authenticated).
|
|
26
|
+
*/
|
|
27
|
+
export default defineEventHandler(async (event) => {
|
|
28
|
+
requireFeature('events');
|
|
29
|
+
const user = requireAuth(event);
|
|
30
|
+
const db = useDB();
|
|
31
|
+
const body = await parseBody(event, createEventSchema);
|
|
32
|
+
|
|
33
|
+
const slug = generateSlug(body.title);
|
|
34
|
+
|
|
35
|
+
return createEvent(db, {
|
|
36
|
+
...body,
|
|
37
|
+
slug,
|
|
38
|
+
createdBy: user.id,
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { getPollOptions, getUserPollVote } from '@commonpub/server';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* GET /api/hubs/:slug/posts/:postId/poll-options
|
|
5
|
+
* Get poll options and the user's vote (if authenticated).
|
|
6
|
+
*/
|
|
7
|
+
export default defineEventHandler(async (event) => {
|
|
8
|
+
requireFeature('hubs');
|
|
9
|
+
const db = useDB();
|
|
10
|
+
const postId = getRouterParam(event, 'postId');
|
|
11
|
+
if (!postId) throw createError({ statusCode: 400, statusMessage: 'Missing postId' });
|
|
12
|
+
|
|
13
|
+
const options = await getPollOptions(db, postId);
|
|
14
|
+
const user = getOptionalUser(event);
|
|
15
|
+
const userVote = user ? await getUserPollVote(db, postId, user.id) : null;
|
|
16
|
+
|
|
17
|
+
return { options, userVote };
|
|
18
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { voteOnPoll } from '@commonpub/server';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
const pollVoteSchema = z.object({
|
|
5
|
+
optionId: z.string().uuid(),
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* POST /api/hubs/:slug/posts/:postId/poll-vote
|
|
10
|
+
* Vote on a poll option.
|
|
11
|
+
*/
|
|
12
|
+
export default defineEventHandler(async (event) => {
|
|
13
|
+
requireFeature('hubs');
|
|
14
|
+
const user = requireAuth(event);
|
|
15
|
+
const db = useDB();
|
|
16
|
+
const postId = getRouterParam(event, 'postId');
|
|
17
|
+
if (!postId) throw createError({ statusCode: 400, statusMessage: 'Missing postId' });
|
|
18
|
+
|
|
19
|
+
const body = await parseBody(event, pollVoteSchema);
|
|
20
|
+
const result = await voteOnPoll(db, postId, body.optionId, user.id);
|
|
21
|
+
|
|
22
|
+
if (!result.voted) {
|
|
23
|
+
throw createError({ statusCode: 400, statusMessage: result.error ?? 'Vote failed' });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return { voted: true };
|
|
27
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { voteOnPost } from '@commonpub/server';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
const voteSchema = z.object({
|
|
5
|
+
direction: z.enum(['up', 'down']),
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* POST /api/hubs/:slug/posts/:postId/vote
|
|
10
|
+
* Upvote or downvote a hub post. Toggles off if same direction, flips if different.
|
|
11
|
+
*/
|
|
12
|
+
export default defineEventHandler(async (event) => {
|
|
13
|
+
requireFeature('hubs');
|
|
14
|
+
const user = requireAuth(event);
|
|
15
|
+
const db = useDB();
|
|
16
|
+
const postId = getRouterParam(event, 'postId');
|
|
17
|
+
if (!postId) throw createError({ statusCode: 400, statusMessage: 'Missing postId' });
|
|
18
|
+
|
|
19
|
+
const body = await parseBody(event, voteSchema);
|
|
20
|
+
return voteOnPost(db, postId, user.id, body.direction);
|
|
21
|
+
});
|