From acf02a78dd403bf9bbffe8e54abb15d6bd914c96 Mon Sep 17 00:00:00 2001 From: robin Date: Thu, 18 Dec 2025 13:24:41 +0100 Subject: [PATCH] feat: enhance bottle metadata with distillation/bottling dates and batch info --- src/components/EditBottleForm.tsx | 51 ++++++++++++++++++++++++++--- src/lib/gemini.ts | 3 ++ src/services/discover-whiskybase.ts | 20 ++++++++--- src/services/save-bottle.ts | 3 ++ src/services/update-bottle.ts | 6 ++++ src/types/whisky.ts | 3 ++ supa_schema.sql | 3 ++ 7 files changed, 81 insertions(+), 8 deletions(-) diff --git a/src/components/EditBottleForm.tsx b/src/components/EditBottleForm.tsx index 00f3663..7ee3ca9 100644 --- a/src/components/EditBottleForm.tsx +++ b/src/components/EditBottleForm.tsx @@ -15,6 +15,9 @@ interface EditBottleFormProps { age: number; whiskybase_id: string | null; purchase_price?: number | null; + distilled_at?: string | null; + bottled_at?: string | null; + batch_info?: string | null; }; onComplete?: () => void; } @@ -34,6 +37,9 @@ export default function EditBottleForm({ bottle, onComplete }: EditBottleFormPro age: bottle.age || 0, whiskybase_id: bottle.whiskybase_id || '', purchase_price: bottle.purchase_price || '', + distilled_at: bottle.distilled_at || '', + bottled_at: bottle.bottled_at || '', + batch_info: bottle.batch_info || '', }); const handleDiscover = async () => { @@ -45,7 +51,10 @@ export default function EditBottleForm({ bottle, onComplete }: EditBottleFormPro name: formData.name, distillery: formData.distillery, abv: formData.abv, - age: formData.age + age: formData.age, + distilled_at: formData.distilled_at || undefined, + bottled_at: formData.bottled_at || undefined, + batch_info: formData.batch_info || undefined, }); if (result.success && result.id) { @@ -73,6 +82,9 @@ export default function EditBottleForm({ bottle, onComplete }: EditBottleFormPro abv: Number(formData.abv), age: formData.age ? Number(formData.age) : undefined, purchase_price: formData.purchase_price ? Number(formData.purchase_price) : undefined, + distilled_at: formData.distilled_at || undefined, + bottled_at: formData.bottled_at || undefined, + batch_info: formData.batch_info || undefined, }); if (response.success) { @@ -215,9 +227,7 @@ export default function EditBottleForm({ bottle, onComplete }: EditBottleFormPro )}
- +
+ +
+ + setFormData({ ...formData, distilled_at: e.target.value })} + className="w-full px-4 py-2 bg-zinc-50 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-xl outline-none focus:ring-2 focus:ring-amber-500" + /> +
+ +
+ + setFormData({ ...formData, bottled_at: e.target.value })} + className="w-full px-4 py-2 bg-zinc-50 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-xl outline-none focus:ring-2 focus:ring-amber-500" + /> +
+ +
+ + setFormData({ ...formData, batch_info: e.target.value })} + className="w-full px-4 py-2 bg-zinc-50 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-xl outline-none focus:ring-2 focus:ring-amber-500" + /> +
{error &&

{error}

} diff --git a/src/lib/gemini.ts b/src/lib/gemini.ts index 1754d3c..ff6c59e 100644 --- a/src/lib/gemini.ts +++ b/src/lib/gemini.ts @@ -27,6 +27,9 @@ Output raw JSON matching the following schema: "vintage": string | null, "bottleCode": string | null, "whiskybaseId": string | null, + "distilled_at": string | null (e.g. "2010" or "12.05.2010"), + "bottled_at": string | null (e.g. "2022" or "15.11.2022"), + "batch_info": string | null (e.g. "Batch 1" or "L12.03.2022"), "is_whisky": boolean, "confidence": number (0-100) } diff --git a/src/services/discover-whiskybase.ts b/src/services/discover-whiskybase.ts index 70a0e48..5572595 100644 --- a/src/services/discover-whiskybase.ts +++ b/src/services/discover-whiskybase.ts @@ -9,6 +9,9 @@ export async function discoverWhiskybaseId(bottle: { distillery?: string; abv?: number; age?: number; + distilled_at?: string; + bottled_at?: string; + batch_info?: string; }) { // Both Gemini and Custom Search often use the same API key if created via AI Studio const apiKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY; @@ -24,24 +27,33 @@ export async function discoverWhiskybaseId(bottle: { try { // Construct targeted search query const queryParts = [ - `"${bottle.distillery || ''}"`, - `"${bottle.name}"`, + bottle.distillery ? `${bottle.distillery}` : '', // Removed quotes for more fuzzy matching + bottle.name ? `${bottle.name}` : '', bottle.abv ? `${bottle.abv}%` : '', - bottle.age ? `${bottle.age} year old` : '' + bottle.age ? `${bottle.age} year old` : '', + bottle.batch_info ? `"${bottle.batch_info}"` : '', + bottle.distilled_at ? `distilled ${bottle.distilled_at}` : '', + bottle.bottled_at ? `bottled ${bottle.bottled_at}` : '' ].filter(Boolean); const q = queryParts.join(' '); + console.log('Whiskybase Search Query:', q); + const url = `https://www.googleapis.com/customsearch/v1?key=${apiKey}&cx=${cx}&q=${encodeURIComponent(q)}`; const response = await fetch(url); const data = await response.json(); if (data.error) { + console.error('Google API Error Response:', data.error); throw new Error(data.error.message || 'Google API Error'); } if (!data.items || data.items.length === 0) { - return { success: false, error: 'Keine Treffer auf Whiskybase gefunden.' }; + return { + success: false, + error: `Keine Treffer auf Whiskybase gefunden. (Query: ${q})` + }; } // Try to find the first result that looks like a valid product page diff --git a/src/services/save-bottle.ts b/src/services/save-bottle.ts index 5ecb4c9..3180563 100644 --- a/src/services/save-bottle.ts +++ b/src/services/save-bottle.ts @@ -55,6 +55,9 @@ export async function saveBottle( status: 'sealed', // Default status is_whisky: metadata.is_whisky ?? true, confidence: metadata.confidence ?? 100, + distilled_at: metadata.distilled_at, + bottled_at: metadata.bottled_at, + batch_info: metadata.batch_info, }) .select() .single(); diff --git a/src/services/update-bottle.ts b/src/services/update-bottle.ts index 25c520c..7d0496c 100644 --- a/src/services/update-bottle.ts +++ b/src/services/update-bottle.ts @@ -12,6 +12,9 @@ export async function updateBottle(bottleId: string, data: { age?: number; whiskybase_id?: string; purchase_price?: number; + distilled_at?: string; + bottled_at?: string; + batch_info?: string; }) { const supabase = createServerActionClient({ cookies }); @@ -29,6 +32,9 @@ export async function updateBottle(bottleId: string, data: { age: data.age, whiskybase_id: data.whiskybase_id, purchase_price: data.purchase_price, + distilled_at: data.distilled_at, + bottled_at: data.bottled_at, + batch_info: data.batch_info, updated_at: new Date().toISOString(), }) .eq('id', bottleId) diff --git a/src/types/whisky.ts b/src/types/whisky.ts index 8ba44eb..cb9d15c 100644 --- a/src/types/whisky.ts +++ b/src/types/whisky.ts @@ -9,6 +9,9 @@ export const BottleMetadataSchema = z.object({ vintage: z.string().nullable(), bottleCode: z.string().nullable(), whiskybaseId: z.string().nullable(), + distilled_at: z.string().nullable(), + bottled_at: z.string().nullable(), + batch_info: z.string().nullable(), is_whisky: z.boolean().default(true), confidence: z.number().min(0).max(100).default(100), }); diff --git a/supa_schema.sql b/supa_schema.sql index 30871b6..2c8db52 100644 --- a/supa_schema.sql +++ b/supa_schema.sql @@ -44,6 +44,9 @@ CREATE TABLE IF NOT EXISTS bottles ( is_whisky BOOLEAN DEFAULT true, confidence INTEGER DEFAULT 100, finished_at TIMESTAMP WITH TIME ZONE, + distilled_at TEXT, + bottled_at TEXT, + batch_info TEXT, created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('Europe/Berlin'::text, now()), updated_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('Europe/Berlin'::text, now()) );