Initial commit: DrinkTracker full-stack app

Next.js 14 drink collection tracker with AI-powered search,
menu scanning, ratings, wishlist, sharing, and CSV backup/restore.

Features:
- Auth (credentials + OAuth ready)
- Drink collection with ratings and reviews
- AI search via Claude/OpenAI with search history
- Menu photo scanning with AI extraction
- Wishlist / Try Later system
- Public sharing via slug URLs
- CSV backup and restore (merge/replace modes)
- Docker Compose for Postgres + MinIO + dev server

Security: docker-compose files use env var interpolation
instead of hardcoded secrets.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
JP Scott
2026-03-01 12:27:08 -07:00
commit 969bc9347a
115 changed files with 19397 additions and 0 deletions

73
src/lib/validators.ts Normal file
View File

@@ -0,0 +1,73 @@
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>