Files
drinktracker/src/lib/validators.ts
JP Scott dc1ad4d0c0 Add recipes, images, AI photo ID, barcode scanning & ingredient matching
- Fuzzy ingredient matching for bar inventory against recipes
- AI photo identification API for bottles/labels (drink + bar context)
- Barcode scanner with photo toggle for My Bar
- Barcode scan + photo ID buttons on Add Drink form
- Auto-pull product images from Open Food Facts barcode lookup
- Recipes section on drink detail pages with bar availability
- Dedicated Recipes page in sidebar navigation
- Bar item image support (schema, upload, display)
- Drink detail image upload component
- MinIO image proxy through Next.js rewrites (fixes broken image links)
- Improved category mapping (energy drinks → Mixers, not Spirits)
- Re-process saved recipe ingredients against current bar inventory

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 22:26:17 -07:00

103 lines
3.9 KiB
TypeScript

import { z } from "zod"
export const drinkCreateSchema = z.object({
name: z.string().min(1, "Name is required").max(200),
type: z.enum(["BEER", "WINE", "COCKTAIL", "SPIRIT", "OTHER"]),
subType: z.string().max(100).optional(),
brewery: z.string().max(200).optional(),
region: z.string().max(200).optional(),
abv: z.number().min(0).max(100).optional(),
description: z.string().max(2000).optional(),
imageUrl: z.string().url().optional(),
})
export const drinkUpdateSchema = drinkCreateSchema.partial()
export const ratingCreateSchema = z.object({
drinkId: z.string().min(1),
score: z.number().int().min(1).max(5),
notes: z.string().max(2000).optional(),
wouldReorder: z.boolean().optional(),
location: z.string().max(200).optional(),
})
export const ratingUpdateSchema = ratingCreateSchema.omit({ drinkId: true }).partial()
export const apiKeySchema = z.object({
provider: z.enum(["claude", "openai"]),
apiKey: z.string().min(1, "API key is required"),
label: z.string().max(100).optional(),
})
export const userPreferenceSchema = z.object({
preferredStyles: z.array(z.string().max(50)).max(20).optional(),
avoidedStyles: z.array(z.string().max(50)).max(20).optional(),
minAbv: z.number().min(0).max(100).optional().nullable(),
maxAbv: z.number().min(0).max(100).optional().nullable(),
defaultProvider: z.enum(["claude", "openai"]).optional().nullable(),
})
export const wishlistCreateSchema = z.object({
name: z.string().min(1, "Name is required").max(200),
type: z.enum(["BEER", "WINE", "COCKTAIL", "SPIRIT", "OTHER"]),
subType: z.string().max(100).optional(),
brewery: z.string().max(200).optional(),
abv: z.number().min(0).max(100).optional(),
description: z.string().max(2000).optional(),
notes: z.string().max(2000).optional(),
source: z.string().max(50).optional(),
})
export const sharedListCreateSchema = z.object({
title: z.string().min(1, "Title is required").max(200),
description: z.string().max(2000).optional(),
listType: z.enum(["collection", "wishlist", "custom"]).default("collection"),
isPublic: z.boolean().default(true),
drinkIds: z.array(z.string()).default([]),
})
export const sharedListUpdateSchema = z.object({
title: z.string().min(1).max(200).optional(),
description: z.string().max(2000).optional().nullable(),
isPublic: z.boolean().optional(),
})
export type DrinkCreate = z.infer<typeof drinkCreateSchema>
export type DrinkUpdate = z.infer<typeof drinkUpdateSchema>
export type RatingCreate = z.infer<typeof ratingCreateSchema>
export type RatingUpdate = z.infer<typeof ratingUpdateSchema>
export type ApiKeyInput = z.infer<typeof apiKeySchema>
export type UserPreferenceInput = z.infer<typeof userPreferenceSchema>
export type WishlistCreate = z.infer<typeof wishlistCreateSchema>
export type SharedListCreate = z.infer<typeof sharedListCreateSchema>
export type SharedListUpdate = z.infer<typeof sharedListUpdateSchema>
export const barItemCreateSchema = z.object({
name: z.string().min(1, "Name is required").max(200),
category: z.enum(["SPIRITS", "LIQUEURS", "MIXERS", "BITTERS", "GARNISHES", "TOOLS"]),
quantity: z.enum(["FULL", "HALF", "LOW", "EMPTY"]).default("FULL"),
notes: z.string().max(2000).optional(),
barcode: z.string().max(50).optional(),
imageUrl: z.string().url().optional().or(z.literal("")),
})
export const barItemUpdateSchema = barItemCreateSchema.partial()
export type BarItemCreate = z.infer<typeof barItemCreateSchema>
export type BarItemUpdate = z.infer<typeof barItemUpdateSchema>
export const recipeCreateSchema = z.object({
title: z.string().min(1).max(200),
ingredients: z.array(z.object({
name: z.string(),
amount: z.string(),
available: z.boolean(),
})),
steps: z.array(z.string()),
garnish: z.string().max(200).optional().nullable(),
glassware: z.string().max(200).optional().nullable(),
sourceDrinkId: z.string().optional().nullable(),
notes: z.string().max(2000).optional().nullable(),
})
export type RecipeCreate = z.infer<typeof recipeCreateSchema>