Convert sidebar to fixed bottom tab bar on mobile (<768px) with icon-only nav items and active indicator. Todo list uses stacked card layout on mobile instead of 5-column grid. Reduce padding throughout for small screens and enable user scaling for accessibility. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
153 lines
6.0 KiB
TypeScript
153 lines
6.0 KiB
TypeScript
"use client";
|
|
|
|
import Image from "next/image";
|
|
import Link from "next/link";
|
|
import { usePathname } from "next/navigation";
|
|
import { Bell, BellDot, ListTodo, LogOut, Settings, UserPlus } from "lucide-react";
|
|
|
|
import {
|
|
Sidebar,
|
|
SidebarContent,
|
|
SidebarFooter,
|
|
SidebarGroup,
|
|
SidebarGroupContent,
|
|
SidebarHeader,
|
|
SidebarMenu,
|
|
SidebarMenuButton,
|
|
SidebarMenuItem,
|
|
SidebarSeparator,
|
|
useSidebar,
|
|
} from "@/components/ui/sidebar";
|
|
import Avatar from "@/components/ui/avatar";
|
|
import LanguageSwitcher from "@/components/LanguageSwitcher";
|
|
import { useTranslation, type TranslationKey } from "@/lib/i18n";
|
|
import { useNotification } from "@/lib/notification-context";
|
|
import { useUser } from "@/lib/user-context";
|
|
import { clearToken } from "@/lib/auth";
|
|
|
|
const navItems: { href: string; labelKey: TranslationKey; icon: typeof ListTodo }[] = [
|
|
{ href: "/todos", labelKey: "navTodo", icon: ListTodo },
|
|
{ href: "/reminders", labelKey: "navReminder", icon: Bell },
|
|
{ href: "/invites", labelKey: "navInvites", icon: UserPlus },
|
|
{ href: "/settings", labelKey: "navSettings", icon: Settings },
|
|
{ href: "/notifications", labelKey: "navNotifications", icon: BellDot },
|
|
];
|
|
|
|
const AppSidebar = () => {
|
|
const pathname = usePathname();
|
|
const { unreadCount } = useNotification();
|
|
const { user } = useUser();
|
|
const t = useTranslation();
|
|
const { isMobile } = useSidebar();
|
|
|
|
if (isMobile) {
|
|
return (
|
|
<Sidebar>
|
|
<nav className="flex items-center justify-around px-1 py-1.5 safe-bottom">
|
|
{navItems.map((item) => {
|
|
const isActive = pathname?.startsWith(item.href);
|
|
const isNotifications = item.href === "/notifications";
|
|
return (
|
|
<Link
|
|
key={item.href}
|
|
href={item.href}
|
|
className={`relative flex flex-col items-center gap-0.5 rounded-lg px-3 py-1.5 text-[10px] font-medium transition-colors ${
|
|
isActive
|
|
? "text-primary"
|
|
: "text-muted-foreground"
|
|
}`}
|
|
>
|
|
<span className="relative">
|
|
<item.icon className="h-5 w-5" />
|
|
{isNotifications && unreadCount > 0 && (
|
|
<span className="absolute -right-1.5 -top-1 h-2 w-2 rounded-full bg-red-500" />
|
|
)}
|
|
</span>
|
|
<span>{t(item.labelKey)}</span>
|
|
{isActive && (
|
|
<span className="absolute -bottom-1 h-0.5 w-4 rounded-full bg-primary" />
|
|
)}
|
|
</Link>
|
|
);
|
|
})}
|
|
</nav>
|
|
</Sidebar>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Sidebar variant="inset" className="h-[calc(100vh-3rem)] self-start bg-white">
|
|
<SidebarHeader className="gap-2 px-3 py-4">
|
|
<div className="flex items-center gap-2">
|
|
<Image src="/notify_icon.png" alt="Notify" width={28} height={28} className="h-7 w-7 rounded-lg" />
|
|
<span className="text-[15px] font-semibold tracking-tight text-foreground group-data-[state=collapsed]/sidebar:hidden">
|
|
notify
|
|
</span>
|
|
</div>
|
|
</SidebarHeader>
|
|
<SidebarContent>
|
|
<SidebarGroup>
|
|
<SidebarGroupContent>
|
|
<SidebarMenu>
|
|
{navItems.map((item) => {
|
|
const isActive = pathname?.startsWith(item.href);
|
|
const isNotifications = item.href === "/notifications";
|
|
return (
|
|
<SidebarMenuItem key={item.href}>
|
|
<SidebarMenuButton
|
|
asChild
|
|
isActive={isActive}
|
|
className="group-data-[state=collapsed]/sidebar:justify-center group-data-[state=collapsed]/sidebar:px-2"
|
|
>
|
|
<Link href={item.href}>
|
|
<span className="relative">
|
|
<item.icon className={`h-[18px] w-[18px] ${isActive ? "text-sidebar-primary" : "text-muted-foreground"}`} />
|
|
{isNotifications && unreadCount > 0 && (
|
|
<span className="absolute -right-1 -top-1 hidden h-2 w-2 rounded-full bg-red-500 group-data-[state=collapsed]/sidebar:inline-flex" />
|
|
)}
|
|
</span>
|
|
<span className="group-data-[state=collapsed]/sidebar:hidden">
|
|
{t(item.labelKey)}
|
|
</span>
|
|
{isNotifications && unreadCount > 0 && (
|
|
<span className="ml-auto rounded-full bg-red-500 px-2 py-0.5 text-xs font-semibold text-white group-data-[state=collapsed]/sidebar:hidden">
|
|
{unreadCount}
|
|
</span>
|
|
)}
|
|
</Link>
|
|
</SidebarMenuButton>
|
|
</SidebarMenuItem>
|
|
);
|
|
})}
|
|
</SidebarMenu>
|
|
</SidebarGroupContent>
|
|
</SidebarGroup>
|
|
</SidebarContent>
|
|
<SidebarSeparator />
|
|
<SidebarFooter className="px-3 py-3">
|
|
<div className="flex items-center justify-between group-data-[state=collapsed]/sidebar:justify-center">
|
|
<div className="flex items-center gap-2 min-w-0">
|
|
<Avatar username={user?.username} src={user?.avatar} size="sm" />
|
|
<span className="truncate text-sm font-medium text-foreground/80 group-data-[state=collapsed]/sidebar:hidden">
|
|
{user?.username}
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center gap-1 group-data-[state=collapsed]/sidebar:hidden">
|
|
<LanguageSwitcher />
|
|
<button
|
|
type="button"
|
|
onClick={() => clearToken()}
|
|
className="inline-flex h-8 w-8 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
|
title={t("logout")}
|
|
>
|
|
<LogOut className="h-4 w-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</SidebarFooter>
|
|
</Sidebar>
|
|
);
|
|
};
|
|
|
|
export default AppSidebar;
|