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:
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
48
src/components/EmptyState.tsx
Normal file
48
src/components/EmptyState.tsx
Normal 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
Reference in New Issue
Block a user