@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.
- package/.env.example +10 -0
- package/Dockerfile +36 -0
- package/README.MD +50 -0
- package/components.json +25 -0
- package/eslint.config.js +28 -0
- package/index.html +39 -0
- package/package.json +84 -0
- package/pnpm-workspace.yaml +2 -0
- package/public/favicon.ico +0 -0
- package/public/logo-dark.png +0 -0
- package/public/logo-mark-dark.png +0 -0
- package/public/logo-mark.png +0 -0
- package/public/logo.png +0 -0
- package/src/App.css +158 -0
- package/src/App.tsx +151 -0
- package/src/components/app-shell/brand.tsx +70 -0
- package/src/components/app-shell/breadcrumb.tsx +81 -0
- package/src/components/app-shell/document-title-handler.tsx +169 -0
- package/src/components/app-shell/error-component.tsx +134 -0
- package/src/components/app-shell/header.tsx +127 -0
- package/src/components/app-shell/layout.tsx +39 -0
- package/src/components/app-shell/loading-overlay.tsx +36 -0
- package/src/components/app-shell/sidebar.tsx +351 -0
- package/src/components/app-shell/user-avatar.tsx +42 -0
- package/src/components/app-shell/user-info.tsx +53 -0
- package/src/components/auth/auth-layout.tsx +82 -0
- package/src/components/auth/forgot-password-form.tsx +58 -0
- package/src/components/auth/input-password.tsx +38 -0
- package/src/components/auth/sign-in-form.tsx +111 -0
- package/src/components/auth/sign-up-form.tsx +127 -0
- package/src/components/data-table/data-table-filter.tsx +994 -0
- package/src/components/data-table/data-table-pagination.tsx +146 -0
- package/src/components/data-table/data-table-sorter.tsx +47 -0
- package/src/components/data-table/data-table.tsx +321 -0
- package/src/components/notifications/toaster.tsx +23 -0
- package/src/components/notifications/undoable-notification.tsx +84 -0
- package/src/components/notifications/use-notification-provider.tsx +62 -0
- package/src/components/resources/buttons/clone.tsx +81 -0
- package/src/components/resources/buttons/create.tsx +70 -0
- package/src/components/resources/buttons/delete.tsx +113 -0
- package/src/components/resources/buttons/edit.tsx +81 -0
- package/src/components/resources/buttons/list.tsx +70 -0
- package/src/components/resources/buttons/refresh.tsx +81 -0
- package/src/components/resources/buttons/show.tsx +81 -0
- package/src/components/resources/status-badge.tsx +37 -0
- package/src/components/resources/views/create-view.tsx +81 -0
- package/src/components/resources/views/edit-view.tsx +104 -0
- package/src/components/resources/views/list-view.tsx +86 -0
- package/src/components/resources/views/show-view.tsx +108 -0
- package/src/components/theme/theme-provider.tsx +71 -0
- package/src/components/theme/theme-select.tsx +97 -0
- package/src/components/theme/theme-toggle.tsx +66 -0
- package/src/components/ui/accordion.tsx +72 -0
- package/src/components/ui/alert-dialog.tsx +185 -0
- package/src/components/ui/alert.tsx +76 -0
- package/src/components/ui/aspect-ratio.tsx +22 -0
- package/src/components/ui/avatar.tsx +107 -0
- package/src/components/ui/badge.tsx +52 -0
- package/src/components/ui/breadcrumb.tsx +125 -0
- package/src/components/ui/button.tsx +58 -0
- package/src/components/ui/calendar.tsx +221 -0
- package/src/components/ui/card.tsx +103 -0
- package/src/components/ui/carousel.tsx +240 -0
- package/src/components/ui/chart.tsx +373 -0
- package/src/components/ui/checkbox.tsx +29 -0
- package/src/components/ui/collapsible.tsx +19 -0
- package/src/components/ui/command.tsx +192 -0
- package/src/components/ui/context-menu.tsx +271 -0
- package/src/components/ui/dialog.tsx +160 -0
- package/src/components/ui/drawer.tsx +226 -0
- package/src/components/ui/dropdown-menu.tsx +268 -0
- package/src/components/ui/form.tsx +174 -0
- package/src/components/ui/hover-card.tsx +49 -0
- package/src/components/ui/input-group.tsx +158 -0
- package/src/components/ui/input-otp.tsx +87 -0
- package/src/components/ui/input.tsx +20 -0
- package/src/components/ui/label.tsx +18 -0
- package/src/components/ui/menubar.tsx +278 -0
- package/src/components/ui/navigation-menu.tsx +168 -0
- package/src/components/ui/pagination.tsx +130 -0
- package/src/components/ui/popover.tsx +91 -0
- package/src/components/ui/progress.tsx +81 -0
- package/src/components/ui/radio-group.tsx +38 -0
- package/src/components/ui/resizable.tsx +48 -0
- package/src/components/ui/scroll-area.tsx +55 -0
- package/src/components/ui/select.tsx +199 -0
- package/src/components/ui/separator.tsx +25 -0
- package/src/components/ui/sheet.tsx +136 -0
- package/src/components/ui/sidebar.tsx +723 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/slider.tsx +52 -0
- package/src/components/ui/sonner.tsx +47 -0
- package/src/components/ui/switch.tsx +32 -0
- package/src/components/ui/table.tsx +114 -0
- package/src/components/ui/tabs.tsx +82 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/toggle-group.tsx +87 -0
- package/src/components/ui/toggle.tsx +43 -0
- package/src/components/ui/tooltip.tsx +66 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/index.tsx +17 -0
- package/src/lib/utils.ts +19 -0
- package/src/pages/blog-posts/create.tsx +177 -0
- package/src/pages/blog-posts/edit.tsx +197 -0
- package/src/pages/blog-posts/index.ts +4 -0
- package/src/pages/blog-posts/list.tsx +131 -0
- package/src/pages/blog-posts/show.tsx +81 -0
- package/src/pages/categories/create.tsx +73 -0
- package/src/pages/categories/edit.tsx +73 -0
- package/src/pages/categories/index.ts +4 -0
- package/src/pages/categories/list.tsx +86 -0
- package/src/pages/categories/show.tsx +37 -0
- package/src/pages/forgot-password/index.tsx +6 -0
- package/src/pages/forgotPassword/index.tsx +5 -0
- package/src/pages/login/index.tsx +5 -0
- package/src/pages/register/index.tsx +5 -0
- package/src/providers/auth.ts +204 -0
- package/src/providers/constants.ts +75 -0
- package/src/providers/data.ts +232 -0
- package/src/vite-env.d.ts +1 -0
- package/tsconfig.json +25 -0
- package/tsconfig.node.json +21 -0
- 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";
|