@nocobase/portal-template-default 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.
Files changed (123) hide show
  1. package/.env.example +10 -0
  2. package/Dockerfile +36 -0
  3. package/README.MD +50 -0
  4. package/components.json +25 -0
  5. package/eslint.config.js +28 -0
  6. package/index.html +39 -0
  7. package/package.json +84 -0
  8. package/pnpm-workspace.yaml +2 -0
  9. package/public/favicon.ico +0 -0
  10. package/public/logo-dark.png +0 -0
  11. package/public/logo-mark-dark.png +0 -0
  12. package/public/logo-mark.png +0 -0
  13. package/public/logo.png +0 -0
  14. package/src/App.css +158 -0
  15. package/src/App.tsx +151 -0
  16. package/src/components/app-shell/brand.tsx +70 -0
  17. package/src/components/app-shell/breadcrumb.tsx +81 -0
  18. package/src/components/app-shell/document-title-handler.tsx +169 -0
  19. package/src/components/app-shell/error-component.tsx +134 -0
  20. package/src/components/app-shell/header.tsx +127 -0
  21. package/src/components/app-shell/layout.tsx +39 -0
  22. package/src/components/app-shell/loading-overlay.tsx +36 -0
  23. package/src/components/app-shell/sidebar.tsx +351 -0
  24. package/src/components/app-shell/user-avatar.tsx +42 -0
  25. package/src/components/app-shell/user-info.tsx +53 -0
  26. package/src/components/auth/auth-layout.tsx +82 -0
  27. package/src/components/auth/forgot-password-form.tsx +58 -0
  28. package/src/components/auth/input-password.tsx +38 -0
  29. package/src/components/auth/sign-in-form.tsx +111 -0
  30. package/src/components/auth/sign-up-form.tsx +127 -0
  31. package/src/components/data-table/data-table-filter.tsx +994 -0
  32. package/src/components/data-table/data-table-pagination.tsx +146 -0
  33. package/src/components/data-table/data-table-sorter.tsx +47 -0
  34. package/src/components/data-table/data-table.tsx +321 -0
  35. package/src/components/notifications/toaster.tsx +23 -0
  36. package/src/components/notifications/undoable-notification.tsx +84 -0
  37. package/src/components/notifications/use-notification-provider.tsx +62 -0
  38. package/src/components/resources/buttons/clone.tsx +81 -0
  39. package/src/components/resources/buttons/create.tsx +70 -0
  40. package/src/components/resources/buttons/delete.tsx +113 -0
  41. package/src/components/resources/buttons/edit.tsx +81 -0
  42. package/src/components/resources/buttons/list.tsx +70 -0
  43. package/src/components/resources/buttons/refresh.tsx +81 -0
  44. package/src/components/resources/buttons/show.tsx +81 -0
  45. package/src/components/resources/status-badge.tsx +37 -0
  46. package/src/components/resources/views/create-view.tsx +81 -0
  47. package/src/components/resources/views/edit-view.tsx +104 -0
  48. package/src/components/resources/views/list-view.tsx +86 -0
  49. package/src/components/resources/views/show-view.tsx +108 -0
  50. package/src/components/theme/theme-provider.tsx +71 -0
  51. package/src/components/theme/theme-select.tsx +97 -0
  52. package/src/components/theme/theme-toggle.tsx +66 -0
  53. package/src/components/ui/accordion.tsx +72 -0
  54. package/src/components/ui/alert-dialog.tsx +185 -0
  55. package/src/components/ui/alert.tsx +76 -0
  56. package/src/components/ui/aspect-ratio.tsx +22 -0
  57. package/src/components/ui/avatar.tsx +107 -0
  58. package/src/components/ui/badge.tsx +52 -0
  59. package/src/components/ui/breadcrumb.tsx +125 -0
  60. package/src/components/ui/button.tsx +58 -0
  61. package/src/components/ui/calendar.tsx +221 -0
  62. package/src/components/ui/card.tsx +103 -0
  63. package/src/components/ui/carousel.tsx +240 -0
  64. package/src/components/ui/chart.tsx +373 -0
  65. package/src/components/ui/checkbox.tsx +29 -0
  66. package/src/components/ui/collapsible.tsx +19 -0
  67. package/src/components/ui/command.tsx +192 -0
  68. package/src/components/ui/context-menu.tsx +271 -0
  69. package/src/components/ui/dialog.tsx +160 -0
  70. package/src/components/ui/drawer.tsx +226 -0
  71. package/src/components/ui/dropdown-menu.tsx +268 -0
  72. package/src/components/ui/form.tsx +174 -0
  73. package/src/components/ui/hover-card.tsx +49 -0
  74. package/src/components/ui/input-group.tsx +158 -0
  75. package/src/components/ui/input-otp.tsx +87 -0
  76. package/src/components/ui/input.tsx +20 -0
  77. package/src/components/ui/label.tsx +18 -0
  78. package/src/components/ui/menubar.tsx +278 -0
  79. package/src/components/ui/navigation-menu.tsx +168 -0
  80. package/src/components/ui/pagination.tsx +130 -0
  81. package/src/components/ui/popover.tsx +91 -0
  82. package/src/components/ui/progress.tsx +81 -0
  83. package/src/components/ui/radio-group.tsx +38 -0
  84. package/src/components/ui/resizable.tsx +48 -0
  85. package/src/components/ui/scroll-area.tsx +55 -0
  86. package/src/components/ui/select.tsx +199 -0
  87. package/src/components/ui/separator.tsx +25 -0
  88. package/src/components/ui/sheet.tsx +136 -0
  89. package/src/components/ui/sidebar.tsx +723 -0
  90. package/src/components/ui/skeleton.tsx +13 -0
  91. package/src/components/ui/slider.tsx +52 -0
  92. package/src/components/ui/sonner.tsx +47 -0
  93. package/src/components/ui/switch.tsx +32 -0
  94. package/src/components/ui/table.tsx +114 -0
  95. package/src/components/ui/tabs.tsx +82 -0
  96. package/src/components/ui/textarea.tsx +18 -0
  97. package/src/components/ui/toggle-group.tsx +87 -0
  98. package/src/components/ui/toggle.tsx +43 -0
  99. package/src/components/ui/tooltip.tsx +66 -0
  100. package/src/hooks/use-mobile.ts +19 -0
  101. package/src/index.tsx +17 -0
  102. package/src/lib/utils.ts +19 -0
  103. package/src/pages/blog-posts/create.tsx +177 -0
  104. package/src/pages/blog-posts/edit.tsx +197 -0
  105. package/src/pages/blog-posts/index.ts +4 -0
  106. package/src/pages/blog-posts/list.tsx +131 -0
  107. package/src/pages/blog-posts/show.tsx +81 -0
  108. package/src/pages/categories/create.tsx +73 -0
  109. package/src/pages/categories/edit.tsx +73 -0
  110. package/src/pages/categories/index.ts +4 -0
  111. package/src/pages/categories/list.tsx +86 -0
  112. package/src/pages/categories/show.tsx +37 -0
  113. package/src/pages/forgot-password/index.tsx +6 -0
  114. package/src/pages/forgotPassword/index.tsx +5 -0
  115. package/src/pages/login/index.tsx +5 -0
  116. package/src/pages/register/index.tsx +5 -0
  117. package/src/providers/auth.ts +204 -0
  118. package/src/providers/constants.ts +75 -0
  119. package/src/providers/data.ts +232 -0
  120. package/src/vite-env.d.ts +1 -0
  121. package/tsconfig.json +25 -0
  122. package/tsconfig.node.json +21 -0
  123. package/vite.config.ts +74 -0
