- 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>
103 lines
3.9 KiB
TypeScript
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>
|