mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-08-29 01:35:39 -04:00
Add mobile bottom tab bar for small screen navigation
- New MobileTabBar component with Home, Search, and Account tabs - Animated indicator follows active tab (spring layoutId transition) - Shown only below sm breakpoint where desktop nav links are hidden - Account tab provides quick sign-out access - Safe area padding for devices with home indicators - Added bottom padding to layout to prevent content overlap Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@
|
|||||||
import { CommandPalette } from "@/components/command-palette";
|
import { CommandPalette } from "@/components/command-palette";
|
||||||
import { KeyboardHelpDialog } from "@/components/keyboard-help-dialog";
|
import { KeyboardHelpDialog } from "@/components/keyboard-help-dialog";
|
||||||
import { KeyboardProvider } from "@/components/keyboard-provider";
|
import { KeyboardProvider } from "@/components/keyboard-provider";
|
||||||
|
import { MobileTabBar } from "@/components/mobile-tab-bar";
|
||||||
import { NavBar } from "@/components/nav-bar";
|
import { NavBar } from "@/components/nav-bar";
|
||||||
import { Toaster } from "@/components/ui/sonner";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
|
|
||||||
@@ -13,7 +14,7 @@ export default function PagesLayout({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<KeyboardProvider>
|
<KeyboardProvider>
|
||||||
<div className="min-h-screen">
|
<div className="min-h-screen pb-14 sm:pb-0">
|
||||||
<NavBar />
|
<NavBar />
|
||||||
{/* Ambient glow */}
|
{/* Ambient glow */}
|
||||||
<div className="pointer-events-none fixed left-1/2 top-1/4 -translate-x-1/2 -translate-y-1/2 h-[600px] w-[800px] rounded-full bg-primary/3 blur-[200px]" />
|
<div className="pointer-events-none fixed left-1/2 top-1/4 -translate-x-1/2 -translate-y-1/2 h-[600px] w-[800px] rounded-full bg-primary/3 blur-[200px]" />
|
||||||
@@ -21,6 +22,7 @@ export default function PagesLayout({
|
|||||||
{children}
|
{children}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
<MobileTabBar />
|
||||||
<CommandPalette />
|
<CommandPalette />
|
||||||
<KeyboardHelpDialog />
|
<KeyboardHelpDialog />
|
||||||
<Toaster position="bottom-right" />
|
<Toaster position="bottom-right" />
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { IconHome, IconSearch, IconUser } from "@tabler/icons-react";
|
||||||
|
import { motion } from "motion/react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { usePathname, useRouter } from "next/navigation";
|
||||||
|
import { signOut, useSession } from "@/lib/auth/client";
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ href: "/dashboard", label: "Home", icon: IconHome },
|
||||||
|
{ href: "/search", label: "Search", icon: IconSearch },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export function MobileTabBar() {
|
||||||
|
const { data: session } = useSession();
|
||||||
|
const pathname = usePathname();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
if (!session?.user) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav className="fixed bottom-0 left-0 right-0 z-50 border-t border-border/50 bg-background/90 backdrop-blur-xl sm:hidden">
|
||||||
|
<div className="flex h-14 items-stretch">
|
||||||
|
{tabs.map((tab) => {
|
||||||
|
const Icon = tab.icon;
|
||||||
|
const isActive =
|
||||||
|
tab.href === "/dashboard"
|
||||||
|
? pathname === "/dashboard"
|
||||||
|
: pathname.startsWith(tab.href);
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={tab.href}
|
||||||
|
href={tab.href}
|
||||||
|
className="relative flex flex-1 flex-col items-center justify-center gap-0.5"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
size={20}
|
||||||
|
className={isActive ? "text-primary" : "text-muted-foreground"}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className={`text-[10px] font-medium ${isActive ? "text-primary" : "text-muted-foreground"}`}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</span>
|
||||||
|
{isActive && (
|
||||||
|
<motion.div
|
||||||
|
layoutId="mobile-tab-indicator"
|
||||||
|
className="absolute top-0 h-0.5 w-8 rounded-full bg-primary"
|
||||||
|
transition={{
|
||||||
|
type: "spring",
|
||||||
|
stiffness: 380,
|
||||||
|
damping: 30,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={async () => {
|
||||||
|
await signOut();
|
||||||
|
router.push("/");
|
||||||
|
router.refresh();
|
||||||
|
}}
|
||||||
|
className="flex flex-1 flex-col items-center justify-center gap-0.5"
|
||||||
|
>
|
||||||
|
<IconUser size={20} className="text-muted-foreground" />
|
||||||
|
<span className="text-[10px] font-medium text-muted-foreground">
|
||||||
|
{session.user.name?.split(" ")[0] ?? "Account"}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/* Safe area for devices with home indicator */}
|
||||||
|
<div className="h-[env(safe-area-inset-bottom)]" />
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user