feat: implement robust offline-first sync with Dexie.js
- Migrated to Dexie.js for IndexedDB management - Added optimistic UI for tasting notes with 'Wartet auf Sync' badge - Implemented background caching for tags and buddies - Unified scanning and tasting sync in a global UploadQueue
This commit is contained in:
@@ -6,6 +6,8 @@ import Link from 'next/link';
|
||||
import AvatarStack from './AvatarStack';
|
||||
import { useI18n } from '@/i18n/I18nContext';
|
||||
import { deleteTasting } from '@/services/delete-tasting';
|
||||
import { useLiveQuery } from 'dexie-react-hooks';
|
||||
import { db } from '@/lib/db';
|
||||
|
||||
interface Tasting {
|
||||
id: string;
|
||||
@@ -40,14 +42,16 @@ interface Tasting {
|
||||
interface TastingListProps {
|
||||
initialTastings: Tasting[];
|
||||
currentUserId?: string;
|
||||
bottleId?: string;
|
||||
}
|
||||
|
||||
export default function TastingList({ initialTastings, currentUserId }: TastingListProps) {
|
||||
export default function TastingList({ initialTastings, currentUserId, bottleId }: TastingListProps) {
|
||||
const { t } = useI18n();
|
||||
const [sortBy, setSortBy] = useState<'date-desc' | 'date-asc' | 'rating-desc' | 'rating-asc'>('date-desc');
|
||||
const [isDeleting, setIsDeleting] = useState<string | null>(null);
|
||||
|
||||
const handleDelete = async (tastingId: string, bottleId: string) => {
|
||||
if (tastingId.startsWith('pending-')) return;
|
||||
if (!confirm('Bist du sicher, dass du diese Notiz löschen möchtest?')) return;
|
||||
|
||||
setIsDeleting(tastingId);
|
||||
@@ -63,23 +67,43 @@ export default function TastingList({ initialTastings, currentUserId }: TastingL
|
||||
}
|
||||
};
|
||||
|
||||
const pendingTastings = useLiveQuery(
|
||||
() => bottleId
|
||||
? db.pending_tastings.where('bottle_id').equals(bottleId).toArray()
|
||||
: db.pending_tastings.toArray(),
|
||||
[bottleId],
|
||||
[]
|
||||
);
|
||||
|
||||
const sortedTastings = useMemo(() => {
|
||||
const result = [...initialTastings];
|
||||
return result.sort((a, b) => {
|
||||
const merged = [
|
||||
...initialTastings,
|
||||
...(pendingTastings || []).map(p => ({
|
||||
id: `pending-${p.id}`,
|
||||
rating: p.data.rating,
|
||||
nose_notes: p.data.nose_notes,
|
||||
palate_notes: p.data.palate_notes,
|
||||
finish_notes: p.data.finish_notes,
|
||||
is_sample: p.data.is_sample,
|
||||
bottle_id: p.bottle_id,
|
||||
created_at: p.tasted_at,
|
||||
user_id: currentUserId || '',
|
||||
isPending: true
|
||||
}))
|
||||
];
|
||||
|
||||
return merged.sort((a, b) => {
|
||||
const timeA = new Date(a.created_at).getTime();
|
||||
const timeB = new Date(b.created_at).getTime();
|
||||
switch (sortBy) {
|
||||
case 'date-desc':
|
||||
return new Date(b.created_at).getTime() - new Date(a.created_at).getTime();
|
||||
case 'date-asc':
|
||||
return new Date(a.created_at).getTime() - new Date(b.created_at).getTime();
|
||||
case 'rating-desc':
|
||||
return b.rating - a.rating;
|
||||
case 'rating-asc':
|
||||
return a.rating - b.rating;
|
||||
default:
|
||||
return 0;
|
||||
case 'date-desc': return timeB - timeA;
|
||||
case 'date-asc': return timeA - timeB;
|
||||
case 'rating-desc': return b.rating - a.rating;
|
||||
case 'rating-asc': return a.rating - b.rating;
|
||||
default: return 0;
|
||||
}
|
||||
});
|
||||
}, [initialTastings, sortBy]);
|
||||
}, [initialTastings, pendingTastings, sortBy, currentUserId]);
|
||||
|
||||
if (!initialTastings || initialTastings.length === 0) {
|
||||
return (
|
||||
@@ -125,6 +149,12 @@ export default function TastingList({ initialTastings, currentUserId }: TastingL
|
||||
}`}>
|
||||
{note.is_sample ? 'Sample' : 'Bottle'}
|
||||
</span>
|
||||
{(note as any).isPending && (
|
||||
<div className="bg-amber-100 dark:bg-amber-900/30 text-amber-600 dark:text-amber-400 px-2 py-0.5 rounded-lg text-[10px] font-black uppercase tracking-tighter flex items-center gap-1.5 animate-pulse">
|
||||
<Clock size={10} />
|
||||
Wartet auf Sync...
|
||||
</div>
|
||||
)}
|
||||
<div className="text-[10px] text-zinc-500 font-bold bg-zinc-100 dark:bg-zinc-800 px-2 py-1 rounded-lg flex items-center gap-1">
|
||||
<Clock size={10} />
|
||||
{new Date(note.created_at).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })}
|
||||
@@ -144,7 +174,7 @@ export default function TastingList({ initialTastings, currentUserId }: TastingL
|
||||
<Calendar size={12} />
|
||||
{new Date(note.created_at).toLocaleDateString('de-DE')}
|
||||
</div>
|
||||
{(!currentUserId || note.user_id === currentUserId) && (
|
||||
{(!currentUserId || note.user_id === currentUserId) && !(note as any).isPending && (
|
||||
<button
|
||||
onClick={() => handleDelete(note.id, note.bottle_id)}
|
||||
disabled={!!isDeleting}
|
||||
|
||||
Reference in New Issue
Block a user