Add initial project files
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
import { generalSettings } from '#app/config/general';
|
||||
import { Container } from '#shared/widgets/common/Container';
|
||||
|
||||
export function Footer() {
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
return (
|
||||
<footer className="footer">
|
||||
<Container>
|
||||
<div className="flex flex-col items-center justify-center gap-3 py-5 md:flex-row md:justify-between">
|
||||
<div className="order-2 flex gap-2 text-sm font-normal md:order-1">
|
||||
<span className="text-muted-foreground">{currentYear} ©</span>
|
||||
<a
|
||||
href="https://keenthemes.com"
|
||||
target="_blank"
|
||||
className="text-secondary-foreground hover:text-primary"
|
||||
>
|
||||
Keenthemes Inc.
|
||||
</a>
|
||||
</div>
|
||||
<nav className="text-muted-foreground order-1 flex gap-4 text-sm font-normal md:order-2">
|
||||
<a href={generalSettings.docsLink} target="_blank" className="hover:text-primary">
|
||||
Docs
|
||||
</a>
|
||||
<a href={generalSettings.purchaseLink} target="_blank" className="hover:text-primary">
|
||||
Purchase
|
||||
</a>
|
||||
<a href={generalSettings.faqLink} target="_blank" className="hover:text-primary">
|
||||
FAQ
|
||||
</a>
|
||||
<a href="https://devs.keenthemes.com" target="_blank" className="hover:text-primary">
|
||||
Support
|
||||
</a>
|
||||
<a href={generalSettings.licenseLink} target="_blank" className="hover:text-primary">
|
||||
License
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
</Container>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Menu } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { toAbsoluteUrl } from '#shared/lib/helpers';
|
||||
import { Button } from '#shared/ui/Button';
|
||||
import { Sheet, SheetBody, SheetContent, SheetHeader, SheetTrigger } from '#shared/ui/Sheet';
|
||||
import { Container } from '#shared/widgets/common/Container';
|
||||
import { SidebarPrimary } from './SidebarPrimary';
|
||||
import { SidebarSecondary } from './SidebarSecondary';
|
||||
|
||||
export function Header() {
|
||||
const [isSheetOpen, setIsSheetOpen] = useState(false);
|
||||
const { pathname } = useLocation();
|
||||
|
||||
// Close sheet when route changes
|
||||
useEffect(() => {
|
||||
setIsSheetOpen(false);
|
||||
}, [pathname]);
|
||||
|
||||
return (
|
||||
<header className="fixed start-0 end-0 top-0 z-10 flex h-(--header-height) shrink-0 items-center bg-(--page-bg) lg:hidden dark:bg-(--page-bg-dark)">
|
||||
<Container className="flex flex-wrap items-center justify-between gap-3">
|
||||
<Link to="/">
|
||||
<img
|
||||
src={toAbsoluteUrl('/media/app/mini-logo-gray.svg')}
|
||||
className="min-h-7.5 dark:hidden"
|
||||
alt=""
|
||||
/>
|
||||
<img
|
||||
src={toAbsoluteUrl('/media/app/mini-logo-gray-dark.svg')}
|
||||
className="hidden min-h-7.5 dark:block"
|
||||
alt=""
|
||||
/>
|
||||
</Link>
|
||||
|
||||
<Sheet open={isSheetOpen} onOpenChange={setIsSheetOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<Button variant="ghost" mode="icon" className="-ms-2 lg:hidden">
|
||||
<Menu />
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent className="w-(--sidebar-width) gap-0 p-0" side="left" close={false}>
|
||||
<SheetHeader className="space-y-0 p-0" />
|
||||
<SheetBody className="flex shrink-0 items-stretch overflow-y-auto p-0">
|
||||
<SidebarPrimary />
|
||||
<SidebarSecondary />
|
||||
</SheetBody>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</Container>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { SidebarPrimary } from './SidebarPrimary';
|
||||
import { SidebarSecondary } from './SidebarSecondary';
|
||||
|
||||
export function Sidebar() {
|
||||
return (
|
||||
<div className="bg-muted fixed top-0 bottom-0 z-20 flex w-(--sidebar-width) shrink-0 items-stretch">
|
||||
<SidebarPrimary />
|
||||
<SidebarSecondary />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
import { Badge, ChevronDown, FileText, Settings, SquareCode, UserCircle } from 'lucide-react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { AccordionMenu, AccordionMenuClassNames, AccordionMenuGroup, AccordionMenuItem, AccordionMenuLabel } from '#shared/ui/AccordionMenu';
|
||||
import { Button } from '#shared/ui/Button';
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '#shared/ui/DropdownMenu';
|
||||
|
||||
interface DropdownItem {
|
||||
title: string;
|
||||
path: string;
|
||||
icon: React.ComponentType<{ className?: string }>;
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
interface MenuItem {
|
||||
title: string;
|
||||
path?: string;
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
interface MenuLabel {
|
||||
label: string;
|
||||
}
|
||||
|
||||
type MenuNode = MenuItem | MenuLabel;
|
||||
|
||||
export function SidebarMenuDashboard() {
|
||||
const { pathname } = useLocation();
|
||||
|
||||
const dropdownItems: DropdownItem[] = [
|
||||
{
|
||||
title: 'Client API',
|
||||
path: '/account/home/user-profile',
|
||||
icon: SquareCode,
|
||||
active: true
|
||||
},
|
||||
{
|
||||
title: 'Profile',
|
||||
path: '/public-profile/profiles/company',
|
||||
icon: UserCircle
|
||||
},
|
||||
{
|
||||
title: 'My Account',
|
||||
path: '/account/integrations',
|
||||
icon: Settings
|
||||
},
|
||||
{
|
||||
title: 'Projects',
|
||||
path: '/public-profile/projects/3-columns',
|
||||
icon: FileText
|
||||
},
|
||||
{
|
||||
title: 'Personal info',
|
||||
path: '/public-profile/profiles/creator',
|
||||
icon: Badge
|
||||
}
|
||||
];
|
||||
|
||||
const currentDropdownItem = dropdownItems[0];
|
||||
|
||||
const menuItems = useMemo<MenuNode[]>(
|
||||
() => [
|
||||
{ label: 'Configuration' },
|
||||
{ title: 'API Setup', path: '/account/api-keys' },
|
||||
{ title: 'Team Settings', path: '/account/home/settings-sidebar' },
|
||||
{ title: 'Authentication', path: '/authentication/classic/sign-in' },
|
||||
{ title: 'Endpoints Configs', path: '/account/appearance' },
|
||||
{ title: 'Rate Limiting', path: '/public-profile/network' },
|
||||
{ label: 'Security' },
|
||||
{ title: 'Data Encryption', path: '/account/billing/enterprise' },
|
||||
{ title: 'Text', path: '/account/security/overview' },
|
||||
{ title: 'Access Control', path: '/account/security/privacy-settings' },
|
||||
{ label: 'Analytics' },
|
||||
{
|
||||
title: 'Incident Response',
|
||||
path: '/account/security/current-sessions'
|
||||
},
|
||||
{ title: 'Fetching Data', path: '/account/members/team-info' },
|
||||
{ title: 'Custom Reports', path: '/account/home/user-profile' },
|
||||
{
|
||||
title: 'Real Time Analytics',
|
||||
path: '/account/home/settings-enterprise'
|
||||
},
|
||||
{ title: 'Exporting Data', path: '/account/members/import-members' },
|
||||
{ title: 'Dashboard Integration', path: '/account/members/team-info' }
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
const classNames: AccordionMenuClassNames = {
|
||||
root: 'space-y-1',
|
||||
label: 'uppercase text-xs font-medium text-muted-foreground/80 pt-6 mb-2 pb-0',
|
||||
item: 'h-8 hover:bg-background border-accent text-accent-foreground hover:text-primary data-[selected=true]:text-primary data-[selected=true]:bg-background data-[selected=true]:font-medium'
|
||||
};
|
||||
|
||||
const matchPath = useCallback(
|
||||
(path: string): boolean => path === pathname || (path.length > 1 && pathname.startsWith(path)),
|
||||
[pathname]
|
||||
);
|
||||
|
||||
const buildDropdown = () => {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" mode="input" className="w-full justify-between">
|
||||
<span className="flex items-center gap-2">
|
||||
<currentDropdownItem.icon />
|
||||
{currentDropdownItem.title}
|
||||
</span>
|
||||
<ChevronDown className="size-3.5" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-(--radix-dropdown-menu-trigger-width)">
|
||||
{dropdownItems.map((item, index) => (
|
||||
<DropdownMenuItem key={index} asChild>
|
||||
<Link to={item.path}>
|
||||
<item.icon />
|
||||
<span>{item.title}</span>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
|
||||
const buildMenu = () => {
|
||||
return (
|
||||
<AccordionMenu
|
||||
selectedValue={'/account/home/settings-sidebar'}
|
||||
matchPath={matchPath}
|
||||
type="single"
|
||||
collapsible
|
||||
classNames={classNames}
|
||||
>
|
||||
<AccordionMenuGroup>
|
||||
{menuItems.map((item, index) =>
|
||||
'label' in item ? (
|
||||
<AccordionMenuLabel key={index}>{item.label}</AccordionMenuLabel>
|
||||
) : (
|
||||
<AccordionMenuItem
|
||||
key={index}
|
||||
value={item.path || `item-${index}`}
|
||||
className="text-sm"
|
||||
>
|
||||
<Link to={item.path || '#'}>{item.title}</Link>
|
||||
</AccordionMenuItem>
|
||||
)
|
||||
)}
|
||||
</AccordionMenuGroup>
|
||||
</AccordionMenu>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full space-y-1">
|
||||
{buildDropdown()}
|
||||
{buildMenu()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+183
@@ -0,0 +1,183 @@
|
||||
'use client';
|
||||
|
||||
import { JSX, useMemo } from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { cn } from '@crux-kit/utils';
|
||||
import { MenuConfig, MenuItem } from '#app/config/types';
|
||||
import { useMenuConfig } from '#app/menu';
|
||||
import { useMenu } from '#shared/hooks/useMenu';
|
||||
import { createMenuPathMatcher } from '#shared/lib/menuPath';
|
||||
import { AccordionMenu, AccordionMenuClassNames, AccordionMenuGroup, AccordionMenuItem, AccordionMenuLabel, AccordionMenuSub, AccordionMenuSubContent, AccordionMenuSubTrigger } from '#shared/ui/AccordionMenu';
|
||||
import { Badge } from '#shared/ui/Badge';
|
||||
|
||||
export function SidebarMenuDefault() {
|
||||
const { pathname } = useLocation();
|
||||
const fullMenuConfig = useMenuConfig();
|
||||
|
||||
// Memoize matchPath to prevent unnecessary re-renders
|
||||
const matchPath = useMemo(
|
||||
() => createMenuPathMatcher(fullMenuConfig, pathname),
|
||||
[fullMenuConfig, pathname]
|
||||
);
|
||||
|
||||
// Global classNames for consistent styling
|
||||
const classNames: AccordionMenuClassNames = {
|
||||
root: 'lg:ps-1 space-y-1',
|
||||
group: 'gap-px',
|
||||
label: 'uppercase text-xs font-medium text-muted-foreground/70 pt-2.25 pb-px',
|
||||
separator: '',
|
||||
item: 'h-8 hover:bg-background border-accent text-accent-foreground hover:text-primary data-[selected=true]:text-primary data-[selected=true]:bg-background data-[selected=true]:font-medium',
|
||||
sub: '',
|
||||
subTrigger:
|
||||
'h-8 hover:bg-transparent text-accent-foreground hover:text-primary data-[selected=true]:text-primary data-[selected=true]:bg-transparent data-[selected=true]:font-medium',
|
||||
subContent: 'ps-3 py-0',
|
||||
indicator: ''
|
||||
};
|
||||
|
||||
const buildMenu = (items: MenuConfig): JSX.Element[] => {
|
||||
return items.map((item: MenuItem, index: number) => {
|
||||
if (item.heading) {
|
||||
return buildMenuHeading(item, index);
|
||||
} else if (item.disabled) {
|
||||
return buildMenuItemRootDisabled(item, index);
|
||||
} else {
|
||||
return buildMenuItemRoot(item, index);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const buildMenuItemRoot = (item: MenuItem, index: number): JSX.Element => {
|
||||
if (item.children) {
|
||||
return (
|
||||
<AccordionMenuSub key={index} value={item.path || `root-${index}`}>
|
||||
<AccordionMenuSubTrigger className="text-sm">
|
||||
{item.icon && <item.icon data-slot="accordion-menu-icon" />}
|
||||
<span data-slot="accordion-menu-title">{item.title}</span>
|
||||
</AccordionMenuSubTrigger>
|
||||
<AccordionMenuSubContent
|
||||
type="single"
|
||||
collapsible
|
||||
parentValue={item.path || `root-${index}`}
|
||||
>
|
||||
<AccordionMenuGroup>{buildMenuItemChildren(item.children, 1)}</AccordionMenuGroup>
|
||||
</AccordionMenuSubContent>
|
||||
</AccordionMenuSub>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<AccordionMenuItem key={index} value={item.path || ''} className="text-sm">
|
||||
<Link to={item.path || '#'} className="flex grow items-center justify-between gap-2">
|
||||
{item.icon && <item.icon data-slot="accordion-menu-icon" />}
|
||||
<span data-slot="accordion-menu-title">{item.title}</span>
|
||||
</Link>
|
||||
</AccordionMenuItem>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const buildMenuItemRootDisabled = (item: MenuItem, index: number): JSX.Element => {
|
||||
return (
|
||||
<AccordionMenuItem key={index} value={`disabled-${index}`} className="text-sm font-medium">
|
||||
{item.icon && <item.icon data-slot="accordion-menu-icon" />}
|
||||
<span data-slot="accordion-menu-title">{item.title}</span>
|
||||
{item.disabled && (
|
||||
<Badge variant="secondary" size="sm" className="ms-auto -me-2.5">
|
||||
Soon
|
||||
</Badge>
|
||||
)}
|
||||
</AccordionMenuItem>
|
||||
);
|
||||
};
|
||||
|
||||
const buildMenuItemChildren = (items: MenuConfig, level: number = 0): JSX.Element[] => {
|
||||
return items.map((item: MenuItem, index: number) => {
|
||||
if (item.disabled) {
|
||||
return buildMenuItemChildDisabled(item, index, level);
|
||||
} else {
|
||||
return buildMenuItemChild(item, index, level);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const buildMenuItemChild = (item: MenuItem, index: number, level: number = 0): JSX.Element => {
|
||||
if (item.children) {
|
||||
return (
|
||||
<AccordionMenuSub key={index} value={item.path || `child-${level}-${index}`}>
|
||||
<AccordionMenuSubTrigger className="text-[13px]">
|
||||
{item.collapse ? (
|
||||
<span className="text-muted-foreground">
|
||||
<span className="hidden [[data-state=open]>span>&]:inline">
|
||||
{item.collapseTitle}
|
||||
</span>
|
||||
<span className="inline [[data-state=open]>span>&]:hidden">{item.expandTitle}</span>
|
||||
</span>
|
||||
) : (
|
||||
item.title
|
||||
)}
|
||||
</AccordionMenuSubTrigger>
|
||||
<AccordionMenuSubContent
|
||||
type="single"
|
||||
collapsible
|
||||
parentValue={item.path || `child-${level}-${index}`}
|
||||
className={cn(
|
||||
'ps-4',
|
||||
!item.collapse && 'relative',
|
||||
!item.collapse && (level > 0 ? '' : '')
|
||||
)}
|
||||
>
|
||||
<AccordionMenuGroup>
|
||||
{buildMenuItemChildren(item.children, item.collapse ? level : level + 1)}
|
||||
</AccordionMenuGroup>
|
||||
</AccordionMenuSubContent>
|
||||
</AccordionMenuSub>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<AccordionMenuItem key={index} value={item.path || ''} className="text-[13px]">
|
||||
<Link to={item.path || '#'}>{item.title}</Link>
|
||||
</AccordionMenuItem>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const buildMenuItemChildDisabled = (
|
||||
item: MenuItem,
|
||||
index: number,
|
||||
level: number = 0
|
||||
): JSX.Element => {
|
||||
return (
|
||||
<AccordionMenuItem
|
||||
key={index}
|
||||
value={`disabled-child-${level}-${index}`}
|
||||
className="text-[13px]"
|
||||
>
|
||||
<span data-slot="accordion-menu-title">{item.title}</span>
|
||||
{item.disabled && (
|
||||
<Badge variant="secondary" size="sm" className="ms-auto -me-2.5">
|
||||
Soon
|
||||
</Badge>
|
||||
)}
|
||||
</AccordionMenuItem>
|
||||
);
|
||||
};
|
||||
|
||||
const buildMenuHeading = (item: MenuItem, index: number): JSX.Element => {
|
||||
return <AccordionMenuLabel key={index}>{item.heading}</AccordionMenuLabel>;
|
||||
};
|
||||
|
||||
// Memoize the menu to prevent unnecessary re-renders
|
||||
const { getChildren } = useMenu(pathname);
|
||||
const menuItems = getChildren(fullMenuConfig, 0);
|
||||
|
||||
return (
|
||||
<AccordionMenu
|
||||
selectedValue={pathname}
|
||||
matchPath={matchPath}
|
||||
type="single"
|
||||
collapsible
|
||||
classNames={classNames}
|
||||
>
|
||||
{buildMenu(menuItems || [])}
|
||||
</AccordionMenu>
|
||||
);
|
||||
}
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
import { BarChart3, Bell, CheckSquare, Code, MessageSquare, Settings, Shield, ShoppingCart, UserCircle, Users } from 'lucide-react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { useViewport } from '@crux-kit/hooks';
|
||||
import { cn, getHeight } from '@crux-kit/utils';
|
||||
import { toAbsoluteUrl } from '#shared/lib/helpers';
|
||||
import { Button } from '#shared/ui/Button';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '#shared/ui/Tooltip';
|
||||
// import { AppsDropdownMenu } from '#shared/partials/topbar/apps-dropdown-menu';
|
||||
// import { ChatSheet } from '#shared/partials/topbar/chat-sheet';
|
||||
import { UserDropdownMenu } from '#shared/widgets/UserDropdownMenu';
|
||||
|
||||
interface MenuItem {
|
||||
icon: React.ComponentType<{ className?: string }>;
|
||||
tooltip: string;
|
||||
path: string;
|
||||
rootPath?: string;
|
||||
}
|
||||
|
||||
const menuItems: MenuItem[] = [
|
||||
{ icon: BarChart3, tooltip: 'Dashboard', path: '/', rootPath: '/' },
|
||||
{
|
||||
icon: UserCircle,
|
||||
tooltip: 'Profile',
|
||||
path: '/public-profile/profiles/default',
|
||||
rootPath: '/public-profile/'
|
||||
},
|
||||
{
|
||||
icon: Settings,
|
||||
tooltip: 'Account',
|
||||
path: '/account/home/get-started',
|
||||
rootPath: '/account/'
|
||||
},
|
||||
{
|
||||
icon: Users,
|
||||
tooltip: 'Network',
|
||||
path: '/network/get-started',
|
||||
rootPath: '/network/'
|
||||
},
|
||||
{
|
||||
icon: ShoppingCart,
|
||||
tooltip: 'Store - Client',
|
||||
path: '/store-client/home',
|
||||
rootPath: ''
|
||||
},
|
||||
{
|
||||
icon: Shield,
|
||||
tooltip: 'Authentication',
|
||||
path: '/authentication/get-started',
|
||||
rootPath: '/authentication/'
|
||||
},
|
||||
{
|
||||
icon: MessageSquare,
|
||||
tooltip: 'Security Logs',
|
||||
path: '/account/security/security-log',
|
||||
rootPath: '/account/'
|
||||
},
|
||||
{
|
||||
icon: Bell,
|
||||
tooltip: 'Notifications',
|
||||
path: '/account/notifications',
|
||||
rootPath: ''
|
||||
},
|
||||
{
|
||||
icon: CheckSquare,
|
||||
tooltip: 'ACL',
|
||||
path: '/account/members/roles',
|
||||
rootPath: ''
|
||||
},
|
||||
{ icon: Code, tooltip: 'API Keys', path: '/account/api-keys', rootPath: '' }
|
||||
];
|
||||
|
||||
export function SidebarPrimary() {
|
||||
const headerRef = useRef<HTMLDivElement>(null);
|
||||
const footerRef = useRef<HTMLDivElement>(null);
|
||||
const [scrollableHeight, setScrollableHeight] = useState<number>(0);
|
||||
const [viewportHeight] = useViewport();
|
||||
const scrollableOffset = 80;
|
||||
const { pathname } = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
if (headerRef.current && footerRef.current) {
|
||||
const headerHeight = getHeight(headerRef.current);
|
||||
const footerHeight = getHeight(footerRef.current);
|
||||
const availableHeight = viewportHeight - headerHeight - footerHeight - scrollableOffset;
|
||||
setScrollableHeight(availableHeight);
|
||||
} else {
|
||||
setScrollableHeight(viewportHeight);
|
||||
}
|
||||
}, [viewportHeight]);
|
||||
|
||||
const [selectedMenuItem, setSelectedMenuItem] = useState(menuItems[0]);
|
||||
|
||||
useEffect(() => {
|
||||
menuItems.forEach((item) => {
|
||||
if (item.rootPath === pathname || (item.rootPath && pathname.includes(item.rootPath))) {
|
||||
setSelectedMenuItem(item);
|
||||
}
|
||||
});
|
||||
}, [pathname]);
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<div className="border-input flex w-17.5 shrink-0 flex-col items-stretch gap-5 border-e py-5">
|
||||
<div ref={headerRef} className="hidden shrink-0 items-center justify-center lg:flex">
|
||||
<Link to="/">
|
||||
<img
|
||||
src={toAbsoluteUrl('/media/app/mini-logo-gray.svg')}
|
||||
className="min-h-7.5 dark:hidden"
|
||||
alt=""
|
||||
/>
|
||||
<img
|
||||
src={toAbsoluteUrl('/media/app/mini-logo-gray-dark.svg')}
|
||||
className="hidden min-h-7.5 dark:block"
|
||||
alt=""
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="flex shrink-0 grow">
|
||||
<div
|
||||
className="kt-scrollable-y-hover flex shrink-0 grow flex-col gap-2.5 ps-4"
|
||||
style={{ height: `${scrollableHeight}px` }}
|
||||
>
|
||||
{menuItems.map((item, index) => (
|
||||
<Tooltip key={index}>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
asChild
|
||||
variant="ghost"
|
||||
mode="icon"
|
||||
{...(item === selectedMenuItem ? { 'data-state': 'open' } : {})}
|
||||
className={cn(
|
||||
'size-9 shrink-0 rounded-md',
|
||||
'data-[state=open]:bg-background data-[state=open]:border-input data-[state=open]:text-primary data-[state=open]:border',
|
||||
'hover:bg-background hover:border-input hover:text-primary hover:border'
|
||||
)}
|
||||
>
|
||||
<Link to={item.path}>
|
||||
<item.icon className="size-4.5!" />
|
||||
</Link>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">{item.tooltip}</TooltipContent>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ref={footerRef} className="flex shrink-0 flex-col items-center gap-4">
|
||||
{/* <ChatSheet
|
||||
trigger={
|
||||
<Button
|
||||
variant="ghost"
|
||||
mode="icon"
|
||||
className="size-9 hover:bg-background hover:[&_svg]:text-primary"
|
||||
>
|
||||
<MessageCircleMore className="size-4.5!" />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<AppsDropdownMenu
|
||||
trigger={
|
||||
<Button
|
||||
variant="ghost"
|
||||
mode="icon"
|
||||
className="size-9 hover:bg-background hover:[&_svg]:text-primary"
|
||||
>
|
||||
<LayoutGrid className="size-4.5!" />
|
||||
</Button>
|
||||
}
|
||||
/> */}
|
||||
<UserDropdownMenu
|
||||
trigger={
|
||||
<img
|
||||
className="border-border size-9 shrink-0 cursor-pointer rounded-full border"
|
||||
src={toAbsoluteUrl('/media/avatars/300-2.png')}
|
||||
alt="User Avatar"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { useLocation } from 'react-router';
|
||||
import { SidebarMenuDashboard } from './SidebarMenuDashboard';
|
||||
import { SidebarMenuDefault } from './SidebarMenuDefault';
|
||||
|
||||
export function SidebarSecondary() {
|
||||
const { pathname } = useLocation();
|
||||
|
||||
return (
|
||||
<div className="kt-scrollable-y-hover my-5 max-h-[calc(100vh-2rem)] shrink-0 grow ps-3.5 pe-1">
|
||||
{pathname === '/' ? <SidebarMenuDashboard /> : <SidebarMenuDefault />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { Fragment, ReactNode } from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { cn } from '@crux-kit/utils';
|
||||
import { MenuItem } from '#app/config/types';
|
||||
import { useMenuConfig } from '#app/menu';
|
||||
import { useMenu } from '#shared/hooks/useMenu';
|
||||
import { Container } from '#shared/widgets/common/Container';
|
||||
|
||||
export interface ToolbarHeadingProps {
|
||||
title?: string | ReactNode;
|
||||
description?: string | ReactNode;
|
||||
}
|
||||
|
||||
function Toolbar({ children }: { children?: ReactNode }) {
|
||||
return (
|
||||
<div className="mb-5">
|
||||
<Container className="flex flex-wrap items-center justify-between gap-5">
|
||||
{children}
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ToolbarActions({ children }: { children?: ReactNode }) {
|
||||
return <div className="flex items-center gap-2.5">{children}</div>;
|
||||
}
|
||||
|
||||
function ToolbarBreadcrumbs() {
|
||||
const { pathname } = useLocation();
|
||||
const menuConfig = useMenuConfig();
|
||||
const { getBreadcrumb, isActive } = useMenu(pathname);
|
||||
const items: MenuItem[] = getBreadcrumb(menuConfig);
|
||||
|
||||
if (items.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1 text-sm">
|
||||
{items.map((item, index) => {
|
||||
const isLast = index === items.length - 1;
|
||||
const active = item.path ? isActive(item.path) : false;
|
||||
|
||||
return (
|
||||
<Fragment key={index}>
|
||||
{item.path ? (
|
||||
<Link
|
||||
to={item.path}
|
||||
className={cn(
|
||||
'flex items-center gap-1',
|
||||
active ? 'text-mono' : 'text-secondary-foreground hover:text-primary'
|
||||
)}
|
||||
>
|
||||
{item.title}
|
||||
</Link>
|
||||
) : (
|
||||
<span className={cn(isLast ? 'text-mono' : 'text-secondary-foreground')}>
|
||||
{item.title}
|
||||
</span>
|
||||
)}
|
||||
{!isLast && <span className="text-secondary-foreground">/</span>}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const ToolbarHeading = ({ title = '' }: ToolbarHeadingProps) => {
|
||||
const { pathname } = useLocation();
|
||||
const { getCurrentItem } = useMenu(pathname);
|
||||
const menuConfig = useMenuConfig();
|
||||
const item = getCurrentItem(menuConfig);
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-1 lg:gap-5">
|
||||
<h1 className="text-mono text-lg font-medium">{title || item?.title}</h1>
|
||||
<ToolbarBreadcrumbs />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { Toolbar, ToolbarActions, ToolbarBreadcrumbs, ToolbarHeading };
|
||||
Reference in New Issue
Block a user