create-app-ui 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +117 -0
- package/boilerplate/README.md +18 -0
- package/boilerplate/react-base/.env.example +1 -0
- package/boilerplate/react-base/README.md +3 -0
- package/boilerplate/react-base/components.json +19 -0
- package/boilerplate/react-base/eslint.config.js +32 -0
- package/boilerplate/react-base/index.html +12 -0
- package/boilerplate/react-base/package.json +71 -0
- package/boilerplate/react-base/postcss.config.js +6 -0
- package/boilerplate/react-base/prettier.config.js +6 -0
- package/boilerplate/react-base/src/api/axios.ts +20 -0
- package/boilerplate/react-base/src/app/store.ts +13 -0
- package/boilerplate/react-base/src/components/data-table.tsx +919 -0
- package/boilerplate/react-base/src/components/ui/accordion.tsx +44 -0
- package/boilerplate/react-base/src/components/ui/alert-dialog.tsx +105 -0
- package/boilerplate/react-base/src/components/ui/alert.tsx +40 -0
- package/boilerplate/react-base/src/components/ui/avatar.tsx +30 -0
- package/boilerplate/react-base/src/components/ui/badge.tsx +27 -0
- package/boilerplate/react-base/src/components/ui/bar-chart.tsx +76 -0
- package/boilerplate/react-base/src/components/ui/breadcrumb.tsx +87 -0
- package/boilerplate/react-base/src/components/ui/button.tsx +34 -0
- package/boilerplate/react-base/src/components/ui/calendar.tsx +63 -0
- package/boilerplate/react-base/src/components/ui/card.tsx +36 -0
- package/boilerplate/react-base/src/components/ui/chart.tsx +280 -0
- package/boilerplate/react-base/src/components/ui/checkbox.tsx +51 -0
- package/boilerplate/react-base/src/components/ui/context-menu.tsx +173 -0
- package/boilerplate/react-base/src/components/ui/date-picker.tsx +42 -0
- package/boilerplate/react-base/src/components/ui/dialog.tsx +87 -0
- package/boilerplate/react-base/src/components/ui/drawer.tsx +81 -0
- package/boilerplate/react-base/src/components/ui/dropdown-menu.tsx +81 -0
- package/boilerplate/react-base/src/components/ui/dropdown-types.ts +28 -0
- package/boilerplate/react-base/src/components/ui/field.tsx +194 -0
- package/boilerplate/react-base/src/components/ui/hover-card.tsx +26 -0
- package/boilerplate/react-base/src/components/ui/input-group.tsx +98 -0
- package/boilerplate/react-base/src/components/ui/input-otp.tsx +63 -0
- package/boilerplate/react-base/src/components/ui/input.tsx +12 -0
- package/boilerplate/react-base/src/components/ui/item.tsx +152 -0
- package/boilerplate/react-base/src/components/ui/kbd.tsx +13 -0
- package/boilerplate/react-base/src/components/ui/label.tsx +14 -0
- package/boilerplate/react-base/src/components/ui/line-chart.tsx +65 -0
- package/boilerplate/react-base/src/components/ui/menubar.tsx +217 -0
- package/boilerplate/react-base/src/components/ui/multi-select-dropdown.tsx +200 -0
- package/boilerplate/react-base/src/components/ui/navigation-menu.tsx +120 -0
- package/boilerplate/react-base/src/components/ui/pie-chart.tsx +87 -0
- package/boilerplate/react-base/src/components/ui/popover.tsx +29 -0
- package/boilerplate/react-base/src/components/ui/progress.tsx +19 -0
- package/boilerplate/react-base/src/components/ui/radio-group.tsx +36 -0
- package/boilerplate/react-base/src/components/ui/scroll-area.tsx +38 -0
- package/boilerplate/react-base/src/components/ui/searchable-dropdown.tsx +118 -0
- package/boilerplate/react-base/src/components/ui/select.tsx +140 -0
- package/boilerplate/react-base/src/components/ui/separator.tsx +20 -0
- package/boilerplate/react-base/src/components/ui/sheet.tsx +70 -0
- package/boilerplate/react-base/src/components/ui/sidebar.tsx +470 -0
- package/boilerplate/react-base/src/components/ui/skeleton.tsx +11 -0
- package/boilerplate/react-base/src/components/ui/slider.tsx +23 -0
- package/boilerplate/react-base/src/components/ui/sonner.tsx +21 -0
- package/boilerplate/react-base/src/components/ui/sparkline.tsx +38 -0
- package/boilerplate/react-base/src/components/ui/spinner.tsx +10 -0
- package/boilerplate/react-base/src/components/ui/switch.tsx +16 -0
- package/boilerplate/react-base/src/components/ui/table.tsx +80 -0
- package/boilerplate/react-base/src/components/ui/tabs.tsx +32 -0
- package/boilerplate/react-base/src/components/ui/textarea.tsx +12 -0
- package/boilerplate/react-base/src/components/ui/toggle-group.tsx +49 -0
- package/boilerplate/react-base/src/components/ui/toggle.tsx +33 -0
- package/boilerplate/react-base/src/components/ui/tooltip.tsx +23 -0
- package/boilerplate/react-base/src/components/ui/typography.tsx +76 -0
- package/boilerplate/react-base/src/config/constants.ts +3 -0
- package/boilerplate/react-base/src/config/theme.ts +432 -0
- package/boilerplate/react-base/src/config/user.ts +52 -0
- package/boilerplate/react-base/src/context/theme-provider.tsx +12 -0
- package/boilerplate/react-base/src/features/auth/authSlice.ts +19 -0
- package/boilerplate/react-base/src/hooks/index.ts +1 -0
- package/boilerplate/react-base/src/hooks/use-mobile.ts +17 -0
- package/boilerplate/react-base/src/lib/utils.ts +6 -0
- package/boilerplate/react-base/src/routes/index.tsx +7 -0
- package/boilerplate/react-base/src/styles/globals.css +15 -0
- package/boilerplate/react-base/src/vite-env.d.ts +31 -0
- package/boilerplate/react-base/tailwind.config.ts +75 -0
- package/boilerplate/react-base/tsconfig.app.json +20 -0
- package/boilerplate/react-base/tsconfig.json +7 -0
- package/boilerplate/react-base/tsconfig.node.json +16 -0
- package/boilerplate/react-base/vite.config.ts +12 -0
- package/dist/bin/index.js +8 -0
- package/dist/src/cli-args.js +52 -0
- package/dist/src/generator.js +85 -0
- package/dist/src/installer.js +7 -0
- package/dist/src/paths.js +61 -0
- package/dist/src/prompts.js +79 -0
- package/dist/src/replace-placeholders.js +22 -0
- package/dist/src/utils.js +16 -0
- package/package.json +63 -0
- package/templates/admin-portal/README.md +26 -0
- package/templates/admin-portal/src/App.tsx +85 -0
- package/templates/admin-portal/src/assets/auth-hero.jpg +0 -0
- package/templates/admin-portal/src/assets/brand-logo.png +0 -0
- package/templates/admin-portal/src/components/app-breadcrumb.tsx +41 -0
- package/templates/admin-portal/src/components/app-header.tsx +20 -0
- package/templates/admin-portal/src/components/app-sidebar.tsx +78 -0
- package/templates/admin-portal/src/components/auth-layout.tsx +66 -0
- package/templates/admin-portal/src/components/dashboard-metric-card.tsx +105 -0
- package/templates/admin-portal/src/components/data-table.tsx +919 -0
- package/templates/admin-portal/src/components/layout-shell.tsx +23 -0
- package/templates/admin-portal/src/components/notifications-sheet.tsx +91 -0
- package/templates/admin-portal/src/components/sidebar-nav.tsx +164 -0
- package/templates/admin-portal/src/components/user-avatar.tsx +26 -0
- package/templates/admin-portal/src/components/user-menu.tsx +163 -0
- package/templates/admin-portal/src/config/branding.ts +17 -0
- package/templates/admin-portal/src/config/chart-data.ts +44 -0
- package/templates/admin-portal/src/config/navigation.ts +42 -0
- package/templates/admin-portal/src/context/auth-context.tsx +32 -0
- package/templates/admin-portal/src/lib/breadcrumbs.ts +58 -0
- package/templates/admin-portal/src/main.tsx +18 -0
- package/templates/admin-portal/src/pages/components/demo-columns.tsx +170 -0
- package/templates/admin-portal/src/pages/components.tsx +1368 -0
- package/templates/admin-portal/src/pages/dashboard.tsx +143 -0
- package/templates/admin-portal/src/pages/login.tsx +81 -0
- package/templates/admin-portal/src/pages/settings/notifications.tsx +31 -0
- package/templates/admin-portal/src/pages/settings/profile.tsx +26 -0
- package/templates/admin-portal/src/pages/signup.tsx +81 -0
- package/templates/admin-portal/src/pages/users.tsx +12 -0
- package/templates/admin-portal/tsconfig.json +10 -0
- package/templates/blank/README.md +15 -0
- package/templates/blank/src/App.tsx +5 -0
- package/templates/blank/src/main.tsx +15 -0
- package/templates/blank/src/pages/home.tsx +20 -0
- package/templates/blank/tsconfig.json +10 -0
- package/templates/tsconfig.overlay.base.json +7 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
import { AppBreadcrumb } from "@/components/app-breadcrumb";
|
|
3
|
+
import { AppHeader } from "@/components/app-header";
|
|
4
|
+
import { AppSidebar } from "@/components/app-sidebar";
|
|
5
|
+
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
|
|
6
|
+
import { TooltipProvider } from "@/components/ui/tooltip";
|
|
7
|
+
|
|
8
|
+
export function LayoutShell({ children }: { children: ReactNode }) {
|
|
9
|
+
return (
|
|
10
|
+
<TooltipProvider delayDuration={200}>
|
|
11
|
+
<SidebarProvider defaultOpen>
|
|
12
|
+
<AppSidebar />
|
|
13
|
+
<SidebarInset>
|
|
14
|
+
<AppHeader />
|
|
15
|
+
<div className="shrink-0 border-b bg-background px-4 py-3 sm:px-6">
|
|
16
|
+
<AppBreadcrumb />
|
|
17
|
+
</div>
|
|
18
|
+
<div className="min-h-0 flex-1 overflow-y-auto p-4 sm:p-6">{children}</div>
|
|
19
|
+
</SidebarInset>
|
|
20
|
+
</SidebarProvider>
|
|
21
|
+
</TooltipProvider>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { Bell, CheckCheck } from "lucide-react";
|
|
2
|
+
import { useMemo, useState } from "react";
|
|
3
|
+
import { Badge } from "@/components/ui/badge";
|
|
4
|
+
import { Button } from "@/components/ui/button";
|
|
5
|
+
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
6
|
+
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet";
|
|
7
|
+
import { MOCK_NOTIFICATIONS, type NotificationItem } from "@/config/user";
|
|
8
|
+
import { cn } from "@/lib/utils";
|
|
9
|
+
|
|
10
|
+
function NotificationRow({
|
|
11
|
+
item,
|
|
12
|
+
onMarkRead,
|
|
13
|
+
}: {
|
|
14
|
+
item: NotificationItem;
|
|
15
|
+
onMarkRead: (id: string) => void;
|
|
16
|
+
}) {
|
|
17
|
+
return (
|
|
18
|
+
<button
|
|
19
|
+
type="button"
|
|
20
|
+
onClick={() => onMarkRead(item.id)}
|
|
21
|
+
className={cn(
|
|
22
|
+
"flex w-full flex-col gap-1 rounded-lg border p-3 text-left transition-colors hover:bg-muted/60",
|
|
23
|
+
!item.read && "border-primary/20 bg-primary/5",
|
|
24
|
+
)}
|
|
25
|
+
>
|
|
26
|
+
<div className="flex items-start justify-between gap-2">
|
|
27
|
+
<p className="text-sm font-medium leading-snug">{item.title}</p>
|
|
28
|
+
{!item.read && <span className="mt-1 h-2 w-2 shrink-0 rounded-full bg-primary" />}
|
|
29
|
+
</div>
|
|
30
|
+
<p className="text-sm text-muted-foreground">{item.description}</p>
|
|
31
|
+
<p className="text-xs text-muted-foreground">{item.time}</p>
|
|
32
|
+
</button>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function NotificationsSheet() {
|
|
37
|
+
const [open, setOpen] = useState(false);
|
|
38
|
+
const [notifications, setNotifications] = useState(MOCK_NOTIFICATIONS);
|
|
39
|
+
|
|
40
|
+
const unreadCount = useMemo(() => notifications.filter((item) => !item.read).length, [notifications]);
|
|
41
|
+
|
|
42
|
+
const markRead = (id: string) => {
|
|
43
|
+
setNotifications((current) =>
|
|
44
|
+
current.map((item) => (item.id === id ? { ...item, read: true } : item)),
|
|
45
|
+
);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const markAllRead = () => {
|
|
49
|
+
setNotifications((current) => current.map((item) => ({ ...item, read: true })));
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<Sheet open={open} onOpenChange={setOpen}>
|
|
54
|
+
<SheetTrigger asChild>
|
|
55
|
+
<Button variant="outline" size="sm" className="relative h-9 w-9 p-0" aria-label="Open notifications">
|
|
56
|
+
<Bell className="h-4 w-4" />
|
|
57
|
+
{unreadCount > 0 && (
|
|
58
|
+
<Badge className="absolute -right-1 -top-1 flex h-5 min-w-5 items-center justify-center rounded-full px-1 text-[10px]">
|
|
59
|
+
{unreadCount > 9 ? "9+" : unreadCount}
|
|
60
|
+
</Badge>
|
|
61
|
+
)}
|
|
62
|
+
</Button>
|
|
63
|
+
</SheetTrigger>
|
|
64
|
+
<SheetContent side="right" className="flex h-full w-full flex-col gap-0 p-0 sm:max-w-md">
|
|
65
|
+
<SheetHeader className="border-b px-6 py-4 text-left">
|
|
66
|
+
<div className="flex items-center justify-between gap-2">
|
|
67
|
+
<div>
|
|
68
|
+
<SheetTitle>Notifications</SheetTitle>
|
|
69
|
+
<SheetDescription>
|
|
70
|
+
{unreadCount > 0 ? `${unreadCount} unread` : "You're all caught up"}
|
|
71
|
+
</SheetDescription>
|
|
72
|
+
</div>
|
|
73
|
+
{unreadCount > 0 && (
|
|
74
|
+
<Button variant="outline" size="sm" className="h-8 shrink-0" onClick={markAllRead}>
|
|
75
|
+
<CheckCheck className="mr-1.5 h-4 w-4" />
|
|
76
|
+
Mark all read
|
|
77
|
+
</Button>
|
|
78
|
+
)}
|
|
79
|
+
</div>
|
|
80
|
+
</SheetHeader>
|
|
81
|
+
<ScrollArea className="min-h-0 flex-1 px-6 py-4">
|
|
82
|
+
<div className="space-y-3 pr-3">
|
|
83
|
+
{notifications.map((item) => (
|
|
84
|
+
<NotificationRow key={item.id} item={item} onMarkRead={markRead} />
|
|
85
|
+
))}
|
|
86
|
+
</div>
|
|
87
|
+
</ScrollArea>
|
|
88
|
+
</SheetContent>
|
|
89
|
+
</Sheet>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { ChevronRight } from "lucide-react";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { NavLink, useLocation } from "react-router-dom";
|
|
4
|
+
import {
|
|
5
|
+
DropdownMenu,
|
|
6
|
+
DropdownMenuContent,
|
|
7
|
+
DropdownMenuItem,
|
|
8
|
+
DropdownMenuTrigger,
|
|
9
|
+
} from "@/components/ui/dropdown-menu";
|
|
10
|
+
import {
|
|
11
|
+
SidebarMenu,
|
|
12
|
+
SidebarMenuButton,
|
|
13
|
+
SidebarMenuCollapsible,
|
|
14
|
+
SidebarMenuItem,
|
|
15
|
+
SidebarMenuSub,
|
|
16
|
+
SidebarMenuSubButton,
|
|
17
|
+
SidebarMenuSubItem,
|
|
18
|
+
useSidebar,
|
|
19
|
+
} from "@/components/ui/sidebar";
|
|
20
|
+
import { isNavGroup, type NavGroupConfig, type NavItemConfig, type NavLinkConfig } from "@/config/navigation";
|
|
21
|
+
import { THEME } from "@/config/theme";
|
|
22
|
+
import { cn } from "@/lib/utils";
|
|
23
|
+
|
|
24
|
+
function navItemActiveClass(isActive: boolean) {
|
|
25
|
+
return cn(isActive && THEME.classes.navActive);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function NavLinkItem({
|
|
29
|
+
item,
|
|
30
|
+
onNavigate,
|
|
31
|
+
}: {
|
|
32
|
+
item: NavLinkConfig;
|
|
33
|
+
onNavigate?: () => void;
|
|
34
|
+
}) {
|
|
35
|
+
const location = useLocation();
|
|
36
|
+
const Icon = item.icon;
|
|
37
|
+
const isActive = location.pathname === item.to;
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<SidebarMenuItem>
|
|
41
|
+
<SidebarMenuButton asChild isActive={isActive} tooltip={item.label}>
|
|
42
|
+
<NavLink to={item.to} onClick={onNavigate} className={navItemActiveClass(isActive)}>
|
|
43
|
+
<Icon />
|
|
44
|
+
<span>{item.label}</span>
|
|
45
|
+
</NavLink>
|
|
46
|
+
</SidebarMenuButton>
|
|
47
|
+
</SidebarMenuItem>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function NavGroupItem({
|
|
52
|
+
item,
|
|
53
|
+
onNavigate,
|
|
54
|
+
}: {
|
|
55
|
+
item: NavGroupConfig;
|
|
56
|
+
onNavigate?: () => void;
|
|
57
|
+
}) {
|
|
58
|
+
const location = useLocation();
|
|
59
|
+
const { state: sidebarState } = useSidebar();
|
|
60
|
+
const Icon = item.icon;
|
|
61
|
+
const isGroupActive = location.pathname === item.basePath || location.pathname.startsWith(`${item.basePath}/`);
|
|
62
|
+
const [open, setOpen] = useState(isGroupActive);
|
|
63
|
+
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (isGroupActive) {
|
|
66
|
+
setOpen(true);
|
|
67
|
+
}
|
|
68
|
+
}, [isGroupActive]);
|
|
69
|
+
|
|
70
|
+
if (sidebarState === "collapsed") {
|
|
71
|
+
return (
|
|
72
|
+
<SidebarMenuItem>
|
|
73
|
+
<DropdownMenu>
|
|
74
|
+
<DropdownMenuTrigger asChild>
|
|
75
|
+
<SidebarMenuButton
|
|
76
|
+
isActive={isGroupActive}
|
|
77
|
+
tooltip={item.label}
|
|
78
|
+
className={navItemActiveClass(isGroupActive)}
|
|
79
|
+
>
|
|
80
|
+
<Icon />
|
|
81
|
+
<span>{item.label}</span>
|
|
82
|
+
<ChevronRight className="ml-auto h-4 w-4" />
|
|
83
|
+
</SidebarMenuButton>
|
|
84
|
+
</DropdownMenuTrigger>
|
|
85
|
+
<DropdownMenuContent side="right" align="start" className="min-w-44">
|
|
86
|
+
{item.children.map((child) => {
|
|
87
|
+
const isChildActive = location.pathname === child.to;
|
|
88
|
+
return (
|
|
89
|
+
<DropdownMenuItem key={child.to} asChild>
|
|
90
|
+
<NavLink
|
|
91
|
+
to={child.to}
|
|
92
|
+
onClick={onNavigate}
|
|
93
|
+
className={cn(
|
|
94
|
+
"w-full cursor-pointer",
|
|
95
|
+
isChildActive && THEME.classes.navActive,
|
|
96
|
+
)}
|
|
97
|
+
>
|
|
98
|
+
{child.label}
|
|
99
|
+
</NavLink>
|
|
100
|
+
</DropdownMenuItem>
|
|
101
|
+
);
|
|
102
|
+
})}
|
|
103
|
+
</DropdownMenuContent>
|
|
104
|
+
</DropdownMenu>
|
|
105
|
+
</SidebarMenuItem>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<SidebarMenuItem>
|
|
111
|
+
<SidebarMenuButton
|
|
112
|
+
isActive={isGroupActive}
|
|
113
|
+
tooltip={item.label}
|
|
114
|
+
className={navItemActiveClass(isGroupActive)}
|
|
115
|
+
onClick={() => setOpen((current) => !current)}
|
|
116
|
+
aria-expanded={open}
|
|
117
|
+
>
|
|
118
|
+
<Icon />
|
|
119
|
+
<span>{item.label}</span>
|
|
120
|
+
<ChevronRight className={cn("ml-auto h-4 w-4 shrink-0 transition-transform duration-200", open && "rotate-90")} />
|
|
121
|
+
</SidebarMenuButton>
|
|
122
|
+
<SidebarMenuCollapsible open={open}>
|
|
123
|
+
<SidebarMenuSub>
|
|
124
|
+
{item.children.map((child) => {
|
|
125
|
+
const isActive = location.pathname === child.to;
|
|
126
|
+
return (
|
|
127
|
+
<SidebarMenuSubItem key={child.to}>
|
|
128
|
+
<SidebarMenuSubButton asChild isActive={isActive}>
|
|
129
|
+
<NavLink
|
|
130
|
+
to={child.to}
|
|
131
|
+
onClick={onNavigate}
|
|
132
|
+
className={navItemActiveClass(isActive)}
|
|
133
|
+
>
|
|
134
|
+
<span>{child.label}</span>
|
|
135
|
+
</NavLink>
|
|
136
|
+
</SidebarMenuSubButton>
|
|
137
|
+
</SidebarMenuSubItem>
|
|
138
|
+
);
|
|
139
|
+
})}
|
|
140
|
+
</SidebarMenuSub>
|
|
141
|
+
</SidebarMenuCollapsible>
|
|
142
|
+
</SidebarMenuItem>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function SidebarNavMenu({
|
|
147
|
+
items,
|
|
148
|
+
onNavigate,
|
|
149
|
+
}: {
|
|
150
|
+
items: NavItemConfig[];
|
|
151
|
+
onNavigate?: () => void;
|
|
152
|
+
}) {
|
|
153
|
+
return (
|
|
154
|
+
<SidebarMenu>
|
|
155
|
+
{items.map((item) =>
|
|
156
|
+
isNavGroup(item) ? (
|
|
157
|
+
<NavGroupItem key={item.basePath} item={item} onNavigate={onNavigate} />
|
|
158
|
+
) : (
|
|
159
|
+
<NavLinkItem key={item.to} item={item} onNavigate={onNavigate} />
|
|
160
|
+
),
|
|
161
|
+
)}
|
|
162
|
+
</SidebarMenu>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
|
2
|
+
import { CURRENT_USER } from "@/config/user";
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
const sizeClasses = {
|
|
6
|
+
sm: "h-8 w-8",
|
|
7
|
+
md: "h-9 w-9",
|
|
8
|
+
lg: "h-10 w-10",
|
|
9
|
+
} as const;
|
|
10
|
+
|
|
11
|
+
type UserAvatarProps = {
|
|
12
|
+
className?: string;
|
|
13
|
+
size?: keyof typeof sizeClasses;
|
|
14
|
+
rounded?: "full" | "lg";
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function UserAvatar({ className, size = "md", rounded = "full" }: UserAvatarProps) {
|
|
18
|
+
return (
|
|
19
|
+
<Avatar
|
|
20
|
+
className={cn(sizeClasses[size], rounded === "lg" && "rounded-lg", className)}
|
|
21
|
+
>
|
|
22
|
+
<AvatarImage src={CURRENT_USER.avatarUrl} alt={CURRENT_USER.name} />
|
|
23
|
+
<AvatarFallback className={cn(rounded === "lg" && "rounded-lg")}>{CURRENT_USER.initials}</AvatarFallback>
|
|
24
|
+
</Avatar>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { LogOut, Settings, User } from "lucide-react";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { useNavigate } from "react-router-dom";
|
|
4
|
+
import { UserAvatar } from "@/components/user-avatar";
|
|
5
|
+
import {
|
|
6
|
+
AlertDialog,
|
|
7
|
+
AlertDialogAction,
|
|
8
|
+
AlertDialogCancel,
|
|
9
|
+
AlertDialogContent,
|
|
10
|
+
AlertDialogDescription,
|
|
11
|
+
AlertDialogFooter,
|
|
12
|
+
AlertDialogHeader,
|
|
13
|
+
AlertDialogTitle,
|
|
14
|
+
} from "@/components/ui/alert-dialog";
|
|
15
|
+
import { Button } from "@/components/ui/button";
|
|
16
|
+
import {
|
|
17
|
+
DropdownMenu,
|
|
18
|
+
DropdownMenuContent,
|
|
19
|
+
DropdownMenuItem,
|
|
20
|
+
DropdownMenuLabel,
|
|
21
|
+
DropdownMenuSeparator,
|
|
22
|
+
DropdownMenuTrigger,
|
|
23
|
+
} from "@/components/ui/dropdown-menu";
|
|
24
|
+
import { SidebarMenuButton } from "@/components/ui/sidebar";
|
|
25
|
+
import { CURRENT_USER } from "@/config/user";
|
|
26
|
+
import { useAuth } from "@/context/auth-context";
|
|
27
|
+
import { cn } from "@/lib/utils";
|
|
28
|
+
|
|
29
|
+
type UserMenuProps = {
|
|
30
|
+
variant?: "header" | "sidebar";
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function LogoutConfirmDialog({
|
|
34
|
+
open,
|
|
35
|
+
onOpenChange,
|
|
36
|
+
}: {
|
|
37
|
+
open: boolean;
|
|
38
|
+
onOpenChange: (open: boolean) => void;
|
|
39
|
+
}) {
|
|
40
|
+
const { logout } = useAuth();
|
|
41
|
+
const navigate = useNavigate();
|
|
42
|
+
|
|
43
|
+
const handleConfirm = () => {
|
|
44
|
+
logout();
|
|
45
|
+
onOpenChange(false);
|
|
46
|
+
navigate("/login", { replace: true });
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
|
51
|
+
<AlertDialogContent>
|
|
52
|
+
<AlertDialogHeader>
|
|
53
|
+
<AlertDialogTitle>Log out?</AlertDialogTitle>
|
|
54
|
+
<AlertDialogDescription>
|
|
55
|
+
You will be signed out of your admin workspace. You can sign in again at any time.
|
|
56
|
+
</AlertDialogDescription>
|
|
57
|
+
</AlertDialogHeader>
|
|
58
|
+
<AlertDialogFooter>
|
|
59
|
+
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
60
|
+
<AlertDialogAction onClick={handleConfirm}>Log out</AlertDialogAction>
|
|
61
|
+
</AlertDialogFooter>
|
|
62
|
+
</AlertDialogContent>
|
|
63
|
+
</AlertDialog>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function UserMenuItems({ onLogoutClick }: { onLogoutClick: () => void }) {
|
|
68
|
+
const navigate = useNavigate();
|
|
69
|
+
|
|
70
|
+
const handleLogoutSelect = () => {
|
|
71
|
+
window.setTimeout(() => onLogoutClick(), 0);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<>
|
|
76
|
+
<DropdownMenuLabel className="font-normal">
|
|
77
|
+
<div className="flex items-center gap-2">
|
|
78
|
+
<UserAvatar size="md" />
|
|
79
|
+
<div className="grid text-left text-sm leading-tight">
|
|
80
|
+
<span className="font-medium">{CURRENT_USER.name}</span>
|
|
81
|
+
<span className="text-xs text-muted-foreground">{CURRENT_USER.email}</span>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
</DropdownMenuLabel>
|
|
85
|
+
<DropdownMenuSeparator />
|
|
86
|
+
<DropdownMenuItem onSelect={() => navigate("/settings/profile")}>
|
|
87
|
+
<User className="mr-2 h-4 w-4" />
|
|
88
|
+
Profile
|
|
89
|
+
</DropdownMenuItem>
|
|
90
|
+
<DropdownMenuItem onSelect={() => navigate("/settings/notifications")}>
|
|
91
|
+
<Settings className="mr-2 h-4 w-4" />
|
|
92
|
+
Settings
|
|
93
|
+
</DropdownMenuItem>
|
|
94
|
+
<DropdownMenuSeparator />
|
|
95
|
+
<DropdownMenuItem onSelect={handleLogoutSelect}>
|
|
96
|
+
<LogOut className="mr-2 h-4 w-4" />
|
|
97
|
+
Log out
|
|
98
|
+
</DropdownMenuItem>
|
|
99
|
+
</>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function UserMenu({ variant = "header" }: UserMenuProps) {
|
|
104
|
+
const [logoutDialogOpen, setLogoutDialogOpen] = useState(false);
|
|
105
|
+
|
|
106
|
+
const openLogoutDialog = () => setLogoutDialogOpen(true);
|
|
107
|
+
|
|
108
|
+
if (variant === "sidebar") {
|
|
109
|
+
return (
|
|
110
|
+
<>
|
|
111
|
+
<div className="flex w-full items-center gap-1">
|
|
112
|
+
<DropdownMenu>
|
|
113
|
+
<DropdownMenuTrigger asChild>
|
|
114
|
+
<SidebarMenuButton
|
|
115
|
+
size="lg"
|
|
116
|
+
className="min-w-0 flex-1 data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
|
117
|
+
>
|
|
118
|
+
<UserAvatar size="lg" rounded="lg" />
|
|
119
|
+
<div className="grid flex-1 text-left text-sm leading-tight group-data-[collapsible=icon]:hidden">
|
|
120
|
+
<span className="truncate font-medium">{CURRENT_USER.name}</span>
|
|
121
|
+
<span className="truncate text-xs text-muted-foreground">{CURRENT_USER.email}</span>
|
|
122
|
+
</div>
|
|
123
|
+
</SidebarMenuButton>
|
|
124
|
+
</DropdownMenuTrigger>
|
|
125
|
+
<DropdownMenuContent side="top" align="end" className="z-[60] w-56">
|
|
126
|
+
<UserMenuItems onLogoutClick={openLogoutDialog} />
|
|
127
|
+
</DropdownMenuContent>
|
|
128
|
+
</DropdownMenu>
|
|
129
|
+
<SidebarMenuButton
|
|
130
|
+
size="lg"
|
|
131
|
+
className="w-9 shrink-0 px-0 group-data-[collapsible=icon]:mx-auto group-data-[collapsible=icon]:w-8"
|
|
132
|
+
aria-label="Log out"
|
|
133
|
+
onClick={openLogoutDialog}
|
|
134
|
+
>
|
|
135
|
+
<LogOut className="h-4 w-4 text-muted-foreground" />
|
|
136
|
+
</SidebarMenuButton>
|
|
137
|
+
</div>
|
|
138
|
+
<LogoutConfirmDialog open={logoutDialogOpen} onOpenChange={setLogoutDialogOpen} />
|
|
139
|
+
</>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<>
|
|
145
|
+
<DropdownMenu>
|
|
146
|
+
<DropdownMenuTrigger asChild>
|
|
147
|
+
<Button
|
|
148
|
+
variant="outline"
|
|
149
|
+
size="sm"
|
|
150
|
+
className={cn("h-9 gap-2 rounded-full pl-1 pr-2 sm:pl-1 sm:pr-3")}
|
|
151
|
+
>
|
|
152
|
+
<UserAvatar size="sm" />
|
|
153
|
+
<span className="hidden max-w-[120px] truncate text-sm font-medium sm:inline">{CURRENT_USER.name}</span>
|
|
154
|
+
</Button>
|
|
155
|
+
</DropdownMenuTrigger>
|
|
156
|
+
<DropdownMenuContent align="end" className="w-56">
|
|
157
|
+
<UserMenuItems onLogoutClick={openLogoutDialog} />
|
|
158
|
+
</DropdownMenuContent>
|
|
159
|
+
</DropdownMenu>
|
|
160
|
+
<LogoutConfirmDialog open={logoutDialogOpen} onOpenChange={setLogoutDialogOpen} />
|
|
161
|
+
</>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import heroImage from "@/assets/auth-hero.jpg";
|
|
2
|
+
import logoImage from "@/assets/brand-logo.png";
|
|
3
|
+
|
|
4
|
+
export const BRANDING = {
|
|
5
|
+
projectName: "Omobio Common GUI",
|
|
6
|
+
productName: "Omobio Admin Portal",
|
|
7
|
+
companyName: "Omobio",
|
|
8
|
+
logoPath: logoImage,
|
|
9
|
+
heroImagePath: heroImage,
|
|
10
|
+
auth: {
|
|
11
|
+
loginTitle: "Welcome back",
|
|
12
|
+
loginSubtitle: "Sign in to your admin workspace",
|
|
13
|
+
signupTitle: "Create your account",
|
|
14
|
+
signupSubtitle: "Register your store and start managing operations",
|
|
15
|
+
continueWithGoogleLabel: "Continue with Google",
|
|
16
|
+
},
|
|
17
|
+
} as const;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { chartFill } from "@/config/theme";
|
|
2
|
+
|
|
3
|
+
export const monthlyRevenueData = [
|
|
4
|
+
{ month: "Jan", revenue: 4200, users: 240 },
|
|
5
|
+
{ month: "Feb", revenue: 3800, users: 198 },
|
|
6
|
+
{ month: "Mar", revenue: 5100, users: 280 },
|
|
7
|
+
{ month: "Apr", revenue: 4600, users: 260 },
|
|
8
|
+
{ month: "May", revenue: 5400, users: 310 },
|
|
9
|
+
{ month: "Jun", revenue: 6200, users: 340 },
|
|
10
|
+
{ month: "Jul", revenue: 5800, users: 320 },
|
|
11
|
+
{ month: "Aug", revenue: 6400, users: 360 },
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
export const weeklyActiveUsersData = [
|
|
15
|
+
{ week: "W1", users: 1200 },
|
|
16
|
+
{ week: "W2", users: 1450 },
|
|
17
|
+
{ week: "W3", users: 1380 },
|
|
18
|
+
{ week: "W4", users: 1620 },
|
|
19
|
+
{ week: "W5", users: 1780 },
|
|
20
|
+
{ week: "W6", users: 1690 },
|
|
21
|
+
{ week: "W7", users: 1920 },
|
|
22
|
+
{ week: "W8", users: 2100 },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
export const trafficSourceData = [
|
|
26
|
+
{ source: "Organic", visitors: 4200 },
|
|
27
|
+
{ source: "Paid", visitors: 2800 },
|
|
28
|
+
{ source: "Referral", visitors: 1900 },
|
|
29
|
+
{ source: "Social", visitors: 1400 },
|
|
30
|
+
{ source: "Email", visitors: 900 },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
export const userRoleDistribution = [
|
|
34
|
+
{ role: "Admin", count: 12, fill: chartFill(1) },
|
|
35
|
+
{ role: "Editor", count: 28, fill: chartFill(2) },
|
|
36
|
+
{ role: "Viewer", count: 45, fill: chartFill(3) },
|
|
37
|
+
{ role: "Invited", count: 8, fill: chartFill(4) },
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
export const deviceUsageData = [
|
|
41
|
+
{ device: "Desktop", value: 58, fill: chartFill(1) },
|
|
42
|
+
{ device: "Mobile", value: 32, fill: chartFill(2) },
|
|
43
|
+
{ device: "Tablet", value: 10, fill: chartFill(3) },
|
|
44
|
+
];
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { BarChart3, Blocks, Settings, Users, type LucideIcon } from "lucide-react";
|
|
2
|
+
|
|
3
|
+
export type NavLinkConfig = {
|
|
4
|
+
type: "link";
|
|
5
|
+
to: string;
|
|
6
|
+
label: string;
|
|
7
|
+
icon: LucideIcon;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type NavGroupConfig = {
|
|
11
|
+
type: "group";
|
|
12
|
+
label: string;
|
|
13
|
+
icon: LucideIcon;
|
|
14
|
+
/** Prefix used to auto-expand and mark parent active (e.g. `/settings`). */
|
|
15
|
+
basePath: string;
|
|
16
|
+
children: { to: string; label: string }[];
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type NavItemConfig = NavLinkConfig | NavGroupConfig;
|
|
20
|
+
|
|
21
|
+
export const overviewNav: NavLinkConfig[] = [
|
|
22
|
+
{ type: "link", to: "/dashboard", label: "Dashboard", icon: BarChart3 },
|
|
23
|
+
{ type: "link", to: "/users", label: "Users", icon: Users },
|
|
24
|
+
{ type: "link", to: "/components", label: "Components", icon: Blocks },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
export const workspaceNav: NavItemConfig[] = [
|
|
28
|
+
{
|
|
29
|
+
type: "group",
|
|
30
|
+
label: "Settings",
|
|
31
|
+
icon: Settings,
|
|
32
|
+
basePath: "/settings",
|
|
33
|
+
children: [
|
|
34
|
+
{ to: "/settings/profile", label: "Profile" },
|
|
35
|
+
{ to: "/settings/notifications", label: "Notifications" },
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
export function isNavGroup(item: NavItemConfig): item is NavGroupConfig {
|
|
41
|
+
return item.type === "group";
|
|
42
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { createContext, ReactNode, useContext, useMemo, useState } from "react";
|
|
2
|
+
|
|
3
|
+
type AuthContextValue = {
|
|
4
|
+
isAuthenticated: boolean;
|
|
5
|
+
login: () => void;
|
|
6
|
+
logout: () => void;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const AuthContext = createContext<AuthContextValue | null>(null);
|
|
10
|
+
|
|
11
|
+
export function AuthProvider({ children }: { children: ReactNode }) {
|
|
12
|
+
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
|
13
|
+
|
|
14
|
+
const value = useMemo<AuthContextValue>(
|
|
15
|
+
() => ({
|
|
16
|
+
isAuthenticated,
|
|
17
|
+
login: () => setIsAuthenticated(true),
|
|
18
|
+
logout: () => setIsAuthenticated(false),
|
|
19
|
+
}),
|
|
20
|
+
[isAuthenticated],
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function useAuth() {
|
|
27
|
+
const context = useContext(AuthContext);
|
|
28
|
+
if (!context) {
|
|
29
|
+
throw new Error("useAuth must be used within an AuthProvider.");
|
|
30
|
+
}
|
|
31
|
+
return context;
|
|
32
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { isNavGroup, overviewNav, workspaceNav, type NavItemConfig } from "@/config/navigation";
|
|
2
|
+
|
|
3
|
+
export type BreadcrumbSegment = {
|
|
4
|
+
label: string;
|
|
5
|
+
to?: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const allNavItems: NavItemConfig[] = [...overviewNav, ...workspaceNav];
|
|
9
|
+
|
|
10
|
+
function formatSegmentLabel(segment: string) {
|
|
11
|
+
return segment
|
|
12
|
+
.split("-")
|
|
13
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
14
|
+
.join(" ");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function getBreadcrumbs(pathname: string): BreadcrumbSegment[] {
|
|
18
|
+
const segments: BreadcrumbSegment[] = [{ label: "Home", to: "/dashboard" }];
|
|
19
|
+
|
|
20
|
+
for (const item of allNavItems) {
|
|
21
|
+
if (!isNavGroup(item)) {
|
|
22
|
+
if (pathname === item.to) {
|
|
23
|
+
segments.push({ label: item.label });
|
|
24
|
+
return segments;
|
|
25
|
+
}
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const child = item.children.find((entry) => pathname === entry.to);
|
|
30
|
+
if (child) {
|
|
31
|
+
segments.push({ label: item.label, to: item.basePath });
|
|
32
|
+
segments.push({ label: child.label });
|
|
33
|
+
return segments;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (pathname === item.basePath) {
|
|
37
|
+
segments.push({ label: item.label });
|
|
38
|
+
return segments;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const pathParts = pathname.split("/").filter(Boolean);
|
|
43
|
+
if (pathParts.length === 0) {
|
|
44
|
+
return segments;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let currentPath = "";
|
|
48
|
+
pathParts.forEach((part, index) => {
|
|
49
|
+
currentPath += `/${part}`;
|
|
50
|
+
const isLast = index === pathParts.length - 1;
|
|
51
|
+
segments.push({
|
|
52
|
+
label: formatSegmentLabel(part),
|
|
53
|
+
to: isLast ? undefined : currentPath,
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return segments;
|
|
58
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import ReactDOM from "react-dom/client";
|
|
3
|
+
import { BrowserRouter } from "react-router-dom";
|
|
4
|
+
import App from "./App";
|
|
5
|
+
import { Toaster } from "@/components/ui/sonner";
|
|
6
|
+
import { initializeTheme } from "@/config/theme";
|
|
7
|
+
|
|
8
|
+
initializeTheme();
|
|
9
|
+
import "./styles/globals.css";
|
|
10
|
+
|
|
11
|
+
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
|
12
|
+
<React.StrictMode>
|
|
13
|
+
<BrowserRouter>
|
|
14
|
+
<App />
|
|
15
|
+
<Toaster richColors closeButton position="top-right" />
|
|
16
|
+
</BrowserRouter>
|
|
17
|
+
</React.StrictMode>,
|
|
18
|
+
);
|