Add initial project files

This commit is contained in:
Igor Martyniuk
2026-09-01 13:03:41 +03:00
parent 22e111bb44
commit 74cb5b6176
1841 changed files with 242819 additions and 30 deletions
@@ -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} &copy;</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>
);
}
@@ -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>
);
}
@@ -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>
);
}
@@ -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>
);
}
@@ -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 };