deckhearth/components/MobileNavigation.js

171 lines
6.8 KiB
JavaScript
Raw Normal View History

import { useRouter } from 'next/router';
import Link from 'next/link';
import { useState } from 'react';
chore(components): remove dead user prop from MobileNavigation components/MobileNavigation.js has accepted a `user` prop ever since the mobile bottom-bar was extracted from Layout, but it has never read any field of `user`. The bottom-bar items (Cards, Decks, Dashboard, Community, More) are statically configured — none of them branch on auth state, role, user id, or any other per-user attribute. The prop is dead. This was originally surfaced as R8 in the fix-layout-default-user convoy (commit ca302a8) and deliberately deferred there to keep that convoy focused on the Layout default-user fix. The follow-up was queued as cleanup-mobile-nav-dead-props in .convoys/ship-readiness.md § Queued convoys. Pre-edit audit confirms the queue entry's premise: `rg '\\buser\\b' components/MobileNavigation.js` returns 1 hit (the destructure on line 5) before the change and 0 hits after. The only active call site is components/Layout.js line 598; the components/Layout.js.backup snapshot also calls it but is a no-go-zone (per .cursor/rules/no-go-zones.mdc § "Append-only / historical") and stays untouched — when that backup is eventually deleted in a separate convoy, its stale call disappears with it. Verification: npm run lint exit 1 with 128 problems (baseline preserved, no regression introduced); npm run test:run 21/21 pass (test/components/Layout.test.js still asserts the logged-out branch contract from PR #15 — the dead-prop removal is invisible to that suite since it does not inspect MobileNavigation's prop shape). Convoy file .convoys/cleanup-mobile-nav-dead-props.md captures the audit, fix, risks (R1: a future per-user bottom-bar feature would need to re-add the prop — accepted; carrying dead state to hedge hypothetical features is worse than paying the one-line re-add cost when the feature actually lands), and acceptance criteria. Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-26 23:50:13 -04:00
export default function MobileNavigation({ onMenuOpen }) {
const router = useRouter();
// Navigation items for the bottom bar
const navigationItems = [
{
name: 'Cards',
href: '/cards',
icon: 'cards',
active: router.pathname === '/cards'
},
{
name: 'Decks',
href: '/decks',
icon: 'decks',
active: router.pathname === '/decks'
},
{
name: 'Dashboard',
href: '/dashboard',
icon: 'dashboard',
active: router.pathname === '/dashboard' || router.pathname === '/collections' || router.pathname === '/my-cards' || router.pathname === '/analytics',
isPrimary: true // This will be the raised center button
},
{
name: 'Community',
href: '/community',
icon: 'community',
active: router.pathname.startsWith('/community')
},
{
name: 'More',
href: '#',
icon: 'more',
onClick: onMenuOpen,
active: false
}
];
const getIcon = (iconName, isActive = false, isPrimary = false) => {
const iconClass = isPrimary ? "h-7 w-7" : "h-6 w-6";
const strokeWidth = isActive ? 2.5 : 2;
const icons = {
cards: (
<svg className={iconClass} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={strokeWidth} d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z" />
</svg>
),
decks: (
<svg className={iconClass} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={strokeWidth} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
</svg>
),
dashboard: (
<svg className={iconClass} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={strokeWidth} d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" />
</svg>
),
community: (
<svg className={iconClass} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={strokeWidth} d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z" />
</svg>
),
more: (
<svg className={iconClass} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={strokeWidth} d="M4 6h16M4 12h16M4 18h16" />
</svg>
)
};
return icons[iconName] || icons.dashboard;
};
return (
<div className="md:hidden fixed bottom-0 left-0 right-0 z-50">
{/* Background with blur effect */}
<div
className="absolute inset-0 mobile-nav-backdrop border-t"
style={{
backgroundColor: `rgba(var(--bg-secondary-rgb), 0.95)`,
borderColor: 'var(--border)'
}}
/>
{/* Navigation Content */}
<div className="relative px-4 py-2">
<div className="flex items-center justify-around">
{navigationItems.map((item) => {
if (item.isPrimary) {
// Primary/Dashboard button - raised and prominent
return (
<Link key={item.name} href={item.href}>
<div className="relative">
{/* Raised background circle */}
<div
className="absolute inset-0 rounded-full shadow-lg transform -translate-y-2"
style={{
background: item.active
? 'var(--gradient-primary)'
: 'var(--gradient-secondary)',
width: '56px',
height: '56px'
}}
/>
{/* Button content */}
<button
className="relative flex flex-col items-center justify-center w-14 h-14 transform -translate-y-2 transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2"
style={{
color: 'white',
'--tw-ring-color': 'var(--accent-ember)',
'--tw-ring-offset-color': 'var(--bg-primary)'
}}
aria-label={item.name}
>
{getIcon(item.icon, item.active, true)}
</button>
{/* Label below raised button */}
<span
className="absolute top-12 left-1/2 transform -translate-x-1/2 text-xs font-medium whitespace-nowrap"
style={{
color: item.active ? 'var(--accent-ember)' : 'var(--text-secondary)'
}}
>
{item.name}
</span>
</div>
</Link>
);
} else {
// Regular navigation buttons
const ButtonComponent = item.onClick ? 'button' : Link;
const buttonProps = item.onClick
? { onClick: item.onClick }
: { href: item.href };
return (
<ButtonComponent key={item.name} {...buttonProps}>
<div
className="flex flex-col items-center justify-center py-2 px-3 transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 rounded-lg"
style={{
color: item.active ? 'var(--accent-ember)' : 'var(--text-secondary)',
'--tw-ring-color': 'var(--accent-ember)',
'--tw-ring-offset-color': 'var(--bg-secondary)'
}}
>
<div className="mb-1">
{getIcon(item.icon, item.active)}
</div>
<span className="text-xs font-medium">
{item.name}
</span>
</div>
</ButtonComponent>
);
}
})}
</div>
</div>
{/* Safe area padding for devices with home indicator */}
<div className="h-safe-area-inset-bottom" />
</div>
);
}