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:
+108
-4
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user