Files
petfeed/src/components/layout/MobileNav.tsx
T
admin e9b286b4df feat(02-03): /feed page + home redirect + nav Home→/feed
- src/app/(app)/feed/page.tsx: RSC shell with StoryTray seam + FeedList, metadata "Home — PawFeed"
- src/app/page.tsx: redirect("/feed") per D-04 (home route → /feed after Phase 2)
- Sidebar.tsx: Home href "/" → "/feed"; active detection handles /feed and / (D-05)
- MobileNav.tsx: Home href "/" → "/feed"; active state updated for /feed (D-05)
- (app)/layout.tsx: unchanged — zero-pets redirect to /onboarding/pet preserved (D-06)
2026-06-07 12:37:04 +02:00

190 lines
6.6 KiB
TypeScript

"use client";
import Link from "next/link";
import { usePathname, useRouter } from "next/navigation";
import {
Home,
Compass,
Plus,
Bell,
User,
ChevronDown,
} from "lucide-react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { useActivePet } from "@/context/ActivePetContext";
import { getAvatarUrl } from "@/lib/avatar-url";
type Pet = {
id: string;
name: string;
avatarKey: string | null;
species: { name: string };
};
type MobileNavProps = {
pets: Pet[];
};
/**
* Mobile bottom tab bar — visible below lg: breakpoint.
*
* UI-SPEC: Navigation (App Shell) — Mobile
* - Fixed bottom bar (h-16) with bg-card border-top
* - 5 tabs: Home, Explore (placeholder), Post (center, orange-500), Notifications (placeholder), Profile
* - Top active pet switcher dropdown above the tab bar
*
* Explore and Notifications are placeholder tabs (aria-disabled) in Phase 1.
* Post tab (center) links to /pets in Phase 1 — full post creation in Phase 2.
*
* Deviation notes:
* - shadcn base-nova uses @base-ui/react/menu which does not support asChild on
* DropdownMenuTrigger or DropdownMenuItem. Using onClick+useRouter for navigation items.
*/
export function MobileNav({ pets }: MobileNavProps) {
const pathname = usePathname();
const router = useRouter();
const { activePetId, setActivePet } = useActivePet();
const activePet = pets.find((p) => p.id === activePetId) ?? pets[0];
// D-05: Home → /feed; also highlight when on / (which redirects to /feed)
const isHomePage = pathname === "/feed" || pathname === "/";
const isPetsPage = pathname.startsWith("/pets");
return (
<div className="lg:hidden fixed bottom-0 left-0 right-0 z-50">
{/* Active pet switcher bar — shown above tab bar on mobile */}
{activePet && (
<div className="bg-background border-t border px-4 py-2">
<DropdownMenu>
<DropdownMenuTrigger
className="flex items-center gap-2 text-sm cursor-pointer"
aria-label="Switch active pet"
>
<Avatar className="h-6 w-6 shrink-0">
{activePet.avatarKey && (
<AvatarImage
src={getAvatarUrl(activePet.avatarKey)!}
alt={activePet.name}
/>
)}
<AvatarFallback className="text-xs bg-muted">
{activePet.name.slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
<span className="font-medium text-foreground truncate max-w-[120px]">
{activePet.name}
</span>
<ChevronDown className="h-3 w-3 text-muted-foreground" />
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-[200px]">
{pets.map((pet) => (
<DropdownMenuItem
key={pet.id}
onClick={() => setActivePet(pet.id)}
className="flex items-center gap-2 cursor-pointer"
>
<Avatar className="h-6 w-6 shrink-0">
{pet.avatarKey && (
<AvatarImage
src={getAvatarUrl(pet.avatarKey)!}
alt={pet.name}
/>
)}
<AvatarFallback className="text-xs bg-muted">
{pet.name.slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
<span className="flex-1 text-sm truncate">{pet.name}</span>
<Badge variant="secondary" className="text-xs shrink-0">
{pet.species.name}
</Badge>
{pet.id === activePetId && (
<span
className="w-2 h-2 rounded-full bg-orange-500 shrink-0"
aria-label="Active pet"
/>
)}
</DropdownMenuItem>
))}
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => router.push("/onboarding/pet")}
className="flex items-center gap-2 cursor-pointer"
>
<span className="text-orange-500">+</span>
<span>Add Pet</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
)}
{/* Bottom tab bar */}
<nav
className="h-16 bg-card border-t border flex items-center justify-around px-2"
aria-label="Mobile navigation"
>
{/* Home — D-05: href updated to /feed; label stays "Home" */}
<Link
href="/feed"
className={`flex flex-col items-center gap-0.5 px-3 py-2 text-xs ${
isHomePage ? "text-orange-500" : "text-muted-foreground"
}`}
>
<Home className="h-5 w-5" aria-hidden="true" />
<span>Home</span>
</Link>
{/* Explore — placeholder, Phase 1 */}
<span
aria-disabled="true"
className="flex flex-col items-center gap-0.5 px-3 py-2 text-xs text-muted-foreground/50 cursor-not-allowed"
>
<Compass className="h-5 w-5" aria-hidden="true" />
<span>Explore</span>
</span>
{/* Post — center, accent-filled. Phase 1: navigates to /pets as placeholder */}
<Link
href="/pets"
aria-label="My Pets"
className="flex flex-col items-center"
>
<span className="bg-orange-500 text-white rounded-full p-3 flex items-center justify-center">
<Plus className="h-5 w-5" aria-hidden="true" />
</span>
</Link>
{/* Notifications — placeholder, Phase 1 */}
<span
aria-disabled="true"
className="flex flex-col items-center gap-0.5 px-3 py-2 text-xs text-muted-foreground/50 cursor-not-allowed"
>
<Bell className="h-5 w-5" aria-hidden="true" />
<span>Alerts</span>
</span>
{/* Profile (My Pets) */}
<Link
href="/pets"
className={`flex flex-col items-center gap-0.5 px-3 py-2 text-xs ${
isPetsPage ? "text-orange-500" : "text-muted-foreground"
}`}
>
<User className="h-5 w-5" aria-hidden="true" />
<span>Profile</span>
</Link>
</nav>
</div>
);
}