- auth: login/register bottom sheets, token storage, authFetch - AuthProvider context w/ auto-refresh on mount - hide add/edit/delete for guests, sign-in button in nav - SPA mode (ssr:false), loaders→client-side useEffect - API base URL /api (same-origin), loading spinners
105 lines
3.3 KiB
TypeScript
105 lines
3.3 KiB
TypeScript
import { useState } from "react";
|
|
import { NavLink } from "react-router";
|
|
import { LogOut, Music, Sun, Moon, User } from "lucide-react";
|
|
import { useTheme } from "next-themes";
|
|
import { cn } from "~/lib/utils";
|
|
import { Button } from "~/components/ui/button";
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
} from "~/components/ui/dropdown-menu";
|
|
import { useAuth } from "~/lib/auth";
|
|
import { LoginSheet } from "~/components/login-sheet";
|
|
import { RegisterSheet } from "~/components/register-sheet";
|
|
|
|
export function BottomNav() {
|
|
const { resolvedTheme, setTheme } = useTheme();
|
|
const { isAuthenticated, logout } = useAuth();
|
|
const [loginOpen, setLoginOpen] = useState(false);
|
|
const [registerOpen, setRegisterOpen] = useState(false);
|
|
|
|
return (
|
|
<>
|
|
<nav className="border-t bg-background shrink-0">
|
|
<div className="max-w-lg mx-auto flex items-center">
|
|
<NavLink
|
|
to="/"
|
|
end
|
|
className={({ isActive }) =>
|
|
cn(
|
|
"flex flex-col items-center gap-0.5 flex-1 py-2 text-xs transition-colors",
|
|
isActive
|
|
? "text-primary"
|
|
: "text-muted-foreground hover:text-foreground",
|
|
)
|
|
}
|
|
>
|
|
<Music className="w-5 h-5" />
|
|
<span>Library</span>
|
|
</NavLink>
|
|
|
|
<div className="flex items-center gap-1 mr-2">
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="text-muted-foreground hover:text-foreground"
|
|
onClick={() =>
|
|
setTheme(resolvedTheme === "dark" ? "light" : "dark")
|
|
}
|
|
aria-label="Toggle theme"
|
|
>
|
|
{resolvedTheme === "dark" ? (
|
|
<Sun className="w-5 h-5" />
|
|
) : (
|
|
<Moon className="w-5 h-5" />
|
|
)}
|
|
</Button>
|
|
|
|
{isAuthenticated ? (
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="text-muted-foreground hover:text-foreground"
|
|
>
|
|
<User className="w-5 h-5" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
<DropdownMenuItem onClick={() => logout()}>
|
|
<LogOut className="w-4 h-4 mr-2" />
|
|
Sign out
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
) : (
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="text-xs text-muted-foreground hover:text-foreground"
|
|
onClick={() => setLoginOpen(true)}
|
|
>
|
|
Sign in
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
<LoginSheet
|
|
open={loginOpen}
|
|
onOpenChange={setLoginOpen}
|
|
onSwitchToRegister={() => setRegisterOpen(true)}
|
|
/>
|
|
<RegisterSheet
|
|
open={registerOpen}
|
|
onOpenChange={setRegisterOpen}
|
|
onSwitchToLogin={() => setLoginOpen(true)}
|
|
/>
|
|
</>
|
|
);
|
|
}
|