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,7 +2,7 @@
import React, { useState, useMemo } from 'react';
import Link from 'next/link';
import { Search, Filter, X, Calendar, Clock, Package, Lock, Unlock, Ghost, FlaskConical, AlertCircle, Trash2, AlertTriangle, PlusCircle } from 'lucide-react';
import { Search, Filter, X, Calendar, Clock, Package, FlaskConical, AlertCircle, Trash2, AlertTriangle, PlusCircle } from 'lucide-react';
import { getStorageUrl } from '@/lib/supabase';
import { useSearchParams } from 'next/navigation';
import { validateSession } from '@/services/validate-session';
@@ -19,7 +19,6 @@ interface Bottle {
age: number;
image_url: string;
purchase_price?: number | null;
status: 'sealed' | 'open' | 'sampled' | 'empty';
created_at: string;
last_tasted?: string | null;
is_whisky?: boolean;
@@ -33,15 +32,6 @@ interface BottleCardProps {
function BottleCard({ bottle, sessionId }: BottleCardProps) {
const { t, locale } = useI18n();
const statusConfig = {
open: { icon: Unlock, color: 'bg-amber-500/80 border-amber-400/50', label: t('bottle.status.open') },
sampled: { icon: FlaskConical, color: 'bg-purple-500/80 border-purple-400/50', label: t('bottle.status.sampled') },
empty: { icon: Ghost, color: 'bg-zinc-500/80 border-zinc-400/50', label: t('bottle.status.empty') },
sealed: { icon: Lock, color: 'bg-blue-600/80 border-blue-400/50', label: t('bottle.status.sealed') },
};
const StatusIcon = statusConfig[bottle.status as keyof typeof statusConfig]?.icon || Lock;
const statusStyle = statusConfig[bottle.status as keyof typeof statusConfig] || statusConfig.sealed;
return (
<Link
@@ -71,10 +61,6 @@ function BottleCard({ bottle, sessionId }: BottleCardProps) {
</div>
)}
<div className={`absolute bottom-3 left-3 px-3 py-1.5 rounded-xl text-[10px] font-black uppercase flex items-center gap-2 backdrop-blur-md border shadow-lg ${statusStyle.color}`}>
<StatusIcon size={12} />
{statusStyle.label}
</div>
</div>
<div className="p-3 md:p-5 space-y-3 md:space-y-4">
@@ -144,7 +130,6 @@ export default function BottleGrid({ bottles }: BottleGridProps) {
const [searchQuery, setSearchQuery] = useState('');
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
const [selectedDistillery, setSelectedDistillery] = useState<string | null>(null);
const [selectedStatus, setSelectedStatus] = useState<string | null>(null);
const [sortBy, setSortBy] = useState<'name' | 'last_tasted' | 'created_at'>('created_at');
const categories = useMemo(() => {
@@ -174,9 +159,8 @@ export default function BottleGrid({ bottles }: BottleGridProps) {
const matchesCategory = !selectedCategory || bottle.category === selectedCategory;
const matchesDistillery = !selectedDistillery || bottle.distillery === selectedDistillery;
const matchesStatus = !selectedStatus || bottle.status === selectedStatus;
return matchesSearch && matchesCategory && matchesDistillery && matchesStatus;
return matchesSearch && matchesCategory && matchesDistillery;
});
// Sorting logic
@@ -191,7 +175,7 @@ export default function BottleGrid({ bottles }: BottleGridProps) {
return new Date(b.created_at).getTime() - new Date(a.created_at).getTime();
}
});
}, [bottles, searchQuery, selectedCategory, selectedDistillery, selectedStatus, sortBy]);
}, [bottles, searchQuery, selectedCategory, selectedDistillery, sortBy]);
const [isFiltersOpen, setIsFiltersOpen] = useState(false);
@@ -203,7 +187,7 @@ export default function BottleGrid({ bottles }: BottleGridProps) {
);
}
const activeFiltersCount = (selectedCategory ? 1 : 0) + (selectedDistillery ? 1 : 0) + (selectedStatus ? 1 : 0);
const activeFiltersCount = (selectedCategory ? 1 : 0) + (selectedDistillery ? 1 : 0);
return (
<div className="w-full space-y-8">
@@ -314,23 +298,6 @@ export default function BottleGrid({ bottles }: BottleGridProps) {
</div>
</div>
<div className="space-y-3">
<label className="text-[10px] font-black uppercase tracking-[0.2em] text-zinc-400 px-1">{t('grid.filter.status')}</label>
<div className="flex flex-wrap gap-2">
{['sealed', 'open', 'sampled', 'empty'].map((status) => (
<button
key={status}
onClick={() => setSelectedStatus(selectedStatus === status ? null : status)}
className={`px-3 py-1.5 rounded-xl text-[10px] font-black transition-all border ${selectedStatus === status
? status === 'open' ? 'bg-amber-500 border-amber-500 text-white' : status === 'sampled' ? 'bg-purple-500 border-purple-500 text-white' : status === 'empty' ? 'bg-zinc-500 border-zinc-500 text-white' : 'bg-blue-600 border-blue-600 text-white'
: 'bg-zinc-50 dark:bg-zinc-800 border-zinc-200 dark:border-zinc-700 text-zinc-500'
}`}
>
{t(`bottle.status.${status}`).toUpperCase()}
</button>
))}
</div>
</div>
</div>
<div className="pt-4 border-t border-zinc-100 dark:border-zinc-800 flex justify-between items-center">
@@ -338,7 +305,6 @@ export default function BottleGrid({ bottles }: BottleGridProps) {
onClick={() => {
setSelectedCategory(null);
setSelectedDistillery(null);
setSelectedStatus(null);
setSearchQuery('');
}}
className="text-[10px] font-black text-red-500 uppercase tracking-widest hover:underline"