@@ -0,0 +1,81 @@
1
+ "use client";
2
+
3
+ import { Fragment, useMemo } from "react";
4
+ import { Home } from "lucide-react";
5
+ import {
6
+ matchResourceFromRoute,
7
+ useBreadcrumb,
8
+ useLink,
9
+ useResourceParams,
10
+ } from "@refinedev/core";
11
+ import {
12
+ BreadcrumbSeparator as ShadcnBreadcrumbSeparator,
13
+ BreadcrumbItem as ShadcnBreadcrumbItem,
14
+ BreadcrumbList as ShadcnBreadcrumbList,
15
+ BreadcrumbPage as ShadcnBreadcrumbPage,
16
+ Breadcrumb as ShadcnBreadcrumb,
17
+ } from "@/components/ui/breadcrumb";
18
+
19
+ export function Breadcrumb() {
20
+ const Link = useLink();
21
+ const { breadcrumbs } = useBreadcrumb();
22
+ const { resources } = useResourceParams();
23
+ const rootRouteResource = matchResourceFromRoute("/", resources);
24
+
25
+ const breadCrumbItems = useMemo(() => {
26
+ const list: {
27
+ key: string;
28
+ href: string;
29
+ Component: React.ReactNode;
30
+ }[] = [];
31
+
32
+ list.push({
33
+ key: "breadcrumb-item-home",
34
+ href: rootRouteResource.matchedRoute ?? "/",
35
+ Component: (
36
+ <Link to={rootRouteResource.matchedRoute ?? "/"}>
37
+ {rootRouteResource?.resource?.meta?.icon ?? (
38
+ <Home className="h-4 w-4" />
39
+ )}
40
+ </Link>
41
+ ),
42
+ });
43
+
44
+ for (const { label, href } of breadcrumbs) {
45
+ list.push({
46
+ key: `breadcrumb-item-${label}`,
47
+ href: href ?? "",
48
+ Component: href ? <Link to={href}>{label}</Link> : <span>{label}</span>,
49
+ });
50
+ }
51
+
52
+ return list;
53
+ }, [breadcrumbs, Link, rootRouteResource]);
54
+
55
+ return (
56
+ <ShadcnBreadcrumb>
57
+ <ShadcnBreadcrumbList>
58
+ {breadCrumbItems.map((item, index) => {
59
+ if (index === breadCrumbItems.length - 1) {
60
+ return (
61
+ <ShadcnBreadcrumbPage key={item.key}>
62
+ {item.Component}
63
+ </ShadcnBreadcrumbPage>
64
+ );
65
+ }
66
+
67
+ return (
68
+ <Fragment key={item.key}>
69
+ <ShadcnBreadcrumbItem key={item.key}>
70
+ {item.Component}
71
+ </ShadcnBreadcrumbItem>
72
+ <ShadcnBreadcrumbSeparator />
73
+ </Fragment>
74
+ );
75
+ })}
76
+ </ShadcnBreadcrumbList>
77
+ </ShadcnBreadcrumb>
78
+ );
79
+ }
80
+
81
+ Breadcrumb.displayName = "Breadcrumb";
@@ -0,0 +1,169 @@
1
+ import type { Action, IResourceItem } from "@refinedev/core";
2
+ import {
3
+ useParsed,
4
+ useTranslate,
5
+ useUserFriendlyName,
6
+ } from "@refinedev/core";
7
+ import { useLayoutEffect } from "react";
8
+ import { useLocation } from "react-router";
9
+
10
+ export type ActionPrefixContext = {
11
+ id?: string;
12
+ };
13
+
14
+ export type ActionPrefix =
15
+ | string
16
+ | ((context: ActionPrefixContext) => string);
17
+
18
+ export type DocumentTitleContext = {
19
+ action?: Action;
20
+ appName: string;
21
+ defaultTitle: string;
22
+ id?: string;
23
+ params?: Record<string, string | undefined>;
24
+ pathname?: string;
25
+ resource?: IResourceItem;
26
+ resourceName?: string;
27
+ separator: string;
28
+ };
29
+
30
+ export type DocumentTitleFormatter = (context: DocumentTitleContext) => string;
31
+
32
+ export type DocumentTitleHandlerProps = {
33
+ actionPrefixes?: Partial<Record<Action, ActionPrefix>>;
34
+ appName?: string;
35
+ defaultTitle?: string;
36
+ formatTitle?: DocumentTitleFormatter;
37
+ separator?: string;
38
+ };
39
+
40
+ const defaultActionPrefixes = {
41
+ clone: ({ id }: ActionPrefixContext) => `#${id ?? ""} Clone `,
42
+ create: "Create new ",
43
+ edit: ({ id }: ActionPrefixContext) => `#${id ?? ""} Edit `,
44
+ list: "",
45
+ show: ({ id }: ActionPrefixContext) => `#${id ?? ""} Show `,
46
+ } satisfies Partial<Record<Action, ActionPrefix>>;
47
+
48
+ const safeTranslate = (
49
+ translate: ReturnType<typeof useTranslate>,
50
+ key: string,
51
+ defaultMessage?: string,
52
+ options?: Record<string, unknown>
53
+ ) => {
54
+ const translated = options
55
+ ? translate(key, options, defaultMessage)
56
+ : translate(key, defaultMessage);
57
+
58
+ const fallback = defaultMessage ?? key;
59
+
60
+ if (translated === key || typeof translated === "undefined") {
61
+ return fallback;
62
+ }
63
+
64
+ return translated;
65
+ };
66
+
67
+ const resolveActionPrefix = (
68
+ actionPrefix: ActionPrefix | undefined,
69
+ context: ActionPrefixContext
70
+ ) => {
71
+ if (typeof actionPrefix === "function") {
72
+ return actionPrefix(context);
73
+ }
74
+
75
+ return actionPrefix ?? "";
76
+ };
77
+
78
+ export function DocumentTitleHandler({
79
+ actionPrefixes,
80
+ appName = "NocoBase",
81
+ defaultTitle = appName,
82
+ formatTitle,
83
+ separator = " | ",
84
+ }: DocumentTitleHandlerProps) {
85
+ const location = useLocation();
86
+ const { action, id, params, pathname, resource } = useParsed();
87
+ const translate = useTranslate();
88
+ const getUserFriendlyName = useUserFriendlyName();
89
+
90
+ useLayoutEffect(() => {
91
+ const identifier = resource?.identifier ?? resource?.name;
92
+ const resourceNameFallback =
93
+ resource?.meta?.label ??
94
+ (identifier
95
+ ? getUserFriendlyName(
96
+ identifier,
97
+ action === "list" ? "plural" : "singular"
98
+ )
99
+ : undefined);
100
+
101
+ const resourceName = resource?.name
102
+ ? safeTranslate(
103
+ translate,
104
+ `${resource.name}.${resource.name}`,
105
+ resourceNameFallback
106
+ )
107
+ : resourceNameFallback;
108
+
109
+ const context: DocumentTitleContext = {
110
+ action,
111
+ appName,
112
+ defaultTitle,
113
+ id: typeof id === "undefined" ? undefined : `${id}`,
114
+ params,
115
+ pathname,
116
+ resource: resource
117
+ ? {
118
+ ...resource,
119
+ meta: {
120
+ ...resource.meta,
121
+ label: resourceName,
122
+ },
123
+ }
124
+ : undefined,
125
+ resourceName,
126
+ separator,
127
+ };
128
+
129
+ if (formatTitle) {
130
+ document.title = formatTitle(context);
131
+ return;
132
+ }
133
+
134
+ if (!action || !identifier || !resourceName) {
135
+ document.title = defaultTitle;
136
+ return;
137
+ }
138
+
139
+ const mergedActionPrefixes = {
140
+ ...defaultActionPrefixes,
141
+ ...actionPrefixes,
142
+ };
143
+ const prefix = resolveActionPrefix(mergedActionPrefixes[action], context);
144
+ const fallbackTitle = `${prefix}${resourceName}${separator}${appName}`;
145
+
146
+ document.title = safeTranslate(
147
+ translate,
148
+ `documentTitle.${identifier}.${action}`,
149
+ fallbackTitle,
150
+ { id: context.id }
151
+ );
152
+ }, [
153
+ action,
154
+ actionPrefixes,
155
+ appName,
156
+ defaultTitle,
157
+ formatTitle,
158
+ getUserFriendlyName,
159
+ id,
160
+ location,
161
+ params,
162
+ pathname,
163
+ resource,
164
+ separator,
165
+ translate,
166
+ ]);
167
+
168
+ return null;
169
+ }
@@ -0,0 +1,134 @@
1
+ import { Button } from "@/components/ui/button";
2
+ import {
3
+ Tooltip,
4
+ TooltipContent,
5
+ TooltipProvider,
6
+ TooltipTrigger,
7
+ } from "@/components/ui/tooltip";
8
+ import { cn } from "@/lib/utils";
9
+ import { useGo, useResourceParams, useTranslate } from "@refinedev/core";
10
+ import { ChevronLeft, InfoIcon } from "lucide-react";
11
+ import React, { useEffect, useState } from "react";
12
+
13
+ /**
14
+ * When the app is navigated to a non-existent route, refine shows a default error page.
15
+ * A custom error component can be used for this error page.
16
+ *
17
+ * @see {@link https://refine.dev/docs/packages/documentation/routers/} for more details.
18
+ */
19
+ export function ErrorComponent() {
20
+ const [errorMessage, setErrorMessage] = useState<string>();
21
+
22
+ const translate = useTranslate();
23
+ const go = useGo();
24
+
25
+ const { resource, action } = useResourceParams();
26
+
27
+ useEffect(() => {
28
+ if (resource && action) {
29
+ setErrorMessage(
30
+ translate(
31
+ "pages.error.info",
32
+ {
33
+ action: action,
34
+ resource: resource?.name,
35
+ },
36
+ `You may have forgotten to add the "${action}" component to "${resource?.name}" resource.`
37
+ )
38
+ );
39
+ }
40
+ }, [resource, action, translate]);
41
+
42
+ return (
43
+ <div
44
+ className={cn(
45
+ "flex",
46
+ "items-center",
47
+ "justify-center",
48
+ "bg-background",
49
+ "my-auto"
50
+ )}
51
+ >
52
+ <div className={cn("text-center", "space-y-8")}>
53
+ <div className={cn("flex", "justify-center")}>
54
+ <svg
55
+ width="199"
56
+ height="73"
57
+ viewBox="0 0 199 73"
58
+ fill="none"
59
+ xmlns="http://www.w3.org/2000/svg"
60
+ className={cn("w-48", "h-auto")}
61
+ >
62
+ <title>404 text</title>
63
+ <path
64
+ d="M0.293794 60.0909V45.3636L25.112 1.18182H43.2483L18.9756 44.8182V45.3636H60.4302V60.0909H0.293794ZM34.2483 71V55.7273L34.9302 49.1818V27.3636H51.9756V71H34.2483ZM98.983 72.9091C92.642 72.8864 87.1648 71.4205 82.5511 68.5114C77.9375 65.6023 74.3807 61.4091 71.8807 55.9318C69.3807 50.4545 68.142 43.8864 68.1648 36.2273C68.1875 28.5455 69.4375 22.0227 71.9148 16.6591C74.4148 11.2955 77.9602 7.21591 82.5511 4.42045C87.1648 1.625 92.642 0.22727 98.983 0.22727C105.324 0.22727 110.801 1.63636 115.415 4.45455C120.028 7.25 123.585 11.3295 126.085 16.6932C128.585 22.0568 129.824 28.5682 129.801 36.2273C129.801 43.9318 128.551 50.5227 126.051 56C123.551 61.4773 119.994 65.6705 115.381 68.5795C110.79 71.4659 105.324 72.9091 98.983 72.9091ZM98.983 57.5C102.256 57.5 104.938 55.7955 107.028 52.3864C109.142 48.9545 110.188 43.5682 110.165 36.2273C110.165 31.4318 109.688 27.5114 108.733 24.4659C107.778 21.4205 106.46 19.1705 104.778 17.7159C103.097 16.2386 101.165 15.5 98.983 15.5C95.7102 15.5 93.0398 17.1591 90.9716 20.4773C88.9034 23.7955 87.8466 29.0455 87.8011 36.2273C87.7784 41.1136 88.2443 45.1364 89.1989 48.2955C90.1534 51.4318 91.4716 53.75 93.1534 55.25C94.858 56.75 96.8011 57.5 98.983 57.5ZM138.209 60.0909V45.3636L163.027 1.18182H181.164L156.891 44.8182V45.3636H198.345V60.0909H138.209ZM172.164 71V55.7273L172.845 49.1818V27.3636H189.891V71H172.164Z"
65
+ fill="url(#paint0_linear_2672_10455)"
66
+ />
67
+ <defs>
68
+ <linearGradient
69
+ id="paint0_linear_2672_10455"
70
+ x1="99"
71
+ y1="-16"
72
+ x2="99"
73
+ y2="88"
74
+ gradientUnits="userSpaceOnUse"
75
+ >
76
+ <stop stopColor="#D4D4D8" />
77
+ <stop offset="1" stopColor="#E4E4E7" stopOpacity="0.25" />
78
+ </linearGradient>
79
+ </defs>
80
+ </svg>
81
+ </div>
82
+
83
+ <div className={cn("space-y-4")}>
84
+ <h1 className={cn("text-2xl", "font-semibold", "text-foreground")}>
85
+ {translate("pages.error.title", "Page not found.")}
86
+ </h1>
87
+
88
+ <div
89
+ className={cn("flex", "items-center", "justify-center", "gap-2")}
90
+ >
91
+ <p className={cn("text-muted-foreground")}>
92
+ {translate(
93
+ "pages.error.description",
94
+ "The page you're looking for does not exist."
95
+ )}
96
+ </p>
97
+ {errorMessage && (
98
+ <TooltipProvider>
99
+ <Tooltip>
100
+ <TooltipTrigger
101
+ render={<InfoIcon
102
+ className={cn(
103
+ "h-4",
104
+ "w-4",
105
+ "text-muted-foreground",
106
+ "cursor-help"
107
+ )}
108
+ data-testid="error-component-tooltip"
109
+ />}
110
+ />
111
+ <TooltipContent>
112
+ <p>{errorMessage}</p>
113
+ </TooltipContent>
114
+ </Tooltip>
115
+ </TooltipProvider>
116
+ )}
117
+ </div>
118
+ </div>
119
+
120
+ <Button
121
+ onClick={() => {
122
+ go({ to: "/" });
123
+ }}
124
+ className={cn("flex", "items-center", "gap-2", "mx-auto")}
125
+ >
126
+ <ChevronLeft className={cn("h-4", "w-4")} />
127
+ {translate("pages.error.backHome", "Back to homepage")}
128
+ </Button>
129
+ </div>
130
+ </div>
131
+ );
132
+ }
133
+
134
+ ErrorComponent.displayName = "ErrorComponent";
@@ -0,0 +1,127 @@
1
+ import { useActiveAuthProvider, useLogout } from "@refinedev/core";
2
+ import {
3
+ DropdownMenu,
4
+ DropdownMenuItem,
5
+ DropdownMenuContent,
6
+ DropdownMenuSeparator,
7
+ } from "@/components/ui/dropdown-menu";
8
+ import { DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
9
+ import { ThemeToggle } from "@/components/theme/theme-toggle";
10
+ import { UserAvatar } from "@/components/app-shell/user-avatar";
11
+ import { UserInfo } from "@/components/app-shell/user-info";
12
+ import { useSidebar, SidebarTrigger } from "@/components/ui/sidebar";
13
+ import { LogOutIcon } from "lucide-react";
14
+ import { cn } from "@/lib/utils";
15
+ import { Brand } from "@/components/app-shell/brand";
16
+
17
+ export const Header = () => {
18
+ const { isMobile } = useSidebar();
19
+
20
+ return <>{isMobile ? <MobileHeader /> : <DesktopHeader />}</>;
21
+ };
22
+
23
+ function DesktopHeader() {
24
+ return (
25
+ <header
26
+ className={cn(
27
+ "sticky",
28
+ "top-0",
29
+ "flex",
30
+ "h-16",
31
+ "shrink-0",
32
+ "items-center",
33
+ "gap-4",
34
+ "border-b",
35
+ "border-border/70",
36
+ "bg-background/80",
37
+ "px-4",
38
+ "justify-between",
39
+ "backdrop-blur-xl",
40
+ "z-40"
41
+ )}
42
+ >
43
+ <div className="flex items-center gap-3">
44
+ <SidebarTrigger className="size-9 rounded-xl text-muted-foreground hover:text-foreground" />
45
+ <div className="hidden h-5 w-px bg-border sm:block" />
46
+ <span className="hidden text-sm font-medium text-muted-foreground sm:block">
47
+ AI application workspace
48
+ </span>
49
+ </div>
50
+ <div className="flex items-center gap-2">
51
+ <ThemeToggle />
52
+ <UserDropdown />
53
+ </div>
54
+ </header>
55
+ );
56
+ }
57
+
58
+ function MobileHeader() {
59
+ const { isMobile } = useSidebar();
60
+
61
+ return (
62
+ <header
63
+ className={cn(
64
+ "sticky",
65
+ "top-0",
66
+ "flex",
67
+ "h-16",
68
+ "shrink-0",
69
+ "items-center",
70
+ "gap-2",
71
+ "border-b",
72
+ "border-border/70",
73
+ "bg-background/85",
74
+ "px-3",
75
+ "justify-between",
76
+ "backdrop-blur-xl",
77
+ "z-40"
78
+ )}
79
+ >
80
+ <SidebarTrigger
81
+ className={cn(
82
+ "size-9 rounded-xl text-muted-foreground",
83
+ !isMobile && "hidden"
84
+ )}
85
+ />
86
+ <Brand logoClassName="h-6" />
87
+ <ThemeToggle className={cn("size-9")} />
88
+ </header>
89
+ );
90
+ }
91
+
92
+ const UserDropdown = () => {
93
+ const { mutate: logout, isPending: isLoggingOut } = useLogout();
94
+
95
+ const authProvider = useActiveAuthProvider();
96
+
97
+ if (!authProvider?.getIdentity) {
98
+ return null;
99
+ }
100
+
101
+ return (
102
+ <DropdownMenu>
103
+ <DropdownMenuTrigger>
104
+ <UserAvatar />
105
+ </DropdownMenuTrigger>
106
+ <DropdownMenuContent align="end" className="w-72 p-2">
107
+ <div className="px-2 py-2">
108
+ <UserInfo />
109
+ </div>
110
+ <DropdownMenuSeparator />
111
+ <DropdownMenuItem
112
+ className="mt-1 min-h-9 cursor-pointer gap-2 px-2 text-muted-foreground focus:text-foreground"
113
+ onClick={() => {
114
+ logout();
115
+ }}
116
+ >
117
+ <LogOutIcon />
118
+ <span>{isLoggingOut ? "Signing out..." : "Sign out"}</span>
119
+ </DropdownMenuItem>
120
+ </DropdownMenuContent>
121
+ </DropdownMenu>
122
+ );
123
+ };
124
+
125
+ Header.displayName = "Header";
126
+ MobileHeader.displayName = "MobileHeader";
127
+ DesktopHeader.displayName = "DesktopHeader";
@@ -0,0 +1,39 @@
1
+ "use client";
2
+
3
+ import { Header } from "@/components/app-shell/header";
4
+ import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
5
+ import { cn } from "@/lib/utils";
6
+ import type { PropsWithChildren } from "react";
7
+ import { Sidebar } from "./sidebar";
8
+
9
+ export function Layout({ children }: PropsWithChildren) {
10
+ return (
11
+ <SidebarProvider>
12
+ <Sidebar />
13
+ <SidebarInset className="bg-muted/25">
14
+ <Header />
15
+ <main
16
+ className={cn(
17
+ "@container/main",
18
+ "mx-auto",
19
+ "max-w-[1600px]",
20
+ "relative",
21
+ "w-full",
22
+ "flex",
23
+ "flex-col",
24
+ "flex-1",
25
+ "px-4",
26
+ "py-5",
27
+ "md:p-6",
28
+ "lg:px-8",
29
+ "lg:py-7"
30
+ )}
31
+ >
32
+ {children}
33
+ </main>
34
+ </SidebarInset>
35
+ </SidebarProvider>
36
+ );
37
+ }
38
+
39
+ Layout.displayName = "Layout";
@@ -0,0 +1,36 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { Loader2 } from "lucide-react";
5
+ import { cn } from "@/lib/utils";
6
+
7
+ interface LoadingOverlayProps extends React.HTMLAttributes<HTMLDivElement> {
8
+ loading?: boolean;
9
+ children: React.ReactNode;
10
+ }
11
+
12
+ export const LoadingOverlay = React.forwardRef<
13
+ HTMLDivElement,
14
+ LoadingOverlayProps
15
+ >(({ className, loading = false, children, ...props }, ref) => {
16
+ if (!loading) return children;
17
+
18
+ return (
19
+ <div className="relative" ref={ref} {...props}>
20
+ {children}
21
+ <div
22
+ className={cn(
23
+ "absolute inset-0 z-50 flex items-center justify-center",
24
+ "bg-background/60",
25
+ className
26
+ )}
27
+ >
28
+ <div className="flex flex-col items-center gap-2">
29
+ <Loader2 className="h-8 w-8 animate-spin text-primary" />
30
+ </div>
31
+ </div>
32
+ </div>
33
+ );
34
+ });
35
+
36
+ LoadingOverlay.displayName = "LoadingOverlay";