feat: move offline indicator to header, rename to Offline-Modus, and redesign tasting form
This commit is contained in:
@@ -1,17 +1,17 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { WifiOff, ShieldCheck } from 'lucide-react';
|
||||
import { WifiOff, ShieldCheck, Loader2 } from 'lucide-react';
|
||||
|
||||
export default function OfflineIndicator() {
|
||||
const [isOffline, setIsOffline] = useState(false);
|
||||
const [isBunkerReady, setIsBunkerReady] = useState(false);
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
const [progress, setProgress] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
setIsOffline(!navigator.onLine);
|
||||
const savedReady = localStorage.getItem('whisky_bunker_ready') === 'true';
|
||||
setIsBunkerReady(savedReady);
|
||||
setIsReady(savedReady);
|
||||
|
||||
const handleOnline = () => setIsOffline(false);
|
||||
const handleOffline = () => setIsOffline(true);
|
||||
@@ -21,8 +21,7 @@ export default function OfflineIndicator() {
|
||||
setProgress(event.data.progress);
|
||||
}
|
||||
if (event.data?.type === 'PRECACHE_COMPLETE' || event.data?.type === 'BUNKER_STATUS') {
|
||||
console.log('🛡️ PWA: Bunker is ready!');
|
||||
setIsBunkerReady(true);
|
||||
setIsReady(true);
|
||||
localStorage.setItem('whisky_bunker_ready', 'true');
|
||||
}
|
||||
};
|
||||
@@ -32,8 +31,6 @@ export default function OfflineIndicator() {
|
||||
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.addEventListener('message', handleMessage);
|
||||
|
||||
// Initial check: if already active, ask for status
|
||||
if (navigator.serviceWorker.controller) {
|
||||
navigator.serviceWorker.controller.postMessage({ type: 'CHECK_BUNKER_STATUS' });
|
||||
}
|
||||
@@ -48,61 +45,35 @@ export default function OfflineIndicator() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 1. OFFLINE BAR (TOP)
|
||||
if (isOffline) {
|
||||
return (
|
||||
<div className="fixed top-0 left-0 w-full bg-red-600 text-white text-[11px] font-black uppercase tracking-[0.2em] py-2 flex items-center justify-center gap-2 z-[10001] shadow-xl animate-in slide-in-from-top duration-300">
|
||||
<WifiOff size={14} className="animate-pulse" />
|
||||
Offline-Modus: Bunker aktiv 🛡️
|
||||
<div className="flex items-center gap-1.5 px-2.5 py-1 bg-red-600/10 border border-red-600/20 rounded-full animate-pulse">
|
||||
<WifiOff size={10} className="text-red-600" />
|
||||
<span className="text-[9px] font-black uppercase tracking-widest text-red-600">Offline</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 2. READY STATUS (BOTTOM RIGHT)
|
||||
if (isBunkerReady) {
|
||||
if (isReady) {
|
||||
return (
|
||||
<div className="fixed bottom-24 right-4 z-[10000] animate-in fade-in slide-in-from-right-10 duration-700 pointer-events-auto">
|
||||
<div className="bg-zinc-900/95 backdrop-blur-xl border border-green-500/40 px-4 py-2.5 rounded-2xl flex items-center gap-2.5 shadow-[0_20px_50px_rgba(0,0,0,0.5),0_0_20px_rgba(34,197,94,0.1)] group hover:scale-105 transition-all cursor-help ring-1 ring-white/10">
|
||||
<div className="relative">
|
||||
<ShieldCheck size={18} className="text-green-500" />
|
||||
<div className="absolute -top-1 -right-1 w-2 h-2 bg-green-500 rounded-full blur-[2px] animate-pulse" />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[10px] font-black uppercase tracking-[0.2em] text-white">Bunker Aktiv</span>
|
||||
<span className="text-[8px] font-bold text-zinc-500 uppercase tracking-widest">Vollständig Offline fähig</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 px-2.5 py-1 bg-green-600/10 border border-green-600/20 rounded-full group cursor-help relative">
|
||||
<ShieldCheck size={10} className="text-green-600" />
|
||||
<span className="text-[9px] font-black uppercase tracking-widest text-green-600">Offline-Modus aktiv</span>
|
||||
|
||||
{/* Tooltip */}
|
||||
<div className="absolute bottom-full right-0 mb-3 w-56 p-3 bg-zinc-950 text-[10px] text-zinc-400 rounded-2xl border border-white/10 opacity-0 group-hover:opacity-100 transition-all pointer-events-none shadow-2xl translate-y-2 group-hover:translate-y-0">
|
||||
<p className="leading-relaxed">
|
||||
<strong className="text-white block mb-1">Status: Gesichert</strong>
|
||||
Alle wichtigen Bestandteile der App sind lokal gespeichert. Du kannst die App jederzeit im Funkloch nutzen.
|
||||
</p>
|
||||
</div>
|
||||
{/* Tooltip for desktop */}
|
||||
<div className="absolute top-full left-1/2 -translate-x-1/2 mt-2 w-48 p-2 bg-zinc-900 text-[10px] text-zinc-400 rounded-xl border border-white/10 opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none z-50 shadow-2xl text-center">
|
||||
Alle Funktionen sind vollständig offline verfügbar.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 3. LOADING STATUS (BOTTOM RIGHT)
|
||||
return (
|
||||
<div className="fixed bottom-24 right-4 z-[10000] animate-in fade-in slide-in-from-right-10 duration-500 pointer-events-auto">
|
||||
<div className="bg-zinc-900/95 backdrop-blur-xl border border-amber-500/40 px-4 py-2.5 rounded-2xl flex items-center gap-3 shadow-[0_20px_50px_rgba(0,0,0,0.5),0_0_20px_rgba(245,158,11,0.1)] ring-1 ring-white/10">
|
||||
<div className="relative w-5 h-5 flex items-center justify-center">
|
||||
<div className="absolute inset-0 border-2 border-amber-500/20 rounded-full" />
|
||||
<div
|
||||
className="absolute inset-0 border-2 border-amber-500 rounded-full border-t-transparent animate-spin"
|
||||
style={{ borderRightColor: 'transparent' }}
|
||||
/>
|
||||
<div className="w-1.5 h-1.5 bg-amber-500 rounded-full animate-pulse" />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[10px] font-black uppercase tracking-[0.2em] text-white">Bunker lädt...</span>
|
||||
<span className="text-[10px] font-black text-amber-500 tabular-nums">
|
||||
{progress > 0 ? `${progress}%` : 'Initialisiere'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 px-2.5 py-1 bg-amber-600/10 border border-amber-600/20 rounded-full">
|
||||
<Loader2 size={10} className="text-amber-600 animate-spin" />
|
||||
<span className="text-[9px] font-black uppercase tracking-widest text-amber-600">
|
||||
{progress > 0 ? `Lade Offline-Daten... ${progress}%` : 'Initialisiere...'}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -288,77 +288,122 @@ export default function TastingNoteForm({ bottleId, pendingBottleId, sessionId,
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="bg-zinc-50 dark:bg-zinc-800/50 p-4 rounded-2xl border border-zinc-200 dark:border-zinc-700/50 space-y-4">
|
||||
<TagSelector
|
||||
category="nose"
|
||||
selectedTagIds={noseTagIds}
|
||||
onToggleTag={toggleNoseTag}
|
||||
label={t('tasting.nose')}
|
||||
suggestedTagNames={suggestedTags}
|
||||
suggestedCustomTagNames={suggestedCustomTags}
|
||||
/>
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black text-zinc-400 uppercase tracking-widest block opacity-50">Zusätzliche Notizen</label>
|
||||
<textarea
|
||||
value={nose}
|
||||
onChange={(e) => setNose(e.target.value)}
|
||||
placeholder={t('tasting.notesPlaceholder')}
|
||||
rows={2}
|
||||
className="w-full p-3 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-700 rounded-xl text-sm focus:ring-2 focus:ring-amber-500 outline-none resize-none transition-all dark:text-zinc-200"
|
||||
<div className="space-y-6">
|
||||
{/* Nose Section */}
|
||||
<div className="bg-white dark:bg-zinc-900 rounded-3xl border border-zinc-200 dark:border-zinc-800 shadow-sm overflow-hidden transition-all">
|
||||
<div className="bg-zinc-50 dark:bg-zinc-800/50 px-5 py-4 border-b border-zinc-200 dark:border-zinc-800 flex items-center gap-3">
|
||||
<div className="bg-amber-100 dark:bg-amber-900/30 p-2 rounded-xl text-amber-600">
|
||||
<Wind size={18} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm font-black uppercase tracking-widest text-zinc-900 dark:text-white leading-none">
|
||||
{t('tasting.nose')}
|
||||
</h3>
|
||||
<p className="text-[10px] text-zinc-500 font-bold uppercase mt-1">Aroma & Eindruck</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-5 space-y-5">
|
||||
<TagSelector
|
||||
category="nose"
|
||||
selectedTagIds={noseTagIds}
|
||||
onToggleTag={toggleNoseTag}
|
||||
label=""
|
||||
suggestedTagNames={suggestedTags}
|
||||
suggestedCustomTagNames={suggestedCustomTags}
|
||||
/>
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black text-zinc-400 uppercase tracking-widest block px-1">Eigene Notizen</label>
|
||||
<textarea
|
||||
value={nose}
|
||||
onChange={(e) => setNose(e.target.value)}
|
||||
placeholder={t('tasting.notesPlaceholder')}
|
||||
rows={2}
|
||||
className="w-full p-4 bg-zinc-50 dark:bg-zinc-800 border-none rounded-2xl text-sm focus:ring-2 focus:ring-amber-500 outline-none resize-none transition-all dark:text-zinc-200 placeholder:text-zinc-400"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-zinc-50 dark:bg-zinc-800/50 p-4 rounded-2xl border border-zinc-200 dark:border-zinc-700/50 space-y-4">
|
||||
<TagSelector
|
||||
category="taste"
|
||||
selectedTagIds={palateTagIds}
|
||||
onToggleTag={togglePalateTag}
|
||||
label={t('tasting.palate')}
|
||||
suggestedTagNames={suggestedTags}
|
||||
suggestedCustomTagNames={suggestedCustomTags}
|
||||
/>
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black text-zinc-400 uppercase tracking-widest block opacity-50">Zusätzliche Notizen</label>
|
||||
<textarea
|
||||
value={palate}
|
||||
onChange={(e) => setPalate(e.target.value)}
|
||||
placeholder={t('tasting.notesPlaceholder')}
|
||||
rows={2}
|
||||
className="w-full p-3 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-700 rounded-xl text-sm focus:ring-2 focus:ring-amber-500 outline-none resize-none transition-all dark:text-zinc-200"
|
||||
{/* Palate Section */}
|
||||
<div className="bg-white dark:bg-zinc-900 rounded-3xl border border-zinc-200 dark:border-zinc-800 shadow-sm overflow-hidden transition-all">
|
||||
<div className="bg-zinc-50 dark:bg-zinc-800/50 px-5 py-4 border-b border-zinc-200 dark:border-zinc-800 flex items-center gap-3">
|
||||
<div className="bg-amber-100 dark:bg-amber-900/30 p-2 rounded-xl text-amber-600">
|
||||
<Utensils size={18} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm font-black uppercase tracking-widest text-zinc-900 dark:text-white leading-none">
|
||||
{t('tasting.palate')}
|
||||
</h3>
|
||||
<p className="text-[10px] text-zinc-500 font-bold uppercase mt-1">Geschmack & Textur</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-5 space-y-5">
|
||||
<TagSelector
|
||||
category="taste"
|
||||
selectedTagIds={palateTagIds}
|
||||
onToggleTag={togglePalateTag}
|
||||
label=""
|
||||
suggestedTagNames={suggestedTags}
|
||||
suggestedCustomTagNames={suggestedCustomTags}
|
||||
/>
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black text-zinc-400 uppercase tracking-widest block px-1">Eigene Notizen</label>
|
||||
<textarea
|
||||
value={palate}
|
||||
onChange={(e) => setPalate(e.target.value)}
|
||||
placeholder={t('tasting.notesPlaceholder')}
|
||||
rows={2}
|
||||
className="w-full p-4 bg-zinc-50 dark:bg-zinc-800 border-none rounded-2xl text-sm focus:ring-2 focus:ring-amber-500 outline-none resize-none transition-all dark:text-zinc-200 placeholder:text-zinc-400"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-zinc-50 dark:bg-zinc-800/50 p-4 rounded-2xl border border-zinc-200 dark:border-zinc-700/50 space-y-6">
|
||||
<TagSelector
|
||||
category="finish"
|
||||
selectedTagIds={finishTagIds}
|
||||
onToggleTag={toggleFinishTag}
|
||||
label={t('tasting.finish')}
|
||||
suggestedTagNames={suggestedTags}
|
||||
suggestedCustomTagNames={suggestedCustomTags}
|
||||
/>
|
||||
|
||||
<TagSelector
|
||||
category="texture"
|
||||
selectedTagIds={finishTagIds} // Using finish state for texture for now, or separate if needed
|
||||
onToggleTag={toggleFinishTag}
|
||||
label="Textur & Mundgefühl"
|
||||
suggestedTagNames={suggestedTags}
|
||||
suggestedCustomTagNames={suggestedCustomTags}
|
||||
/>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black text-zinc-400 uppercase tracking-widest block opacity-50">Zusätzliche Notizen</label>
|
||||
<textarea
|
||||
value={finish}
|
||||
onChange={(e) => setFinish(e.target.value)}
|
||||
placeholder={t('tasting.notesPlaceholder')}
|
||||
rows={2}
|
||||
className="w-full p-3 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-700 rounded-xl text-sm focus:ring-2 focus:ring-amber-500 outline-none resize-none transition-all dark:text-zinc-200"
|
||||
{/* Finish Section */}
|
||||
<div className="bg-white dark:bg-zinc-900 rounded-3xl border border-zinc-200 dark:border-zinc-800 shadow-sm overflow-hidden transition-all">
|
||||
<div className="bg-zinc-50 dark:bg-zinc-800/50 px-5 py-4 border-b border-zinc-200 dark:border-zinc-800 flex items-center gap-3">
|
||||
<div className="bg-amber-100 dark:bg-amber-900/30 p-2 rounded-xl text-amber-600">
|
||||
<Droplets size={18} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm font-black uppercase tracking-widest text-zinc-900 dark:text-white leading-none">
|
||||
{t('tasting.finish')}
|
||||
</h3>
|
||||
<p className="text-[10px] text-zinc-500 font-bold uppercase mt-1">Abgang & Nachhall</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-5 space-y-5">
|
||||
<TagSelector
|
||||
category="finish"
|
||||
selectedTagIds={finishTagIds}
|
||||
onToggleTag={toggleFinishTag}
|
||||
label=""
|
||||
suggestedTagNames={suggestedTags}
|
||||
suggestedCustomTagNames={suggestedCustomTags}
|
||||
/>
|
||||
|
||||
<div className="space-y-2 pt-2 border-t border-zinc-100 dark:border-zinc-800">
|
||||
<label className="text-[10px] font-black text-zinc-400 uppercase tracking-widest block px-1 opacity-50">Gefühl & Textur</label>
|
||||
<TagSelector
|
||||
category="texture"
|
||||
selectedTagIds={finishTagIds}
|
||||
onToggleTag={toggleFinishTag}
|
||||
label=""
|
||||
suggestedTagNames={suggestedTags}
|
||||
suggestedCustomTagNames={suggestedCustomTags}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black text-zinc-400 uppercase tracking-widest block px-1">Eigene Notizen</label>
|
||||
<textarea
|
||||
value={finish}
|
||||
onChange={(e) => setFinish(e.target.value)}
|
||||
placeholder={t('tasting.notesPlaceholder')}
|
||||
rows={2}
|
||||
className="w-full p-4 bg-zinc-50 dark:bg-zinc-800 border-none rounded-2xl text-sm focus:ring-2 focus:ring-amber-500 outline-none resize-none transition-all dark:text-zinc-200 placeholder:text-zinc-400"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user