feat: Add navigation labels and EmptyState component

Bottom Navigation:
- Added text labels under icons (Start, Sammlung, Filter, Profil)
- Elevated SCAN button with label
- Improved touch targets and visual hierarchy

EmptyState Component:
- Reusable component for empty lists
- Icon, title, description, optional CTA button
- Ready for use in SessionList, BuddyList, BottleGrid
This commit is contained in:
2025-12-26 21:40:31 +01:00
parent af54d8061c
commit 82531c5aff
3 changed files with 94 additions and 31 deletions

View File

@@ -12,6 +12,24 @@ interface BottomNavigationProps {
onScan: (file: File) => void;
}
interface NavButtonProps {
onClick?: () => void;
icon: React.ReactNode;
label: string;
ariaLabel: string;
}
const NavButton = ({ onClick, icon, label, ariaLabel }: NavButtonProps) => (
<button
onClick={onClick}
className="flex flex-col items-center gap-0.5 px-3 py-1.5 text-zinc-400 hover:text-white transition-colors active:scale-95"
aria-label={ariaLabel}
>
{icon}
<span className="text-[9px] font-medium tracking-wide">{label}</span>
</button>
);
export const BottomNavigation = ({ onHome, onShelf, onSearch, onProfile, onScan }: BottomNavigationProps) => {
const fileInputRef = React.useRef<HTMLInputElement>(null);
@@ -27,7 +45,7 @@ export const BottomNavigation = ({ onHome, onShelf, onSearch, onProfile, onScan
};
return (
<div className="fixed bottom-6 left-1/2 -translate-x-1/2 w-[90%] max-w-sm z-50 pointer-events-none">
<div className="fixed bottom-6 left-1/2 -translate-x-1/2 w-[95%] max-w-md z-50 pointer-events-none">
{/* Hidden Input for Scanning */}
<input
type="file"
@@ -37,49 +55,46 @@ export const BottomNavigation = ({ onHome, onShelf, onSearch, onProfile, onScan
className="hidden"
/>
<div className="flex items-center justify-between px-2 py-2 bg-zinc-900/90 backdrop-blur-lg border border-zinc-800 rounded-full shadow-2xl pointer-events-auto">
<div className="flex items-center justify-between px-1 py-1 bg-zinc-900/95 backdrop-blur-lg border border-zinc-800 rounded-full shadow-2xl pointer-events-auto">
{/* Left Items */}
<button
<NavButton
onClick={onHome}
className="p-3 text-zinc-400 hover:text-white transition-colors active:scale-90"
aria-label="Home"
>
<Home size={22} strokeWidth={2.5} />
</button>
icon={<Home size={20} strokeWidth={2.5} />}
label="Start"
ariaLabel="Home"
/>
<button
<NavButton
onClick={onShelf}
className="p-3 text-zinc-400 hover:text-white transition-colors active:scale-90"
aria-label="Shelf"
>
<Grid size={22} strokeWidth={2.5} />
</button>
icon={<Grid size={20} strokeWidth={2.5} />}
label="Sammlung"
ariaLabel="Sammlung"
/>
{/* PRIMARY ACTION - The "Industrial Button" */}
{/* PRIMARY ACTION - Scan Button */}
<button
onClick={handleScanClick}
className="flex items-center justify-center w-14 h-14 rounded-full bg-orange-600 text-white hover:bg-orange-500 active:scale-95 transition-all mx-2 shadow-lg shadow-orange-950/40"
aria-label="Scan Bottle"
className="flex flex-col items-center justify-center w-16 h-16 -mt-4 rounded-full bg-orange-600 text-white hover:bg-orange-500 active:scale-95 transition-all shadow-lg shadow-orange-950/50 border-4 border-zinc-950"
aria-label="Flasche scannen"
>
<Scan size={26} strokeWidth={2.5} />
<Scan size={24} strokeWidth={2.5} />
<span className="text-[8px] font-bold tracking-wide mt-0.5">SCAN</span>
</button>
{/* Right Items */}
<button
<NavButton
onClick={onSearch}
className="p-3 text-zinc-400 hover:text-white transition-colors active:scale-90"
aria-label="Search"
>
<Search size={22} strokeWidth={2.5} />
</button>
icon={<Search size={20} strokeWidth={2.5} />}
label="Filter"
ariaLabel="Filter"
/>
<button
<NavButton
onClick={onProfile}
className="p-3 text-zinc-400 hover:text-white transition-colors active:scale-90"
aria-label="Profile"
>
<User size={22} strokeWidth={2.5} />
</button>
icon={<User size={20} strokeWidth={2.5} />}
label="Profil"
ariaLabel="Profil"
/>
</div>
</div>
);

View File

@@ -0,0 +1,48 @@
'use client';
import { motion } from 'framer-motion';
import { LucideIcon } from 'lucide-react';
interface EmptyStateProps {
icon: LucideIcon;
title: string;
description: string;
actionLabel?: string;
onAction?: () => void;
}
/**
* Reusable empty state component for lists without data
* Shows icon, title, description, and optional action button
*/
export default function EmptyState({
icon: Icon,
title,
description,
actionLabel,
onAction,
}: EmptyStateProps) {
return (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className="flex flex-col items-center justify-center py-12 px-6 text-center"
>
<div className="w-16 h-16 rounded-2xl bg-zinc-800/50 flex items-center justify-center mb-4">
<Icon size={28} className="text-zinc-500" />
</div>
<h3 className="text-lg font-bold text-white mb-2">{title}</h3>
<p className="text-sm text-zinc-500 max-w-xs mb-6">{description}</p>
{actionLabel && onAction && (
<button
onClick={onAction}
className="px-6 py-3 bg-orange-600 hover:bg-orange-500 text-white font-bold text-sm rounded-xl transition-colors active:scale-95"
>
{actionLabel}
</button>
)}
</motion.div>
);
}