diff --git a/src/lib/schemas.ts b/src/lib/schemas.ts index c93ef8d..3cbf10b 100644 --- a/src/lib/schemas.ts +++ b/src/lib/schemas.ts @@ -10,6 +10,16 @@ const FeedbackQuestionBaseSchema = z.object({ help: z.string().max(500).optional(), }); +/** One date/time option in a `date_ranked_choice` question. Times are stored as UTC ISO 8601 strings. */ +export const DateRankedOptionSchema = z.object({ + id: z.string().min(1).max(64).regex(/^[a-zA-Z0-9_-]+$/, { + message: 'option id may only contain letters, digits, "-" and "_"', + }), + start: z.string().datetime({ offset: true }), + end: z.string().datetime({ offset: true }).optional(), + label: z.string().max(200).optional(), +}); + export const FeedbackQuestionSchema = z.discriminatedUnion('type', [ FeedbackQuestionBaseSchema.extend({ type: z.literal('short_text'), @@ -37,6 +47,20 @@ export const FeedbackQuestionSchema = z.discriminatedUnion('type', [ FeedbackQuestionBaseSchema.extend({ type: z.literal('boolean'), }), + FeedbackQuestionBaseSchema.extend({ + type: z.literal('date_ranked_choice'), + options: z.array(DateRankedOptionSchema).min(2).max(50) + .refine( + (opts) => new Set(opts.map((o) => o.id)).size === opts.length, + { message: 'date_ranked_choice option ids must be unique' }, + ), + // Scale is locked at 1-5 (5-point Likert) per design — only the labels are author-configurable. + scale: z.object({ + min_label: z.string().max(50).optional(), + max_label: z.string().max(50).optional(), + }).optional(), + allow_partial: z.boolean().optional(), + }), ]); /** Version stamp like `0.260505` (YYMMDD) or `0.260505.b` for same-day re-edits. */ @@ -77,6 +101,14 @@ export const FeedbackInstanceUpdateSchema = z.object({ const ANSWER_MAX = 5000; +/** Per-option rating map for `date_ranked_choice` answers, e.g. `{ opt1: 5, opt2: null }`. */ +const DateRankedAnswerSchema = z.record( + z.string().min(1).max(64), + z.number().int().min(1).max(5).nullable(), +).refine((m) => Object.keys(m).length <= 50, { + message: 'too many rated options', +}); + export const FeedbackSubmissionSchema = z.object({ display_name: z.string().max(80).nullable().optional() .transform((v) => (v && v.trim() ? v.trim().replace(/[\r\n]+/g, ' ') : null)), @@ -88,6 +120,7 @@ export const FeedbackSubmissionSchema = z.object({ z.number(), z.boolean(), z.array(z.string().max(200)).max(20), + DateRankedAnswerSchema, z.null(), ]), ), @@ -121,3 +154,5 @@ export const ShareCreateSchema = z.object({ export type FeedbackQuestion = z.infer; export type FeedbackFormDefinition = z.infer; +export type DateRankedOption = z.infer; +export type DateRankedAnswer = z.infer;