feat: implement offline queue, background sync and AI robustness

This commit is contained in:
2025-12-17 23:25:12 +01:00
parent fe82d52a85
commit 6f08bb3c4c
70 changed files with 3132 additions and 55 deletions

View File

@@ -6,6 +6,8 @@ import { createClientComponentClient } from '@supabase/auth-helpers-nextjs';
import { analyzeBottle } from '@/services/analyze-bottle';
import { saveBottle } from '@/services/save-bottle';
import { BottleMetadata } from '@/types/whisky';
import { savePendingBottle } from '@/lib/offline-db';
import { v4 as uuidv4 } from 'uuid';
interface CameraCaptureProps {
onImageCaptured?: (base64Image: string) => void;
@@ -21,6 +23,7 @@ export default function CameraCapture({ onImageCaptured, onAnalysisComplete, onS
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [analysisResult, setAnalysisResult] = useState<BottleMetadata | null>(null);
const [isQueued, setIsQueued] = useState(false);
const handleCapture = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
@@ -29,6 +32,7 @@ export default function CameraCapture({ onImageCaptured, onAnalysisComplete, onS
setIsProcessing(true);
setError(null);
setAnalysisResult(null);
setIsQueued(false);
try {
const compressedBase64 = await compressImage(file);
@@ -38,6 +42,18 @@ export default function CameraCapture({ onImageCaptured, onAnalysisComplete, onS
onImageCaptured(compressedBase64);
}
// Check if Offline
if (!navigator.onLine) {
console.log('Offline detected. Queuing image...');
await savePendingBottle({
id: uuidv4(),
imageBase64: compressedBase64,
timestamp: Date.now(),
});
setIsQueued(true);
return;
}
const response = await analyzeBottle(compressedBase64);
if (response.success && response.data) {
@@ -162,7 +178,7 @@ export default function CameraCapture({ onImageCaptured, onAnalysisComplete, onS
/>
<button
onClick={previewUrl && analysisResult ? handleSave : triggerUpload}
onClick={isQueued ? () => setPreviewUrl(null) : (previewUrl && analysisResult ? handleSave : triggerUpload)}
disabled={isProcessing || isSaving}
className="w-full py-4 px-6 bg-amber-600 hover:bg-amber-700 text-white rounded-xl font-semibold flex items-center justify-center gap-2 transition-all active:scale-[0.98] shadow-lg shadow-amber-600/20 disabled:opacity-50"
>
@@ -171,6 +187,11 @@ export default function CameraCapture({ onImageCaptured, onAnalysisComplete, onS
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white"></div>
Wird gespeichert...
</>
) : isQueued ? (
<>
<CheckCircle2 size={20} />
Nächste Flasche
</>
) : previewUrl && analysisResult ? (
<>
<CheckCircle2 size={20} />
@@ -196,7 +217,14 @@ export default function CameraCapture({ onImageCaptured, onAnalysisComplete, onS
</div>
)}
{previewUrl && !isProcessing && !error && (
{isQueued && (
<div className="flex items-center gap-2 text-purple-500 text-sm bg-purple-50 dark:bg-purple-900/10 p-4 rounded-xl w-full border border-purple-100 dark:border-purple-800/30 font-medium">
<Sparkles size={16} />
Offline! Foto wurde gemerkt wird automatisch analysiert, sobald du wieder Netz hast. 📡
</div>
)}
{previewUrl && !isProcessing && !error && !isQueued && (
<div className="flex flex-col gap-3 w-full animate-in fade-in slide-in-from-top-4 duration-500">
<div className="flex items-center gap-2 text-green-500 text-sm bg-green-50 dark:bg-green-900/10 p-3 rounded-lg w-full">
<CheckCircle2 size={16} />