From 7f600698e4cd221ed6f777ceef53f6e9a17cb1a9 Mon Sep 17 00:00:00 2001 From: robin Date: Thu, 18 Dec 2025 16:46:39 +0100 Subject: [PATCH] feat: modernize search filters & intelligent label shortening - Introduced shortenCategory utility to strip redundant terms from labels - Refactored BottleGrid filters into a compact, collapsible layout - Added filter count indicator and improved chip styling - Fully localized new filter UI elements --- src/components/BottleGrid.tsx | 217 ++++++++++++++++++------------- src/components/CameraCapture.tsx | 3 +- src/i18n/de.ts | 3 + src/i18n/en.ts | 3 + src/i18n/types.ts | 3 + src/lib/format.ts | 33 +++++ 6 files changed, 174 insertions(+), 88 deletions(-) create mode 100644 src/lib/format.ts diff --git a/src/components/BottleGrid.tsx b/src/components/BottleGrid.tsx index b7561f1..c0ade91 100644 --- a/src/components/BottleGrid.tsx +++ b/src/components/BottleGrid.tsx @@ -7,6 +7,7 @@ import { getStorageUrl } from '@/lib/supabase'; import { useSearchParams } from 'next/navigation'; import { validateSession } from '@/services/validate-session'; import { useI18n } from '@/i18n/I18nContext'; +import { shortenCategory } from '@/lib/format'; interface Bottle { id: string; @@ -94,7 +95,7 @@ function BottleCard({ bottle, sessionId }: BottleCardProps) {
- {bottle.category} + {shortenCategory(bottle.category)} {bottle.abv}% VOL @@ -188,6 +189,8 @@ export default function BottleGrid({ bottles }: BottleGridProps) { }); }, [bottles, searchQuery, selectedCategory, selectedDistillery, selectedStatus, sortBy]); + const [isFiltersOpen, setIsFiltersOpen] = useState(false); + if (!bottles || bottles.length === 0) { return (
@@ -196,117 +199,157 @@ export default function BottleGrid({ bottles }: BottleGridProps) { ); } + const activeFiltersCount = (selectedCategory ? 1 : 0) + (selectedDistillery ? 1 : 0) + (selectedStatus ? 1 : 0); + return (
{/* Search and Filters */} -
-
-
- +
+
+
+ setSearchQuery(e.target.value)} - className="w-full pl-10 pr-4 py-3 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-xl focus:ring-2 focus:ring-amber-500 outline-none transition-all" + className="w-full pl-12 pr-12 py-3.5 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-[1.25rem] focus:ring-2 focus:ring-amber-500/20 focus:border-amber-500/50 outline-none transition-all shadow-sm" /> {searchQuery && ( )}
- setSortBy(e.target.value as any)} + className="flex-1 md:flex-none px-4 py-3.5 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-[1.25rem] text-sm font-bold focus:ring-2 focus:ring-amber-500/20 outline-none cursor-pointer appearance-none text-zinc-700 dark:text-zinc-300 shadow-sm" + > + + + + + + +
+
+ + {/* Category Quick Filter (Always visible row) */} +
+ + {categories.map((cat) => ( + + ))}
-
- {/* Category Filter */} -
- {t('grid.filter.category')} -
- - {categories.map((cat) => ( - - ))} -
-
+ {/* Collapsible Advanced Filters */} + {isFiltersOpen && ( +
+
+
+ +
+ + {distilleries.map((dist) => ( + + ))} +
+
- {/* Distillery Filter */} -
- {t('grid.filter.distillery')} -
- - {distilleries.map((dist) => ( - - ))} +
+ +
+ {['sealed', 'open', 'sampled', 'empty'].map((status) => ( + + ))} +
+
-
- {/* Status Filter */} -
- {t('grid.filter.status')} -
- {['sealed', 'open', 'sampled', 'empty'].map((status) => ( - - ))} +
+ +
-
+ )}
{/* Grid */} diff --git a/src/components/CameraCapture.tsx b/src/components/CameraCapture.tsx index b44b22d..9c347bf 100644 --- a/src/components/CameraCapture.tsx +++ b/src/components/CameraCapture.tsx @@ -15,6 +15,7 @@ import { discoverWhiskybaseId } from '@/services/discover-whiskybase'; import { updateBottle } from '@/services/update-bottle'; import Link from 'next/link'; import { useI18n } from '@/i18n/I18nContext'; +import { shortenCategory } from '@/lib/format'; interface CameraCaptureProps { onImageCaptured?: (base64Image: string) => void; @@ -457,7 +458,7 @@ export default function CameraCapture({ onImageCaptured, onAnalysisComplete, onS
{t('bottle.categoryLabel')}: - {analysisResult.category || '-'} + {shortenCategory(analysisResult.category || '-')}
{t('bottle.abvLabel')}: diff --git a/src/i18n/de.ts b/src/i18n/de.ts index ce825af..1622391 100644 --- a/src/i18n/de.ts +++ b/src/i18n/de.ts @@ -58,6 +58,9 @@ export const de: TranslationKeys = { addedOn: 'Hinzugefügt am', reviewRequired: 'REVIEW', unknownBottle: 'Unbekannte Flasche', + filters: 'Filter', + resetAll: 'Alle Filter zurücksetzen', + close: 'Schließen', }, bottle: { details: 'Details', diff --git a/src/i18n/en.ts b/src/i18n/en.ts index 6d4ba22..eeb6af5 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -58,6 +58,9 @@ export const en: TranslationKeys = { addedOn: 'Added on', reviewRequired: 'REVIEW', unknownBottle: 'Unknown Bottle', + filters: 'Filters', + resetAll: 'Reset all filters', + close: 'Close', }, bottle: { details: 'Details', diff --git a/src/i18n/types.ts b/src/i18n/types.ts index b22bc67..2c9aba1 100644 --- a/src/i18n/types.ts +++ b/src/i18n/types.ts @@ -56,6 +56,9 @@ export type TranslationKeys = { addedOn: string; reviewRequired: string; unknownBottle: string; + filters: string; + resetAll: string; + close: string; }; bottle: { details: string; diff --git a/src/lib/format.ts b/src/lib/format.ts new file mode 100644 index 0000000..75d6af6 --- /dev/null +++ b/src/lib/format.ts @@ -0,0 +1,33 @@ +/** + * Shortens a whisky category label by removing redundant common terms. + * Example: "Single Malt Scotch Whisky" -> "Single Malt" + */ +export const shortenCategory = (label: string): string => { + if (!label) return ''; + + const replacements: [string, string][] = [ + ['Single Malt Scotch Whisky', 'Single Malt'], + ['Blended Malt Scotch Whisky', 'Blended Malt'], + ['Single Grain Scotch Whisky', 'Single Grain'], + ['Blended Scotch Whisky', 'Blended'], + ['Kentucky Straight Bourbon Whiskey', 'Bourbon'], + ['Straight Bourbon Whiskey', 'Bourbon'], + ['Tennessee Whiskey', 'Tennessee'], + ['Japanese Whisky', 'Japanese'], + ['Irish Whiskey', 'Irish'], + ['Canadian Whisky', 'Canadian'], + ['American Whiskey', 'American'], + ['Rye Whiskey', 'Rye'], + ]; + + for (const [full, short] of replacements) { + if (label === full) return short; + } + + return label + .replace(/ Scotch Whisky/gi, '') + .replace(/ Scotch/gi, '') + .replace(/ Whisky/gi, '') + .replace(/ Whiskey/gi, '') + .trim(); +};