@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.
Files changed (47) hide show
  1. package/components/DiscussionItem.vue +4 -1
  2. package/components/EventCard.vue +121 -0
  3. package/components/PollDisplay.vue +108 -0
  4. package/components/PostVoteButtons.vue +108 -0
  5. package/components/contest/ContestJudgeManager.vue +110 -0
  6. package/components/hub/HubDiscussions.vue +2 -0
  7. package/components/nav/MobileNavRenderer.vue +94 -0
  8. package/components/nav/NavDropdown.vue +101 -0
  9. package/components/nav/NavLink.vue +40 -0
  10. package/components/nav/NavRenderer.vue +51 -0
  11. package/composables/useAuth.ts +20 -15
  12. package/composables/useFeatures.ts +34 -13
  13. package/layouts/admin.vue +1 -0
  14. package/layouts/default.vue +50 -108
  15. package/middleware/feature-gate.global.ts +9 -4
  16. package/package.json +6 -6
  17. package/pages/admin/navigation.vue +350 -0
  18. package/pages/contests/[slug]/index.vue +1 -0
  19. package/pages/events/[slug]/edit.vue +182 -0
  20. package/pages/events/[slug]/index.vue +249 -0
  21. package/pages/events/create.vue +140 -0
  22. package/pages/events/index.vue +47 -0
  23. package/pages/federated-hubs/[id]/index.vue +1 -0
  24. package/pages/hubs/[slug]/index.vue +1 -0
  25. package/server/api/admin/navigation/items.get.ts +11 -0
  26. package/server/api/admin/navigation/items.put.ts +51 -0
  27. package/server/api/contests/[slug]/entries/[entryId]/vote.delete.ts +20 -0
  28. package/server/api/contests/[slug]/entries/[entryId]/vote.post.ts +20 -0
  29. package/server/api/contests/[slug]/judge.post.ts +5 -7
  30. package/server/api/contests/[slug]/judges/[userId].delete.ts +26 -0
  31. package/server/api/contests/[slug]/judges/accept.post.ts +21 -0
  32. package/server/api/contests/[slug]/judges/index.get.ts +17 -0
  33. package/server/api/contests/[slug]/judges/index.post.ts +36 -0
  34. package/server/api/events/[slug]/attendees.get.ts +23 -0
  35. package/server/api/events/[slug]/rsvp.delete.ts +23 -0
  36. package/server/api/events/[slug]/rsvp.post.ts +23 -0
  37. package/server/api/events/[slug].delete.ts +22 -0
  38. package/server/api/events/[slug].get.ts +17 -0
  39. package/server/api/events/[slug].put.ts +38 -0
  40. package/server/api/events/index.get.ts +21 -0
  41. package/server/api/events/index.post.ts +40 -0
  42. package/server/api/hubs/[slug]/posts/[postId]/poll-options.get.ts +18 -0
  43. package/server/api/hubs/[slug]/posts/[postId]/poll-vote.post.ts +27 -0
  44. package/server/api/hubs/[slug]/posts/[postId]/vote.post.ts +21 -0
  45. package/server/api/navigation/items.get.ts +10 -0
  46. package/server/middleware/features.ts +1 -0
  47. 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
+ });
@@ -0,0 +1,10 @@
1
+ import { getNavItems } from '@commonpub/server';
2
+
3
+ /**
4
+ * GET /api/navigation/items
5
+ * Returns the navigation item configuration (public).
6
+ */
7
+ export default defineEventHandler(async () => {
8
+ const db = useDB();
9
+ return getNavItems(db);
10
+ });
@@ -7,6 +7,7 @@ const ROUTE_FEATURE_MAP: Record<string, string> = {
7
7
  '/videos': 'video',
8
8
  '/admin': 'admin',
9
9
  '/contests': 'contests',
10
+ '/events': 'events',
10
11
  '/explainer': 'explainers',
11
12
  };
12
13
 
package/types/hub.ts CHANGED
@@ -23,6 +23,7 @@ export interface HubPostViewModel {
23
23
  author: HubPostAuthor
24
24
  createdAt: string
25
25
  likeCount: number
26
+ voteScore: number
26
27
  replyCount: number
27
28
  isPinned: boolean
28
29
  isLocked: boolean