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:
@@ -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 && (
|
||||
|
||||
Reference in New Issue
Block a user