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; 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) => { export const BottomNavigation = ({ onHome, onShelf, onSearch, onProfile, onScan }: BottomNavigationProps) => {
const fileInputRef = React.useRef<HTMLInputElement>(null); const fileInputRef = React.useRef<HTMLInputElement>(null);
@@ -27,7 +45,7 @@ export const BottomNavigation = ({ onHome, onShelf, onSearch, onProfile, onScan
}; };
return ( 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 */} {/* Hidden Input for Scanning */}
<input <input
type="file" type="file"
@@ -37,49 +55,46 @@ export const BottomNavigation = ({ onHome, onShelf, onSearch, onProfile, onScan
className="hidden" 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 */} {/* Left Items */}
<button <NavButton
onClick={onHome} onClick={onHome}
className="p-3 text-zinc-400 hover:text-white transition-colors active:scale-90" icon={<Home size={20} strokeWidth={2.5} />}
aria-label="Home" label="Start"
> ariaLabel="Home"
<Home size={22} strokeWidth={2.5} /> />
</button>
<button <NavButton
onClick={onShelf} onClick={onShelf}
className="p-3 text-zinc-400 hover:text-white transition-colors active:scale-90" icon={<Grid size={20} strokeWidth={2.5} />}
aria-label="Shelf" label="Sammlung"
> ariaLabel="Sammlung"
<Grid size={22} strokeWidth={2.5} /> />
</button>
{/* PRIMARY ACTION - The "Industrial Button" */} {/* PRIMARY ACTION - Scan Button */}
<button <button
onClick={handleScanClick} 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" 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="Scan Bottle" 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> </button>
{/* Right Items */} {/* Right Items */}
<button <NavButton
onClick={onSearch} onClick={onSearch}
className="p-3 text-zinc-400 hover:text-white transition-colors active:scale-90" icon={<Search size={20} strokeWidth={2.5} />}
aria-label="Search" label="Filter"
> ariaLabel="Filter"
<Search size={22} strokeWidth={2.5} /> />
</button>
<button <NavButton
onClick={onProfile} onClick={onProfile}
className="p-3 text-zinc-400 hover:text-white transition-colors active:scale-90" icon={<User size={20} strokeWidth={2.5} />}
aria-label="Profile" label="Profil"
> ariaLabel="Profil"
<User size={22} strokeWidth={2.5} /> />
</button>
</div> </div>
</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>
);
}

File diff suppressed because one or more lines are too long