Initial commit of Vellum, an event photo product for guest uploads, host moderation, and original-quality galleries.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-07 19:36:14 -04:00
co-authored by Cursor
commit 27e2f196eb
149 changed files with 13847 additions and 0 deletions
+216
View File
@@ -0,0 +1,216 @@
import { z } from "zod";
export const eventStatusSchema = z.enum(["draft", "published", "closed"]);
export const groupRoleSchema = z.enum(["owner", "member"]);
export const eventRoleSchema = z.enum([
"owner",
"manager",
"moderator",
"viewer",
]);
export const platformRoleSchema = z.enum([
"super_admin",
"admin",
"moderator",
"viewer",
]);
export const eventCreatePolicySchema = z.enum(["open", "invite", "admin_only"]);
export const entitlementSourceSchema = z.enum([
"signup_default",
"invite",
"code",
"platform",
]);
export const photoProcessingStatusSchema = z.enum([
"uploading",
"processing",
"ready",
"failed",
]);
export const photoVisibilitySchema = z.enum([
"pending",
"public",
"hidden",
"private",
"rejected",
]);
export const photoJobStatusSchema = z.enum([
"pending",
"processing",
"completed",
"failed",
]);
export const allowedImageTypes = [
"image/jpeg",
"image/png",
"image/webp",
"image/heic",
"image/heif",
] as const;
export const allowedImageTypeSchema = z.enum(allowedImageTypes);
export const MAX_PHOTO_BYTES = 25 * 1024 * 1024;
export const eventSlugSchema = z
.string()
.min(3)
.max(64)
.regex(
/^[a-z0-9]+(?:-[a-z0-9]+)*$/,
"Use lowercase letters, numbers, and hyphens",
);
export const contributorNameSchema = z
.string()
.trim()
.max(80)
.optional()
.transform((value) => {
const trimmed = value?.trim();
return trimmed ? trimmed : undefined;
});
export const guestEmailSchema = z
.string()
.trim()
.email()
.max(200)
.optional()
.or(z.literal(""));
export const guestNoteSchema = z.string().trim().max(2000).optional();
export const createEventInputSchema = z.object({
groupId: z.string().uuid().optional(),
title: z.string().trim().min(1).max(120),
slug: eventSlugSchema.optional(),
description: z.string().trim().max(2000).optional(),
startsAt: z.coerce.date().optional().nullable(),
endsAt: z.coerce.date().optional().nullable(),
inviteCode: z.string().trim().max(80).optional(),
});
export const updateEventInputSchema = z.object({
eventId: z.string().uuid(),
title: z.string().trim().min(1).max(120).optional(),
slug: eventSlugSchema.optional(),
description: z.string().trim().max(2000).nullable().optional(),
startsAt: z.coerce.date().nullable().optional(),
endsAt: z.coerce.date().nullable().optional(),
status: eventStatusSchema.optional(),
listed: z.boolean().optional(),
uploadEnabled: z.boolean().optional(),
});
export const ensureGuestInputSchema = z.object({
eventSlug: eventSlugSchema,
displayName: contributorNameSchema,
email: z
.string()
.trim()
.max(200)
.optional()
.transform((value) => {
const trimmed = value?.trim();
return trimmed ? trimmed : undefined;
})
.pipe(z.string().email().optional()),
notifyWhenReady: z.boolean().optional(),
note: guestNoteSchema,
});
export const startSubmissionInputSchema = z.object({
eventSlug: eventSlugSchema,
});
export const createPhotoInputSchema = z.object({
eventSlug: eventSlugSchema,
submissionId: z.string().uuid(),
contentType: allowedImageTypeSchema,
fileName: z.string().min(1).max(255),
byteSize: z.number().int().positive().max(MAX_PHOTO_BYTES),
});
export const completePhotoInputSchema = z.object({
photoId: z.string().uuid(),
});
export const moderatePhotoInputSchema = z.object({
photoId: z.string().uuid(),
visibility: photoVisibilitySchema,
});
export const moderateSubmissionInputSchema = z.object({
submissionId: z.string().uuid(),
visibility: photoVisibilitySchema,
});
export const updateDeploymentSettingsInputSchema = z.object({
openSignup: z.boolean().optional(),
eventCreatePolicy: eventCreatePolicySchema.optional(),
defaultEventLimit: z.number().int().min(0).max(1000).optional(),
});
export const createEmailInviteInputSchema = z.object({
email: z.string().trim().email().max(200),
groupId: z.string().uuid().optional(),
eventId: z.string().uuid().optional(),
groupRole: groupRoleSchema.optional(),
eventRole: eventRoleSchema.optional(),
grantUnlimitedEvents: z.boolean().optional(),
grantEventLimit: z.number().int().min(1).max(1000).optional(),
grantComplimentary: z.boolean().optional(),
});
export const createInviteCodeInputSchema = z.object({
reusable: z.boolean().optional(),
maxUses: z.number().int().min(1).max(10000).optional(),
groupId: z.string().uuid().optional(),
eventId: z.string().uuid().optional(),
groupRole: groupRoleSchema.optional(),
eventRole: eventRoleSchema.optional(),
grantUnlimitedEvents: z.boolean().optional(),
grantEventLimit: z.number().int().min(1).max(1000).optional(),
grantComplimentary: z.boolean().optional(),
expiresAt: z.coerce.date().optional().nullable(),
});
export const redeemInviteInputSchema = z.object({
token: z.string().trim().min(4).max(120),
});
export const grantEntitlementInputSchema = z.object({
groupId: z.string().uuid(),
eventLimit: z.number().int().min(1).max(10000).nullable().optional(),
expiresAt: z.coerce.date().optional().nullable(),
complimentary: z.boolean().optional(),
});
export const setPlatformRoleInputSchema = z.object({
userId: z.string().min(1),
role: platformRoleSchema.nullable(),
});
export const setEventMemberInputSchema = z.object({
eventId: z.string().uuid(),
userId: z.string().min(1).optional(),
email: z.string().trim().email().optional(),
role: eventRoleSchema,
});
export const setGroupMemberInputSchema = z.object({
groupId: z.string().uuid(),
userId: z.string().min(1).optional(),
email: z.string().trim().email().optional(),
role: groupRoleSchema,
});
export type EventStatus = z.infer<typeof eventStatusSchema>;
export type GroupRole = z.infer<typeof groupRoleSchema>;
export type EventRole = z.infer<typeof eventRoleSchema>;
export type PlatformRole = z.infer<typeof platformRoleSchema>;
export type EventCreatePolicy = z.infer<typeof eventCreatePolicySchema>;
export type PhotoProcessingStatus = z.infer<typeof photoProcessingStatusSchema>;
export type PhotoVisibility = z.infer<typeof photoVisibilitySchema>;
export type AllowedImageType = z.infer<typeof allowedImageTypeSchema>;