@commonpub/layer 0.11.0 → 0.14.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/DiscussionItem.vue +4 -1
- 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/hub/HubDiscussions.vue +2 -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/useAuth.ts +20 -15
- package/composables/useFeatures.ts +34 -13
- package/layouts/admin.vue +1 -0
- package/layouts/default.vue +50 -108
- package/middleware/feature-gate.global.ts +9 -4
- package/package.json +6 -6
- package/pages/admin/navigation.vue +350 -0
- package/pages/contests/[slug]/index.vue +1 -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/pages/federated-hubs/[id]/index.vue +1 -0
- package/pages/hubs/[slug]/index.vue +1 -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
- package/types/hub.ts +1 -0
|
@@ -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
|
+
});
|