- Drink Images: upload/display photos of bottles/cans on drink cards and detail pages - My Bar: inventory tracker for spirits, liqueurs, mixers, bitters, garnishes, tools - Bartender: AI-powered cocktail recipe generation, "what can I make" suggestions, saved recipes. Cross-references bar inventory for ingredient availability. - Recommend: AI flavor profile analysis, personalized drink recommendations, "find similar" drinks based on highly-rated favorites - Navigation: desktop sidebar with all 8 routes, mobile bottom nav with 4 primary items + "More" popup menu - New Prisma models: BarItem, Recipe, FlavorProfile - Backup/restore updated to include bar items Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
103 lines
2.4 KiB
TypeScript
103 lines
2.4 KiB
TypeScript
"use client"
|
|
|
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
|
|
import type { RecipeCreate } from "@/lib/validators"
|
|
|
|
export interface RecipeIngredient {
|
|
name: string
|
|
amount: string
|
|
available: boolean
|
|
}
|
|
|
|
export interface Recipe {
|
|
id: string
|
|
userId: string
|
|
title: string
|
|
ingredients: RecipeIngredient[]
|
|
steps: string[]
|
|
garnish: string | null
|
|
glassware: string | null
|
|
sourceDrinkId: string | null
|
|
notes: string | null
|
|
createdAt: string
|
|
updatedAt: string
|
|
sourceDrink?: { name: string; type: string } | null
|
|
}
|
|
|
|
export interface SuggestedCocktail {
|
|
title: string
|
|
ingredients: RecipeIngredient[]
|
|
steps: string[]
|
|
garnish?: string
|
|
glassware?: string
|
|
missingCount: number
|
|
}
|
|
|
|
async function fetchWithError(url: string, options?: RequestInit) {
|
|
const res = await fetch(url, options)
|
|
if (!res.ok) {
|
|
const body = await res.json().catch(() => ({}))
|
|
throw new Error(body.error || `Request failed with status ${res.status}`)
|
|
}
|
|
return res.json()
|
|
}
|
|
|
|
export function useRecreateRecipe() {
|
|
return useMutation({
|
|
mutationFn: (data: { cocktailName: string; drinkId?: string }) =>
|
|
fetchWithError("/api/bartender/recreate", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(data),
|
|
}),
|
|
})
|
|
}
|
|
|
|
export function useSuggestCocktails() {
|
|
return useMutation({
|
|
mutationFn: () =>
|
|
fetchWithError("/api/bartender/suggest", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({}),
|
|
}),
|
|
})
|
|
}
|
|
|
|
export function useRecipes() {
|
|
return useQuery<{ recipes: Recipe[] }>({
|
|
queryKey: ["recipes"],
|
|
queryFn: () => fetchWithError("/api/recipes"),
|
|
})
|
|
}
|
|
|
|
export function useSaveRecipe() {
|
|
const queryClient = useQueryClient()
|
|
|
|
return useMutation({
|
|
mutationFn: (data: RecipeCreate) =>
|
|
fetchWithError("/api/recipes", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(data),
|
|
}),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ["recipes"] })
|
|
},
|
|
})
|
|
}
|
|
|
|
export function useDeleteRecipe() {
|
|
const queryClient = useQueryClient()
|
|
|
|
return useMutation({
|
|
mutationFn: (id: string) =>
|
|
fetchWithError(`/api/recipes/${id}`, {
|
|
method: "DELETE",
|
|
}),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ["recipes"] })
|
|
},
|
|
})
|
|
}
|