Add mobile-first responsive design with bottom tab navigation

Converts desktop sidebar to hidden on mobile, adds bottom tab bar with
5 primary items and a "More" overflow menu. All pages get responsive
spacing, smaller touch targets, and tighter grids on small screens.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-02-16 12:58:48 -06:00
parent faa7dbf4d3
commit 4903b84aef
14 changed files with 263 additions and 137 deletions
+108 -4
View File
@@ -21,8 +21,24 @@ const navItems = [
{ to: '/', label: 'Settings', icon: SettingsIcon },
]
// Bottom bar shows a subset of nav items (most used) to avoid crowding
const mobileNavItems = [
{ to: '/feed', label: 'Feed', icon: FeedIcon },
{ to: '/users', label: 'Users', icon: UsersIcon },
{ to: '/gallery', label: 'Gallery', icon: GalleryNavIcon },
{ to: '/search', label: 'Search', icon: SearchIcon },
{ to: '/more', label: 'More', icon: MoreIcon },
]
const moreNavItems = [
{ to: '/downloads', label: 'Downloads', icon: DownloadIcon },
{ to: '/scrape', label: 'Scrape', icon: ScrapeIcon },
{ to: '/', label: 'Settings', icon: SettingsIcon },
]
export default function App() {
const [currentUser, setCurrentUser] = useState(null)
const [moreOpen, setMoreOpen] = useState(false)
const location = useLocation()
useEffect(() => {
@@ -33,6 +49,11 @@ export default function App() {
})
}, [])
// Close "more" menu on route change
useEffect(() => {
setMoreOpen(false)
}, [location.pathname])
const refreshUser = () => {
getMe().then((data) => {
if (!data.error) {
@@ -41,10 +62,14 @@ export default function App() {
})
}
const isMoreActive = moreNavItems.some((item) =>
item.to === '/' ? location.pathname === '/' : location.pathname.startsWith(item.to)
)
return (
<div className="flex min-h-screen bg-[#0a0a0a]">
{/* Sidebar */}
<aside className="fixed left-0 top-0 bottom-0 w-60 bg-[#111] border-r border-[#222] flex flex-col z-50">
{/* Desktop Sidebar */}
<aside className="hidden md:flex fixed left-0 top-0 bottom-0 w-60 bg-[#111] border-r border-[#222] flex-col z-50">
{/* Logo */}
<div className="p-6 border-b border-[#222]">
<h1 className="text-xl font-bold text-white tracking-tight">
@@ -104,8 +129,8 @@ export default function App() {
</aside>
{/* Main Content */}
<main className="ml-60 flex-1 min-h-screen">
<div className="max-w-5xl mx-auto p-6">
<main className="md:ml-60 flex-1 min-h-screen pb-20 md:pb-0">
<div className="max-w-5xl mx-auto p-4 md:p-6">
<Routes>
<Route path="/" element={<Login onAuth={refreshUser} />} />
<Route path="/feed" element={<Feed />} />
@@ -119,6 +144,77 @@ export default function App() {
</Routes>
</div>
</main>
{/* Mobile Bottom Nav */}
<nav className="md:hidden fixed bottom-0 left-0 right-0 bg-[#111] border-t border-[#222] z-50 safe-bottom">
<div className="flex items-center justify-around h-14">
{mobileNavItems.map((item) => {
const Icon = item.icon
if (item.to === '/more') {
return (
<button
key="more"
onClick={() => setMoreOpen((v) => !v)}
className={`flex flex-col items-center justify-center gap-0.5 w-full h-full transition-colors ${
isMoreActive || moreOpen ? 'text-[#0095f6]' : 'text-gray-500'
}`}
>
<Icon className="w-5 h-5" />
<span className="text-[10px]">{item.label}</span>
</button>
)
}
const isActive =
item.to === '/'
? location.pathname === '/'
: location.pathname.startsWith(item.to)
return (
<NavLink
key={item.to}
to={item.to}
className={`flex flex-col items-center justify-center gap-0.5 w-full h-full transition-colors ${
isActive ? 'text-[#0095f6]' : 'text-gray-500'
}`}
>
<Icon className="w-5 h-5" />
<span className="text-[10px]">{item.label}</span>
</NavLink>
)
})}
</div>
{/* More menu popover */}
{moreOpen && (
<>
<div className="fixed inset-0 z-40" onClick={() => setMoreOpen(false)} />
<div className="absolute bottom-full left-0 right-0 bg-[#161616] border-t border-[#222] z-50 shadow-xl">
{moreNavItems.map((item) => {
const Icon = item.icon
const isActive =
item.to === '/'
? location.pathname === '/'
: location.pathname.startsWith(item.to)
return (
<NavLink
key={item.to}
to={item.to}
className={`flex items-center gap-3 px-5 py-3.5 transition-colors ${
isActive ? 'text-[#0095f6] bg-[#0095f6]/5' : 'text-gray-400'
}`}
>
<Icon className="w-5 h-5" />
<span className="text-sm font-medium">{item.label}</span>
</NavLink>
)
})}
</div>
</>
)}
</nav>
</div>
)
}
@@ -181,3 +277,11 @@ function SettingsIcon({ className }) {
</svg>
)
}
function MoreIcon({ className }) {
return (
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
</svg>
)
}