frontend initiatewith shadcn uis
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { HugeiconsIcon } from '@hugeicons/react'
|
||||
import { ChevronRightIcon, UserIcon } from '@hugeicons/core-free-icons'
|
||||
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarGroup,
|
||||
SidebarHeader,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarMenuSub,
|
||||
SidebarMenuSubButton,
|
||||
SidebarMenuSubItem,
|
||||
SidebarRail,
|
||||
} from '@/components/ui/sidebar'
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from '@/components/ui/collapsible'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { useAuth, type Role } from '@/contexts/auth-context'
|
||||
import { DASHBOARD_ITEM, NAV_GROUPS, type NavGroup, type NavItem } from '@/config/nav'
|
||||
|
||||
function canSee(item: NavItem, permissions: string[]): boolean {
|
||||
if (!item.permission) return true
|
||||
return permissions.includes('*') || permissions.includes(item.permission)
|
||||
}
|
||||
|
||||
function isGroupVisible(group: NavGroup, permissions: string[]): boolean {
|
||||
return group.items.some((item) => canSee(item, permissions))
|
||||
}
|
||||
|
||||
export function AppSidebar() {
|
||||
const { user, setRole, permissions } = useAuth()
|
||||
const pathname = usePathname()
|
||||
|
||||
const dashboardActive = pathname === DASHBOARD_ITEM.href
|
||||
|
||||
return (
|
||||
<Sidebar collapsible="icon">
|
||||
<SidebarHeader>
|
||||
<div className="flex items-center gap-2 px-2 py-1">
|
||||
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-primary text-primary-foreground text-sm font-bold">
|
||||
D
|
||||
</div>
|
||||
<span className="truncate font-semibold text-sidebar-foreground group-data-[collapsible=icon]:hidden">
|
||||
Distribution
|
||||
</span>
|
||||
</div>
|
||||
</SidebarHeader>
|
||||
|
||||
<SidebarContent className="gap-0">
|
||||
{/* Dashboard — standalone, not inside a collapsible group */}
|
||||
<SidebarGroup className="py-1">
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
render={<Link href={DASHBOARD_ITEM.href} />}
|
||||
isActive={dashboardActive}
|
||||
tooltip={DASHBOARD_ITEM.title}
|
||||
>
|
||||
<HugeiconsIcon icon={DASHBOARD_ITEM.icon} size={18} />
|
||||
<span>{DASHBOARD_ITEM.title}</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
|
||||
{/* Collapsible groups */}
|
||||
{NAV_GROUPS.map((group) => {
|
||||
if (!isGroupVisible(group, permissions)) return null
|
||||
|
||||
const visibleItems = group.items.filter((item) =>
|
||||
canSee(item, permissions),
|
||||
)
|
||||
const groupActive = visibleItems.some(
|
||||
(item) =>
|
||||
pathname === item.href ||
|
||||
(item.href !== '/' && pathname.startsWith(item.href)),
|
||||
)
|
||||
|
||||
return (
|
||||
<Collapsible
|
||||
key={group.title}
|
||||
defaultOpen={false}
|
||||
className="group/collapsible"
|
||||
>
|
||||
<SidebarGroup className="py-0">
|
||||
<SidebarMenu>
|
||||
{/* Group trigger — looks like a regular nav button */}
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
render={<CollapsibleTrigger />}
|
||||
isActive={groupActive}
|
||||
tooltip={group.title}
|
||||
>
|
||||
<HugeiconsIcon icon={group.icon} size={18} />
|
||||
<span>{group.title}</span>
|
||||
<HugeiconsIcon
|
||||
icon={ChevronRightIcon}
|
||||
size={14}
|
||||
className="ml-auto shrink-0 transition-transform duration-200 group-data-[open]/collapsible:rotate-90 group-data-[collapsible=icon]:hidden"
|
||||
/>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
|
||||
{/* Sub-items with indent line */}
|
||||
<CollapsibleContent>
|
||||
<SidebarMenuSub>
|
||||
{visibleItems.map((item) => {
|
||||
const isActive =
|
||||
pathname === item.href ||
|
||||
(item.href !== '/' &&
|
||||
pathname.startsWith(item.href))
|
||||
return (
|
||||
<SidebarMenuSubItem key={item.href}>
|
||||
<SidebarMenuSubButton
|
||||
render={<Link href={item.href} />}
|
||||
isActive={isActive}
|
||||
>
|
||||
<HugeiconsIcon icon={item.icon} size={15} />
|
||||
<span>{item.title}</span>
|
||||
</SidebarMenuSubButton>
|
||||
</SidebarMenuSubItem>
|
||||
)
|
||||
})}
|
||||
</SidebarMenuSub>
|
||||
</CollapsibleContent>
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
</Collapsible>
|
||||
)
|
||||
})}
|
||||
</SidebarContent>
|
||||
|
||||
<SidebarFooter>
|
||||
<div className="flex items-center gap-2 px-2 py-1 group-data-[collapsible=icon]:justify-center">
|
||||
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-muted text-muted-foreground">
|
||||
<HugeiconsIcon icon={UserIcon} size={16} />
|
||||
</div>
|
||||
<div className="flex min-w-0 flex-1 flex-col group-data-[collapsible=icon]:hidden">
|
||||
<span className="truncate text-sm font-medium text-sidebar-foreground">
|
||||
{user.fullName}
|
||||
</span>
|
||||
<Select value={user.role} onValueChange={(v) => setRole(v as Role)}>
|
||||
<SelectTrigger className="h-5 border-0 p-0 text-xs text-muted-foreground shadow-none focus:ring-0">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="admin">Admin</SelectItem>
|
||||
<SelectItem value="warehouse">Warehouse</SelectItem>
|
||||
<SelectItem value="sales">Sales</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</SidebarFooter>
|
||||
|
||||
<SidebarRail />
|
||||
</Sidebar>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { HugeiconsIcon } from '@hugeicons/react'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useAuth } from '@/contexts/auth-context'
|
||||
import { BOTTOM_NAV_CANDIDATES, type NavItem } from '@/config/nav'
|
||||
|
||||
const MAX_TABS = 5
|
||||
|
||||
export function BottomNav() {
|
||||
const pathname = usePathname()
|
||||
const { permissions } = useAuth()
|
||||
|
||||
const visibleItems: NavItem[] = BOTTOM_NAV_CANDIDATES.filter((item) => {
|
||||
if (!item.permission) return true
|
||||
return permissions.includes('*') || permissions.includes(item.permission)
|
||||
}).slice(0, MAX_TABS)
|
||||
|
||||
return (
|
||||
<nav className="fixed bottom-0 left-0 right-0 z-50 border-t border-border bg-background md:hidden">
|
||||
<div className="flex h-16 items-stretch">
|
||||
{visibleItems.map((item) => {
|
||||
const isActive =
|
||||
pathname === item.href ||
|
||||
(item.href !== '/' && pathname.startsWith(item.href))
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className={cn(
|
||||
'flex flex-1 flex-col items-center justify-center gap-1 text-xs transition-colors',
|
||||
isActive
|
||||
? 'text-primary'
|
||||
: 'text-muted-foreground hover:text-foreground',
|
||||
)}
|
||||
>
|
||||
<HugeiconsIcon
|
||||
icon={item.icon}
|
||||
size={22}
|
||||
className={isActive ? 'text-primary' : undefined}
|
||||
/>
|
||||
<span className="truncate">{item.title}</span>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user