feat: implement automated Whiskybase ID discovery
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { Camera, Upload, CheckCircle2, AlertCircle, Sparkles, ExternalLink, ChevronRight } from 'lucide-react';
|
||||
import { Camera, Upload, CheckCircle2, AlertCircle, Sparkles, ExternalLink, ChevronRight, Search, Loader2 } from 'lucide-react';
|
||||
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { analyzeBottle } from '@/services/analyze-bottle';
|
||||
@@ -11,6 +11,8 @@ import { savePendingBottle } from '@/lib/offline-db';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { findMatchingBottle } from '@/services/find-matching-bottle';
|
||||
import { validateSession } from '@/services/validate-session';
|
||||
import { discoverWhiskybaseId } from '@/services/discover-whiskybase';
|
||||
import { updateBottle } from '@/services/update-bottle';
|
||||
import Link from 'next/link';
|
||||
|
||||
interface CameraCaptureProps {
|
||||
@@ -47,6 +49,8 @@ export default function CameraCapture({ onImageCaptured, onAnalysisComplete, onS
|
||||
const [isQueued, setIsQueued] = useState(false);
|
||||
const [matchingBottle, setMatchingBottle] = useState<{ id: string; name: string } | null>(null);
|
||||
const [lastSavedId, setLastSavedId] = useState<string | null>(null);
|
||||
const [wbDiscovery, setWbDiscovery] = useState<{ id: string; url: string; title: string } | null>(null);
|
||||
const [isDiscovering, setIsDiscovering] = useState(false);
|
||||
|
||||
const handleCapture = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
@@ -153,6 +157,33 @@ export default function CameraCapture({ onImageCaptured, onAnalysisComplete, onS
|
||||
}
|
||||
};
|
||||
|
||||
const handleDiscoverWb = async () => {
|
||||
if (!lastSavedId || !analysisResult) return;
|
||||
setIsDiscovering(true);
|
||||
const result = await discoverWhiskybaseId({
|
||||
name: analysisResult.name || '',
|
||||
distillery: analysisResult.distillery || undefined,
|
||||
abv: analysisResult.abv || undefined,
|
||||
age: analysisResult.age || undefined
|
||||
});
|
||||
|
||||
if (result.success && result.id) {
|
||||
setWbDiscovery({ id: result.id, url: result.url!, title: result.title! });
|
||||
}
|
||||
setIsDiscovering(false);
|
||||
};
|
||||
|
||||
const handleLinkWb = async () => {
|
||||
if (!lastSavedId || !wbDiscovery) return;
|
||||
const res = await updateBottle(lastSavedId, {
|
||||
whiskybase_id: wbDiscovery.id
|
||||
});
|
||||
if (res.success) {
|
||||
setWbDiscovery(null);
|
||||
// Show some success feedback if needed, but the button will disappear anyway
|
||||
}
|
||||
};
|
||||
|
||||
const compressImage = (file: File): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
@@ -245,6 +276,50 @@ export default function CameraCapture({ onImageCaptured, onAnalysisComplete, onS
|
||||
<ChevronRight size={20} />
|
||||
</button>
|
||||
|
||||
{!wbDiscovery && !isDiscovering && (
|
||||
<button
|
||||
onClick={handleDiscoverWb}
|
||||
className="w-full py-3 px-6 bg-amber-50 dark:bg-amber-900/20 text-amber-600 rounded-xl font-bold flex items-center justify-center gap-2 border border-amber-200 dark:border-amber-800/50 hover:bg-amber-100 transition-all text-sm"
|
||||
>
|
||||
<Search size={16} />
|
||||
Whiskybase-Link suchen
|
||||
</button>
|
||||
)}
|
||||
|
||||
{isDiscovering && (
|
||||
<div className="w-full py-3 px-6 text-zinc-400 font-bold flex items-center justify-center gap-2 text-sm italic">
|
||||
<Loader2 size={16} className="animate-spin" />
|
||||
Suche auf Whiskybase...
|
||||
</div>
|
||||
)}
|
||||
|
||||
{wbDiscovery && (
|
||||
<div className="p-4 bg-zinc-50 dark:bg-zinc-800/50 border border-amber-500/30 rounded-2xl space-y-3 animate-in fade-in slide-in-from-top-2">
|
||||
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-amber-600">
|
||||
<Sparkles size={12} /> Treffer gefunden
|
||||
</div>
|
||||
<p className="text-xs font-bold text-zinc-800 dark:text-zinc-200 line-clamp-2 leading-snug">
|
||||
{wbDiscovery.title}
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={handleLinkWb}
|
||||
className="flex-1 py-2.5 bg-amber-600 text-white text-[10px] font-black uppercase rounded-lg hover:bg-amber-700 transition-colors"
|
||||
>
|
||||
Verknüpfen
|
||||
</button>
|
||||
<a
|
||||
href={wbDiscovery.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex-1 py-2.5 bg-zinc-200 dark:bg-zinc-700 text-zinc-600 dark:text-zinc-300 text-[10px] font-black uppercase rounded-lg hover:bg-zinc-300 transition-colors flex items-center justify-center gap-1"
|
||||
>
|
||||
<ExternalLink size={12} /> Prüfen
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
setPreviewUrl(null);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { Edit2, Save, X, Info, Tag, FlaskConical, CircleDollarSign } from 'lucide-react';
|
||||
import { Edit2, Save, X, Info, Tag, FlaskConical, CircleDollarSign, Search, Loader2, ExternalLink } from 'lucide-react';
|
||||
import { updateBottle } from '@/services/update-bottle';
|
||||
import { discoverWhiskybaseId } from '@/services/discover-whiskybase';
|
||||
|
||||
interface EditBottleFormProps {
|
||||
bottle: {
|
||||
@@ -21,7 +22,9 @@ interface EditBottleFormProps {
|
||||
export default function EditBottleForm({ bottle, onComplete }: EditBottleFormProps) {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [discoveryResult, setDiscoveryResult] = useState<{ id: string; url: string; title: string } | null>(null);
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
name: bottle.name,
|
||||
@@ -33,6 +36,33 @@ export default function EditBottleForm({ bottle, onComplete }: EditBottleFormPro
|
||||
purchase_price: bottle.purchase_price || '',
|
||||
});
|
||||
|
||||
const handleDiscover = async () => {
|
||||
setIsSearching(true);
|
||||
setError(null);
|
||||
setDiscoveryResult(null);
|
||||
|
||||
const result = await discoverWhiskybaseId({
|
||||
name: formData.name,
|
||||
distillery: formData.distillery,
|
||||
abv: formData.abv,
|
||||
age: formData.age
|
||||
});
|
||||
|
||||
if (result.success && result.id) {
|
||||
setDiscoveryResult({ id: result.id!, url: result.url!, title: result.title! });
|
||||
} else {
|
||||
setError(result.error || 'Keinen Treffer gefunden.');
|
||||
}
|
||||
setIsSearching(false);
|
||||
};
|
||||
|
||||
const applyDiscovery = () => {
|
||||
if (discoveryResult) {
|
||||
setFormData({ ...formData, whiskybase_id: discoveryResult.id });
|
||||
setDiscoveryResult(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
setIsSaving(true);
|
||||
setError(null);
|
||||
@@ -142,13 +172,47 @@ export default function EditBottleForm({ bottle, onComplete }: EditBottleFormPro
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<label className="text-[10px] font-black uppercase text-zinc-400 ml-1">Whiskybase ID</label>
|
||||
<label className="text-[10px] font-black uppercase text-zinc-400 ml-1 flex justify-between items-center">
|
||||
<span>Whiskybase ID</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDiscover}
|
||||
disabled={isSearching}
|
||||
className="text-amber-600 hover:text-amber-700 flex items-center gap-1 normal-case font-bold"
|
||||
>
|
||||
{isSearching ? <Loader2 size={10} className="animate-spin" /> : <Search size={10} />}
|
||||
Automatisch suchen
|
||||
</button>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.whiskybase_id}
|
||||
onChange={(e) => setFormData({ ...formData, whiskybase_id: 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"
|
||||
/>
|
||||
{discoveryResult && (
|
||||
<div className="mt-2 p-3 bg-zinc-50 dark:bg-zinc-800/50 border border-amber-500/20 rounded-xl animate-in fade-in slide-in-from-top-2">
|
||||
<p className="text-[10px] text-zinc-500 mb-2">Treffer gefunden:</p>
|
||||
<p className="text-[11px] font-bold text-zinc-800 dark:text-zinc-200 mb-2 truncate">{discoveryResult.title}</p>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={applyDiscovery}
|
||||
className="px-3 py-1.5 bg-amber-600 text-white text-[10px] font-black uppercase rounded-lg hover:bg-amber-700 transition-colors"
|
||||
>
|
||||
ID Übernehmen
|
||||
</button>
|
||||
<a
|
||||
href={discoveryResult.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="px-3 py-1.5 bg-zinc-200 dark:bg-zinc-700 text-zinc-600 dark:text-zinc-300 text-[10px] font-black uppercase rounded-lg hover:bg-zinc-300 dark:hover:bg-zinc-600 transition-colors flex items-center gap-1"
|
||||
>
|
||||
<ExternalLink size={10} /> Prüfen
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<label className="text-[10px] font-black uppercase text-amber-600 ml-1 flex items-center gap-1">
|
||||
|
||||
Reference in New Issue
Block a user