feat: implement advanced tagging system, tag weighting, and app focus refactoring

- Implemented reusable TagSelector component with i18n support
- Added tag weighting system (popularity scores 1-5)
- Created admin panel for tag management
- Integrated Nebius AI and Brave Search for 'Magic Scan'
- Refactored app focus: removed bottle status, updated counters, and displayed extended bottle details
- Updated i18n for German and English
- Added database migration scripts
This commit is contained in:
2025-12-19 12:58:44 +01:00
parent 9eb9b41061
commit b2a1d292da
30 changed files with 2420 additions and 194 deletions

View File

@@ -2,10 +2,11 @@
import React, { useState, useEffect } from 'react';
import { saveTasting } from '@/services/save-tasting';
import { Loader2, Send, Star, Users, Check, Sparkles, Droplets } from 'lucide-react';
import { Loader2, Send, Star, Users, Check, Sparkles, Droplets, Wind, Utensils, Zap } from 'lucide-react';
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs';
import { useI18n } from '@/i18n/I18nContext';
import { useSession } from '@/context/SessionContext';
import TagSelector from './TagSelector';
interface Buddy {
id: string;
@@ -29,6 +30,9 @@ export default function TastingNoteForm({ bottleId, sessionId }: TastingNoteForm
const [error, setError] = useState<string | null>(null);
const [buddies, setBuddies] = useState<Buddy[]>([]);
const [selectedBuddyIds, setSelectedBuddyIds] = useState<string[]>([]);
const [noseTagIds, setNoseTagIds] = useState<string[]>([]);
const [palateTagIds, setPalateTagIds] = useState<string[]>([]);
const [finishTagIds, setFinishTagIds] = useState<string[]>([]);
const { activeSession } = useSession();
const effectiveSessionId = sessionId || activeSession?.id;
@@ -62,6 +66,18 @@ export default function TastingNoteForm({ bottleId, sessionId }: TastingNoteForm
);
};
const toggleNoseTag = (id: string) => {
setNoseTagIds(prev => prev.includes(id) ? prev.filter(tid => tid !== id) : [...prev, id]);
};
const togglePalateTag = (id: string) => {
setPalateTagIds(prev => prev.includes(id) ? prev.filter(tid => tid !== id) : [...prev, id]);
};
const toggleFinishTag = (id: string) => {
setFinishTagIds(prev => prev.includes(id) ? prev.filter(tid => tid !== id) : [...prev, id]);
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
@@ -77,6 +93,7 @@ export default function TastingNoteForm({ bottleId, sessionId }: TastingNoteForm
finish_notes: finish,
is_sample: isSample,
buddy_ids: selectedBuddyIds,
tag_ids: [...noseTagIds, ...palateTagIds, ...finishTagIds],
});
if (result.success) {
@@ -84,6 +101,9 @@ export default function TastingNoteForm({ bottleId, sessionId }: TastingNoteForm
setPalate('');
setFinish('');
setSelectedBuddyIds([]);
setNoseTagIds([]);
setPalateTagIds([]);
setFinishTagIds([]);
// We don't need to manually refresh because of revalidatePath in the server action
} else {
setError(result.error || t('common.error'));
@@ -158,37 +178,71 @@ export default function TastingNoteForm({ bottleId, sessionId }: TastingNoteForm
</div>
</div>
<div className="space-y-2">
<label className="text-xs font-bold text-zinc-400 uppercase tracking-tighter">{t('tasting.nose')}</label>
<textarea
value={nose}
onChange={(e) => setNose(e.target.value)}
placeholder={t('tasting.notesPlaceholder')}
rows={2}
className="w-full p-3 bg-zinc-50 dark:bg-zinc-800 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>
<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')}
/>
<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>
</div>
<div className="space-y-2">
<label className="text-xs font-bold text-zinc-400 uppercase tracking-tighter">{t('tasting.palate')}</label>
<textarea
value={palate}
onChange={(e) => setPalate(e.target.value)}
placeholder={t('tasting.notesPlaceholder')}
rows={2}
className="w-full p-3 bg-zinc-50 dark:bg-zinc-800 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>
<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')}
/>
<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"
/>
</div>
</div>
<div className="space-y-2">
<label className="text-xs font-bold text-zinc-400 uppercase tracking-tighter">{t('tasting.finish')}</label>
<textarea
value={finish}
onChange={(e) => setFinish(e.target.value)}
placeholder={t('tasting.notesPlaceholder')}
rows={2}
className="w-full p-3 bg-zinc-50 dark:bg-zinc-800 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="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')}
/>
<TagSelector
category="texture"
selectedTagIds={finishTagIds} // Using finish state for texture for now, or separate if needed
onToggleTag={toggleFinishTag}
label="Textur & Mundgefühl"
/>
<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"
/>
</div>
</div>
</div>
{buddies.length > 0 && (