Sessions 5-7a: 955 tests, deployment ready
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
'use client';
|
||||
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { useParlay } from '@/contexts/ParlayContext';
|
||||
|
||||
const TABS = [
|
||||
{ id: 'home', label: 'Home', href: '/dashboard', icon: HomeIcon },
|
||||
{ id: 'scan', label: 'Read', href: '/scan', icon: ScanIcon },
|
||||
{ id: 'parlay', label: 'Parlay', href: null, icon: ParlayIcon },
|
||||
{ id: 'ledger', label: 'Ledger', href: '/ledger', icon: LedgerIcon },
|
||||
{ id: 'profile', label: 'Profile', href: '/profile', icon: ProfileIcon },
|
||||
] as const;
|
||||
|
||||
// Pages where the bottom tab bar should stay hidden (auth flows, landing).
|
||||
const HIDE_ON = new Set(['/login', '/signup', '/auth/callback', '/']);
|
||||
|
||||
export default function BottomTabBar() {
|
||||
const pathname = usePathname() || '/';
|
||||
const { open, legCount } = useParlay();
|
||||
|
||||
if (HIDE_ON.has(pathname)) return null;
|
||||
|
||||
return (
|
||||
<nav
|
||||
role="navigation"
|
||||
aria-label="Primary"
|
||||
className="mobile-tab-bar"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 64,
|
||||
zIndex: 40,
|
||||
display: 'flex',
|
||||
borderTop: '1px solid var(--border)',
|
||||
background: 'rgba(10,10,15,0.92)',
|
||||
backdropFilter: 'blur(16px)',
|
||||
WebkitBackdropFilter: 'blur(16px)',
|
||||
paddingBottom: 'env(safe-area-inset-bottom)',
|
||||
}}
|
||||
>
|
||||
{TABS.map((t) => {
|
||||
const active = t.href ? (pathname === t.href || pathname.startsWith(`${t.href}/`)) : false;
|
||||
const color = active ? 'var(--grade-a)' : 'var(--text-secondary)';
|
||||
const Icon = t.icon;
|
||||
const isParlay = t.id === 'parlay';
|
||||
const onClick = () => {
|
||||
if (isParlay) open();
|
||||
};
|
||||
|
||||
const inner = (
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 4,
|
||||
color,
|
||||
fontSize: 10,
|
||||
fontWeight: 600,
|
||||
textDecoration: 'none',
|
||||
fontFamily: 'inherit',
|
||||
border: 'none',
|
||||
background: 'transparent',
|
||||
cursor: 'pointer',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<Icon color={color} />
|
||||
<span>{t.label}</span>
|
||||
{isParlay && legCount > 0 && (
|
||||
<span
|
||||
className="mono"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 6,
|
||||
right: 'calc(50% - 22px)',
|
||||
minWidth: 18,
|
||||
height: 18,
|
||||
padding: '0 5px',
|
||||
borderRadius: 999,
|
||||
background: 'var(--grade-a)',
|
||||
color: 'var(--bg-primary)',
|
||||
fontSize: 10,
|
||||
fontWeight: 800,
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
{legCount}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
if (isParlay || !t.href) {
|
||||
return (
|
||||
<button key={t.id} onClick={onClick} style={{ flex: 1, background: 'transparent', border: 'none', padding: 0 }}>
|
||||
{inner}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<a key={t.id} href={t.href} style={{ flex: 1, padding: 0, textDecoration: 'none' }}>
|
||||
{inner}
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
|
||||
<style jsx>{`
|
||||
@media (min-width: 768px) {
|
||||
:global(.mobile-tab-bar) {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
// Lightweight inline SVG icons — keeps the bundle slim and avoids icon-lib install
|
||||
function HomeIcon({ color }: { color: string }) {
|
||||
return (
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M3 12 12 3l9 9" />
|
||||
<path d="M5 10v10h14V10" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function ScanIcon({ color }: { color: string }) {
|
||||
return (
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="11" cy="11" r="7" />
|
||||
<path d="M21 21l-4.3-4.3" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function ParlayIcon({ color }: { color: string }) {
|
||||
return (
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<rect x="3" y="4" width="18" height="4" rx="1" />
|
||||
<rect x="3" y="10" width="18" height="4" rx="1" />
|
||||
<rect x="3" y="16" width="18" height="4" rx="1" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function LedgerIcon({ color }: { color: string }) {
|
||||
return (
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M4 4h16v16H4z" />
|
||||
<path d="M4 9h16" />
|
||||
<path d="M9 4v16" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function ProfileIcon({ color }: { color: string }) {
|
||||
return (
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="12" cy="8" r="4" />
|
||||
<path d="M4 21c1.5-4 5-6 8-6s6.5 2 8 6" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user