Full-stack implementation across 7 sprints: - Backend: 5 Prisma models (StrawPoll, Option, Vote, Comment, Challenge), 4 enums, POLLS_ADMIN role, admin CRUD routes, public voting/SSE/widget endpoints, BullMQ auto-close queue, rate limiting - Admin: StrawPollsPage with inline drawers (campaigns pattern), PollResults bar chart, sidebar under Advocacy - Public: dedicated poll lander with real-time SSE updates, browse page, anonymous voting with token dedup - MkDocs: straw-poll-widget.js hydration (inline vote + card link modes), GrapesJS block types - Social: feed activity (poll_voted), friend badge integration, challenge notifications, notification preferences - Feature flag: enablePolls toggle in Settings, FeatureGate, Zod schema Bunker Admin
68 lines
2.9 KiB
TypeScript
68 lines
2.9 KiB
TypeScript
import { z } from 'zod';
|
|
import { StrawPollType, StrawPollStatus, StrawPollIdentityMode, StrawPollResultVisibility } from '@prisma/client';
|
|
|
|
export const createStrawPollSchema = z.object({
|
|
title: z.string().min(1, 'Title is required').max(200),
|
|
description: z.string().max(2000).optional(),
|
|
type: z.nativeEnum(StrawPollType),
|
|
identityMode: z.nativeEnum(StrawPollIdentityMode).optional().default('ANONYMOUS'),
|
|
resultVisibility: z.nativeEnum(StrawPollResultVisibility).optional().default('LIVE'),
|
|
allowComments: z.boolean().optional().default(true),
|
|
closesAt: z.string().datetime().optional(),
|
|
closeThreshold: z.number().int().min(1).max(100000).nullable().optional(),
|
|
isPrivate: z.boolean().optional().default(false),
|
|
options: z.array(z.object({
|
|
label: z.string().min(1, 'Option label is required').max(500),
|
|
})).min(2, 'At least 2 options required').max(20, 'Maximum 20 options').optional(),
|
|
});
|
|
|
|
export const updateStrawPollSchema = z.object({
|
|
title: z.string().min(1).max(200).optional(),
|
|
description: z.string().max(2000).nullable().optional(),
|
|
identityMode: z.nativeEnum(StrawPollIdentityMode).optional(),
|
|
resultVisibility: z.nativeEnum(StrawPollResultVisibility).optional(),
|
|
allowComments: z.boolean().optional(),
|
|
closesAt: z.string().datetime().nullable().optional(),
|
|
closeThreshold: z.number().int().min(1).max(100000).nullable().optional(),
|
|
isPrivate: z.boolean().optional(),
|
|
options: z.array(z.object({
|
|
id: z.string().optional(),
|
|
label: z.string().min(1).max(500),
|
|
})).min(2).max(20).optional(),
|
|
});
|
|
|
|
export const submitStrawPollVoteSchema = z.object({
|
|
optionId: z.string().min(1, 'Option is required'),
|
|
voterName: z.string().min(1).max(100).optional(),
|
|
voterToken: z.string().optional(),
|
|
});
|
|
|
|
export const submitStrawPollCommentSchema = z.object({
|
|
authorName: z.string().min(1, 'Name is required').max(100),
|
|
content: z.string().min(1, 'Comment is required').max(2000),
|
|
});
|
|
|
|
export const listStrawPollsSchema = z.object({
|
|
page: z.coerce.number().int().positive().default(1),
|
|
limit: z.coerce.number().int().positive().max(100).default(20),
|
|
search: z.string().optional(),
|
|
status: z.nativeEnum(StrawPollStatus).optional(),
|
|
type: z.nativeEnum(StrawPollType).optional(),
|
|
});
|
|
|
|
export const challengeVoteSchema = z.object({
|
|
challengedUserId: z.string().min(1, 'User ID is required'),
|
|
});
|
|
|
|
export const generateLinksSchema = z.object({
|
|
count: z.number().int().min(1).max(500).default(10),
|
|
});
|
|
|
|
export type CreateStrawPollInput = z.infer<typeof createStrawPollSchema>;
|
|
export type UpdateStrawPollInput = z.infer<typeof updateStrawPollSchema>;
|
|
export type SubmitStrawPollVoteInput = z.infer<typeof submitStrawPollVoteSchema>;
|
|
export type SubmitStrawPollCommentInput = z.infer<typeof submitStrawPollCommentSchema>;
|
|
export type ListStrawPollsInput = z.infer<typeof listStrawPollsSchema>;
|
|
export type ChallengeVoteInput = z.infer<typeof challengeVoteSchema>;
|
|
export type GenerateLinksInput = z.infer<typeof generateLinksSchema>;
|