feat: enhance bottle metadata with distillation/bottling dates and batch info
This commit is contained in:
@@ -15,6 +15,9 @@ interface EditBottleFormProps {
|
|||||||
age: number;
|
age: number;
|
||||||
whiskybase_id: string | null;
|
whiskybase_id: string | null;
|
||||||
purchase_price?: number | null;
|
purchase_price?: number | null;
|
||||||
|
distilled_at?: string | null;
|
||||||
|
bottled_at?: string | null;
|
||||||
|
batch_info?: string | null;
|
||||||
};
|
};
|
||||||
onComplete?: () => void;
|
onComplete?: () => void;
|
||||||
}
|
}
|
||||||
@@ -34,6 +37,9 @@ export default function EditBottleForm({ bottle, onComplete }: EditBottleFormPro
|
|||||||
age: bottle.age || 0,
|
age: bottle.age || 0,
|
||||||
whiskybase_id: bottle.whiskybase_id || '',
|
whiskybase_id: bottle.whiskybase_id || '',
|
||||||
purchase_price: bottle.purchase_price || '',
|
purchase_price: bottle.purchase_price || '',
|
||||||
|
distilled_at: bottle.distilled_at || '',
|
||||||
|
bottled_at: bottle.bottled_at || '',
|
||||||
|
batch_info: bottle.batch_info || '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleDiscover = async () => {
|
const handleDiscover = async () => {
|
||||||
@@ -45,7 +51,10 @@ export default function EditBottleForm({ bottle, onComplete }: EditBottleFormPro
|
|||||||
name: formData.name,
|
name: formData.name,
|
||||||
distillery: formData.distillery,
|
distillery: formData.distillery,
|
||||||
abv: formData.abv,
|
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) {
|
if (result.success && result.id) {
|
||||||
@@ -73,6 +82,9 @@ export default function EditBottleForm({ bottle, onComplete }: EditBottleFormPro
|
|||||||
abv: Number(formData.abv),
|
abv: Number(formData.abv),
|
||||||
age: formData.age ? Number(formData.age) : undefined,
|
age: formData.age ? Number(formData.age) : undefined,
|
||||||
purchase_price: formData.purchase_price ? Number(formData.purchase_price) : 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) {
|
if (response.success) {
|
||||||
@@ -215,9 +227,7 @@ export default function EditBottleForm({ bottle, onComplete }: EditBottleFormPro
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<label className="text-[10px] font-black uppercase text-amber-600 ml-1 flex items-center gap-1">
|
<label className="text-[10px] font-black uppercase text-zinc-400 ml-1">Kaufpreis (€)</label>
|
||||||
<CircleDollarSign size={10} /> Kaufpreis (€)
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
step="0.01"
|
step="0.01"
|
||||||
@@ -227,6 +237,39 @@ export default function EditBottleForm({ bottle, onComplete }: EditBottleFormPro
|
|||||||
className="w-full px-4 py-2 bg-amber-50 dark:bg-amber-900/10 border border-amber-200 dark:border-amber-900/30 rounded-xl outline-none focus:ring-2 focus:ring-amber-500 font-bold text-amber-700 dark:text-amber-400"
|
className="w-full px-4 py-2 bg-amber-50 dark:bg-amber-900/10 border border-amber-200 dark:border-amber-900/30 rounded-xl outline-none focus:ring-2 focus:ring-amber-500 font-bold text-amber-700 dark:text-amber-400"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-[10px] font-black uppercase text-zinc-400 ml-1">Destilliert</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="z.B. 2010"
|
||||||
|
value={formData.distilled_at}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-[10px] font-black uppercase text-zinc-400 ml-1">Abgefüllt</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="z.B. 2022"
|
||||||
|
value={formData.bottled_at}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1 md:col-span-2">
|
||||||
|
<label className="text-[10px] font-black uppercase text-zinc-400 ml-1">Batch / Code</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="z.B. Batch 12 oder L-Code"
|
||||||
|
value={formData.batch_info}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && <p className="text-red-500 text-xs italic">{error}</p>}
|
{error && <p className="text-red-500 text-xs italic">{error}</p>}
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ Output raw JSON matching the following schema:
|
|||||||
"vintage": string | null,
|
"vintage": string | null,
|
||||||
"bottleCode": string | null,
|
"bottleCode": string | null,
|
||||||
"whiskybaseId": 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,
|
"is_whisky": boolean,
|
||||||
"confidence": number (0-100)
|
"confidence": number (0-100)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ export async function discoverWhiskybaseId(bottle: {
|
|||||||
distillery?: string;
|
distillery?: string;
|
||||||
abv?: number;
|
abv?: number;
|
||||||
age?: 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
|
// 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;
|
const apiKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY;
|
||||||
@@ -24,24 +27,33 @@ export async function discoverWhiskybaseId(bottle: {
|
|||||||
try {
|
try {
|
||||||
// Construct targeted search query
|
// Construct targeted search query
|
||||||
const queryParts = [
|
const queryParts = [
|
||||||
`"${bottle.distillery || ''}"`,
|
bottle.distillery ? `${bottle.distillery}` : '', // Removed quotes for more fuzzy matching
|
||||||
`"${bottle.name}"`,
|
bottle.name ? `${bottle.name}` : '',
|
||||||
bottle.abv ? `${bottle.abv}%` : '',
|
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);
|
].filter(Boolean);
|
||||||
|
|
||||||
const q = queryParts.join(' ');
|
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 url = `https://www.googleapis.com/customsearch/v1?key=${apiKey}&cx=${cx}&q=${encodeURIComponent(q)}`;
|
||||||
|
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
|
console.error('Google API Error Response:', data.error);
|
||||||
throw new Error(data.error.message || 'Google API Error');
|
throw new Error(data.error.message || 'Google API Error');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data.items || data.items.length === 0) {
|
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
|
// Try to find the first result that looks like a valid product page
|
||||||
|
|||||||
@@ -55,6 +55,9 @@ export async function saveBottle(
|
|||||||
status: 'sealed', // Default status
|
status: 'sealed', // Default status
|
||||||
is_whisky: metadata.is_whisky ?? true,
|
is_whisky: metadata.is_whisky ?? true,
|
||||||
confidence: metadata.confidence ?? 100,
|
confidence: metadata.confidence ?? 100,
|
||||||
|
distilled_at: metadata.distilled_at,
|
||||||
|
bottled_at: metadata.bottled_at,
|
||||||
|
batch_info: metadata.batch_info,
|
||||||
})
|
})
|
||||||
.select()
|
.select()
|
||||||
.single();
|
.single();
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ export async function updateBottle(bottleId: string, data: {
|
|||||||
age?: number;
|
age?: number;
|
||||||
whiskybase_id?: string;
|
whiskybase_id?: string;
|
||||||
purchase_price?: number;
|
purchase_price?: number;
|
||||||
|
distilled_at?: string;
|
||||||
|
bottled_at?: string;
|
||||||
|
batch_info?: string;
|
||||||
}) {
|
}) {
|
||||||
const supabase = createServerActionClient({ cookies });
|
const supabase = createServerActionClient({ cookies });
|
||||||
|
|
||||||
@@ -29,6 +32,9 @@ export async function updateBottle(bottleId: string, data: {
|
|||||||
age: data.age,
|
age: data.age,
|
||||||
whiskybase_id: data.whiskybase_id,
|
whiskybase_id: data.whiskybase_id,
|
||||||
purchase_price: data.purchase_price,
|
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(),
|
updated_at: new Date().toISOString(),
|
||||||
})
|
})
|
||||||
.eq('id', bottleId)
|
.eq('id', bottleId)
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ export const BottleMetadataSchema = z.object({
|
|||||||
vintage: z.string().nullable(),
|
vintage: z.string().nullable(),
|
||||||
bottleCode: z.string().nullable(),
|
bottleCode: z.string().nullable(),
|
||||||
whiskybaseId: 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),
|
is_whisky: z.boolean().default(true),
|
||||||
confidence: z.number().min(0).max(100).default(100),
|
confidence: z.number().min(0).max(100).default(100),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -44,6 +44,9 @@ CREATE TABLE IF NOT EXISTS bottles (
|
|||||||
is_whisky BOOLEAN DEFAULT true,
|
is_whisky BOOLEAN DEFAULT true,
|
||||||
confidence INTEGER DEFAULT 100,
|
confidence INTEGER DEFAULT 100,
|
||||||
finished_at TIMESTAMP WITH TIME ZONE,
|
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()),
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('Europe/Berlin'::text, now()),
|
||||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('Europe/Berlin'::text, now())
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('Europe/Berlin'::text, now())
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user