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,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"
|
||||
|
||||
Reference in New Issue
Block a user