@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,351 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import {
|
|
5
|
+
useMenu,
|
|
6
|
+
useLink,
|
|
7
|
+
type TreeMenuItem,
|
|
8
|
+
} from "@refinedev/core";
|
|
9
|
+
import {
|
|
10
|
+
SidebarRail as ShadcnSidebarRail,
|
|
11
|
+
Sidebar as ShadcnSidebar,
|
|
12
|
+
SidebarContent as ShadcnSidebarContent,
|
|
13
|
+
SidebarFooter as ShadcnSidebarFooter,
|
|
14
|
+
SidebarHeader as ShadcnSidebarHeader,
|
|
15
|
+
useSidebar as useShadcnSidebar,
|
|
16
|
+
} from "@/components/ui/sidebar";
|
|
17
|
+
import {
|
|
18
|
+
DropdownMenu,
|
|
19
|
+
DropdownMenuContent,
|
|
20
|
+
DropdownMenuItem,
|
|
21
|
+
DropdownMenuTrigger,
|
|
22
|
+
} from "@/components/ui/dropdown-menu";
|
|
23
|
+
import {
|
|
24
|
+
Collapsible,
|
|
25
|
+
CollapsibleContent,
|
|
26
|
+
CollapsibleTrigger,
|
|
27
|
+
} from "@/components/ui/collapsible";
|
|
28
|
+
import { Button } from "@/components/ui/button";
|
|
29
|
+
import { ChevronRight, ListIcon, ShieldCheck } from "lucide-react";
|
|
30
|
+
import { cn } from "@/lib/utils";
|
|
31
|
+
import { Brand } from "@/components/app-shell/brand";
|
|
32
|
+
|
|
33
|
+
export function Sidebar() {
|
|
34
|
+
const { open } = useShadcnSidebar();
|
|
35
|
+
const { menuItems, selectedKey } = useMenu();
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<ShadcnSidebar
|
|
39
|
+
collapsible="icon"
|
|
40
|
+
className={cn("border-r border-sidebar-border/70")}
|
|
41
|
+
>
|
|
42
|
+
<ShadcnSidebarRail />
|
|
43
|
+
<SidebarHeader />
|
|
44
|
+
<ShadcnSidebarContent
|
|
45
|
+
className={cn(
|
|
46
|
+
"transition-discrete",
|
|
47
|
+
"duration-200",
|
|
48
|
+
"flex",
|
|
49
|
+
"flex-col",
|
|
50
|
+
"gap-1.5",
|
|
51
|
+
"py-3",
|
|
52
|
+
{
|
|
53
|
+
"px-3": open,
|
|
54
|
+
"px-1": !open,
|
|
55
|
+
}
|
|
56
|
+
)}
|
|
57
|
+
>
|
|
58
|
+
{menuItems.map((item: TreeMenuItem) => (
|
|
59
|
+
<SidebarItem
|
|
60
|
+
key={item.key || item.name}
|
|
61
|
+
item={item}
|
|
62
|
+
selectedKey={selectedKey}
|
|
63
|
+
/>
|
|
64
|
+
))}
|
|
65
|
+
</ShadcnSidebarContent>
|
|
66
|
+
<SidebarFooter />
|
|
67
|
+
</ShadcnSidebar>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
type MenuItemProps = {
|
|
72
|
+
item: TreeMenuItem;
|
|
73
|
+
selectedKey?: string;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
function SidebarItem({ item, selectedKey }: MenuItemProps) {
|
|
77
|
+
const { open } = useShadcnSidebar();
|
|
78
|
+
|
|
79
|
+
if (item.meta?.group) {
|
|
80
|
+
return <SidebarItemGroup item={item} selectedKey={selectedKey} />;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (item.children && item.children.length > 0) {
|
|
84
|
+
if (open) {
|
|
85
|
+
return <SidebarItemCollapsible item={item} selectedKey={selectedKey} />;
|
|
86
|
+
}
|
|
87
|
+
return <SidebarItemDropdown item={item} selectedKey={selectedKey} />;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return <SidebarItemLink item={item} selectedKey={selectedKey} />;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function SidebarItemGroup({ item, selectedKey }: MenuItemProps) {
|
|
94
|
+
const { children } = item;
|
|
95
|
+
const { open } = useShadcnSidebar();
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<div className={cn("mt-2 border-t", "border-sidebar-border/70", "pt-4")}>
|
|
99
|
+
<span
|
|
100
|
+
className={cn(
|
|
101
|
+
"ml-3",
|
|
102
|
+
"block",
|
|
103
|
+
"text-xs",
|
|
104
|
+
"font-semibold",
|
|
105
|
+
"uppercase",
|
|
106
|
+
"text-muted-foreground",
|
|
107
|
+
"transition-all",
|
|
108
|
+
"duration-200",
|
|
109
|
+
{
|
|
110
|
+
"h-8": open,
|
|
111
|
+
"h-0": !open,
|
|
112
|
+
"opacity-0": !open,
|
|
113
|
+
"opacity-100": open,
|
|
114
|
+
"pointer-events-none": !open,
|
|
115
|
+
"pointer-events-auto": open,
|
|
116
|
+
}
|
|
117
|
+
)}
|
|
118
|
+
>
|
|
119
|
+
{getDisplayName(item)}
|
|
120
|
+
</span>
|
|
121
|
+
{children && children.length > 0 && (
|
|
122
|
+
<div className={cn("flex", "flex-col")}>
|
|
123
|
+
{children.map((child: TreeMenuItem) => (
|
|
124
|
+
<SidebarItem
|
|
125
|
+
key={child.key || child.name}
|
|
126
|
+
item={child}
|
|
127
|
+
selectedKey={selectedKey}
|
|
128
|
+
/>
|
|
129
|
+
))}
|
|
130
|
+
</div>
|
|
131
|
+
)}
|
|
132
|
+
</div>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function SidebarItemCollapsible({ item, selectedKey }: MenuItemProps) {
|
|
137
|
+
const { name, children } = item;
|
|
138
|
+
|
|
139
|
+
const chevronIcon = (
|
|
140
|
+
<ChevronRight
|
|
141
|
+
className={cn(
|
|
142
|
+
"h-4",
|
|
143
|
+
"w-4",
|
|
144
|
+
"shrink-0",
|
|
145
|
+
"text-muted-foreground",
|
|
146
|
+
"transition-transform",
|
|
147
|
+
"duration-200",
|
|
148
|
+
"group-data-[state=open]:rotate-90"
|
|
149
|
+
)}
|
|
150
|
+
/>
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<Collapsible key={`collapsible-${name}`} className={cn("w-full", "group")}>
|
|
155
|
+
<CollapsibleTrigger
|
|
156
|
+
render={<SidebarButton item={item} rightIcon={chevronIcon} />}
|
|
157
|
+
/>
|
|
158
|
+
<CollapsibleContent className={cn("ml-6", "flex", "flex-col", "gap-2")}>
|
|
159
|
+
{children?.map((child: TreeMenuItem) => (
|
|
160
|
+
<SidebarItem
|
|
161
|
+
key={child.key || child.name}
|
|
162
|
+
item={child}
|
|
163
|
+
selectedKey={selectedKey}
|
|
164
|
+
/>
|
|
165
|
+
))}
|
|
166
|
+
</CollapsibleContent>
|
|
167
|
+
</Collapsible>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function SidebarItemDropdown({ item, selectedKey }: MenuItemProps) {
|
|
172
|
+
const { children } = item;
|
|
173
|
+
const Link = useLink();
|
|
174
|
+
|
|
175
|
+
return (
|
|
176
|
+
<DropdownMenu>
|
|
177
|
+
<DropdownMenuTrigger render={<SidebarButton item={item} />} />
|
|
178
|
+
<DropdownMenuContent side="right" align="start">
|
|
179
|
+
{children?.map((child: TreeMenuItem) => {
|
|
180
|
+
const { key: childKey } = child;
|
|
181
|
+
const isSelected = childKey === selectedKey;
|
|
182
|
+
|
|
183
|
+
return (
|
|
184
|
+
<DropdownMenuItem
|
|
185
|
+
key={childKey || child.name}
|
|
186
|
+
render={<Link
|
|
187
|
+
to={child.route || ""}
|
|
188
|
+
className={cn("flex w-full items-center gap-2", {
|
|
189
|
+
"bg-accent text-accent-foreground": isSelected,
|
|
190
|
+
})}
|
|
191
|
+
/>}
|
|
192
|
+
>
|
|
193
|
+
<ItemIcon
|
|
194
|
+
icon={child.meta?.icon ?? child.icon}
|
|
195
|
+
isSelected={isSelected}
|
|
196
|
+
/>
|
|
197
|
+
<span>{getDisplayName(child)}</span>
|
|
198
|
+
</DropdownMenuItem>
|
|
199
|
+
);
|
|
200
|
+
})}
|
|
201
|
+
</DropdownMenuContent>
|
|
202
|
+
</DropdownMenu>
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function SidebarItemLink({ item, selectedKey }: MenuItemProps) {
|
|
207
|
+
const isSelected = item.key === selectedKey;
|
|
208
|
+
|
|
209
|
+
return <SidebarButton item={item} isSelected={isSelected} asLink={true} />;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function SidebarHeader() {
|
|
213
|
+
const { open } = useShadcnSidebar();
|
|
214
|
+
|
|
215
|
+
return (
|
|
216
|
+
<ShadcnSidebarHeader
|
|
217
|
+
className={cn(
|
|
218
|
+
"h-16",
|
|
219
|
+
"p-0",
|
|
220
|
+
"border-b",
|
|
221
|
+
"border-sidebar-border/70",
|
|
222
|
+
"flex-row",
|
|
223
|
+
"items-center",
|
|
224
|
+
"overflow-hidden",
|
|
225
|
+
open ? "px-5" : "justify-center px-0"
|
|
226
|
+
)}
|
|
227
|
+
>
|
|
228
|
+
<Brand
|
|
229
|
+
showText={open}
|
|
230
|
+
logoClassName={cn("transition-transform duration-200", !open && "size-9")}
|
|
231
|
+
/>
|
|
232
|
+
</ShadcnSidebarHeader>
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function SidebarFooter() {
|
|
237
|
+
const { open } = useShadcnSidebar();
|
|
238
|
+
|
|
239
|
+
return (
|
|
240
|
+
<ShadcnSidebarFooter className="border-t border-sidebar-border/70 p-0">
|
|
241
|
+
<div
|
|
242
|
+
className={cn(
|
|
243
|
+
"flex min-h-16 items-center",
|
|
244
|
+
open ? "gap-3 px-5 py-3" : "justify-center px-2"
|
|
245
|
+
)}
|
|
246
|
+
>
|
|
247
|
+
<ShieldCheck className="size-4 shrink-0 text-muted-foreground" />
|
|
248
|
+
{open && (
|
|
249
|
+
<div className="min-w-0 text-xs leading-4">
|
|
250
|
+
<div className="font-semibold text-sidebar-foreground">
|
|
251
|
+
AI builds freely.
|
|
252
|
+
</div>
|
|
253
|
+
<div className="text-muted-foreground">
|
|
254
|
+
NocoBase keeps it reliable.
|
|
255
|
+
</div>
|
|
256
|
+
</div>
|
|
257
|
+
)}
|
|
258
|
+
</div>
|
|
259
|
+
</ShadcnSidebarFooter>
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function getDisplayName(item: TreeMenuItem) {
|
|
264
|
+
return item.meta?.label ?? item.label ?? item.name;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
type IconProps = {
|
|
268
|
+
icon: React.ReactNode;
|
|
269
|
+
isSelected?: boolean;
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
function ItemIcon({ icon, isSelected }: IconProps) {
|
|
273
|
+
return (
|
|
274
|
+
<div
|
|
275
|
+
className={cn("w-4", {
|
|
276
|
+
"text-muted-foreground": !isSelected,
|
|
277
|
+
"text-primary": isSelected,
|
|
278
|
+
})}
|
|
279
|
+
>
|
|
280
|
+
{icon ?? <ListIcon />}
|
|
281
|
+
</div>
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
type SidebarButtonProps = React.ComponentProps<typeof Button> & {
|
|
286
|
+
item: TreeMenuItem;
|
|
287
|
+
isSelected?: boolean;
|
|
288
|
+
rightIcon?: React.ReactNode;
|
|
289
|
+
asLink?: boolean;
|
|
290
|
+
onClick?: () => void;
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
function SidebarButton({
|
|
294
|
+
item,
|
|
295
|
+
isSelected = false,
|
|
296
|
+
rightIcon,
|
|
297
|
+
asLink = false,
|
|
298
|
+
className,
|
|
299
|
+
onClick,
|
|
300
|
+
...props
|
|
301
|
+
}: SidebarButtonProps) {
|
|
302
|
+
const Link = useLink();
|
|
303
|
+
|
|
304
|
+
const buttonContent = (
|
|
305
|
+
<>
|
|
306
|
+
<ItemIcon icon={item.meta?.icon ?? item.icon} isSelected={isSelected} />
|
|
307
|
+
<span
|
|
308
|
+
className={cn(
|
|
309
|
+
"tracking-[-0.00875rem] text-foreground",
|
|
310
|
+
{
|
|
311
|
+
"flex-1": rightIcon,
|
|
312
|
+
"text-left": rightIcon,
|
|
313
|
+
"line-clamp-1": !rightIcon,
|
|
314
|
+
truncate: !rightIcon,
|
|
315
|
+
"font-normal": !isSelected,
|
|
316
|
+
"font-medium": isSelected,
|
|
317
|
+
},
|
|
318
|
+
)}
|
|
319
|
+
>
|
|
320
|
+
{getDisplayName(item)}
|
|
321
|
+
</span>
|
|
322
|
+
{rightIcon}
|
|
323
|
+
</>
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
return (
|
|
327
|
+
<Button
|
|
328
|
+
render={
|
|
329
|
+
asLink && item.route ? (
|
|
330
|
+
<Link to={item.route} className={cn("flex w-full items-center gap-2")} />
|
|
331
|
+
) : undefined
|
|
332
|
+
}
|
|
333
|
+
variant="ghost"
|
|
334
|
+
size="default"
|
|
335
|
+
className={cn(
|
|
336
|
+
"flex h-10 w-full items-center justify-start gap-3 rounded-lg px-3 text-sm transition-colors",
|
|
337
|
+
{
|
|
338
|
+
"bg-primary/10 text-primary hover:!bg-primary/15": isSelected,
|
|
339
|
+
"hover:bg-sidebar-accent/80": !isSelected,
|
|
340
|
+
},
|
|
341
|
+
className
|
|
342
|
+
)}
|
|
343
|
+
onClick={onClick}
|
|
344
|
+
{...props}
|
|
345
|
+
>
|
|
346
|
+
{buttonContent}
|
|
347
|
+
</Button>
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
Sidebar.displayName = "Sidebar";
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { useGetIdentity } from "@refinedev/core";
|
|
2
|
+
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
|
3
|
+
import { Skeleton } from "@/components/ui/skeleton";
|
|
4
|
+
import { cn } from "@/lib/utils";
|
|
5
|
+
|
|
6
|
+
type User = {
|
|
7
|
+
id: number;
|
|
8
|
+
firstName: string;
|
|
9
|
+
lastName: string;
|
|
10
|
+
fullName: string;
|
|
11
|
+
email: string;
|
|
12
|
+
avatar?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function UserAvatar() {
|
|
16
|
+
const { data: user, isLoading: userIsLoading } = useGetIdentity<User>();
|
|
17
|
+
|
|
18
|
+
if (userIsLoading || !user) {
|
|
19
|
+
return <Skeleton className={cn("h-10", "w-10", "rounded-full")} />;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const { fullName, avatar } = user;
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<Avatar className={cn("h-10", "w-10")}>
|
|
26
|
+
{avatar && <AvatarImage src={avatar} alt={fullName} />}
|
|
27
|
+
<AvatarFallback>{getInitials(fullName)}</AvatarFallback>
|
|
28
|
+
</Avatar>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const getInitials = (name = "") => {
|
|
33
|
+
const names = name.split(" ");
|
|
34
|
+
let initials = names[0].substring(0, 1).toUpperCase();
|
|
35
|
+
|
|
36
|
+
if (names.length > 1) {
|
|
37
|
+
initials += names[names.length - 1].substring(0, 1).toUpperCase();
|
|
38
|
+
}
|
|
39
|
+
return initials;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
UserAvatar.displayName = "UserAvatar";
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { useGetIdentity } from "@refinedev/core";
|
|
2
|
+
import { Skeleton } from "@/components/ui/skeleton";
|
|
3
|
+
import { UserAvatar } from "@/components/app-shell/user-avatar";
|
|
4
|
+
import { cn } from "@/lib/utils";
|
|
5
|
+
|
|
6
|
+
type User = {
|
|
7
|
+
id: number;
|
|
8
|
+
firstName: string;
|
|
9
|
+
lastName: string;
|
|
10
|
+
fullName: string;
|
|
11
|
+
email: string;
|
|
12
|
+
avatar?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function UserInfo() {
|
|
16
|
+
const { data: user, isLoading: userIsLoading } = useGetIdentity<User>();
|
|
17
|
+
|
|
18
|
+
if (userIsLoading || !user) {
|
|
19
|
+
return (
|
|
20
|
+
<div className={cn("flex", "items-center", "gap-x-2")}>
|
|
21
|
+
<Skeleton className={cn("h-10", "w-10", "rounded-full")} />
|
|
22
|
+
<div className={cn("flex", "flex-col", "justify-between", "h-10")}>
|
|
23
|
+
<Skeleton className={cn("h-4", "w-32")} />
|
|
24
|
+
<Skeleton className={cn("h-4", "w-24")} />
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const { firstName, lastName, email } = user;
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div className={cn("flex", "items-center", "gap-x-2")}>
|
|
34
|
+
<UserAvatar />
|
|
35
|
+
<div
|
|
36
|
+
className={cn(
|
|
37
|
+
"flex",
|
|
38
|
+
"flex-col",
|
|
39
|
+
"justify-between",
|
|
40
|
+
"h-10",
|
|
41
|
+
"text-left"
|
|
42
|
+
)}
|
|
43
|
+
>
|
|
44
|
+
<span className={cn("text-sm", "font-medium", "text-muted-foreground")}>
|
|
45
|
+
{firstName} {lastName}
|
|
46
|
+
</span>
|
|
47
|
+
<span className={cn("text-xs", "text-muted-foreground")}>{email}</span>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
UserInfo.displayName = "UserInfo";
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { PropsWithChildren, ReactNode } from "react";
|
|
2
|
+
import { Blocks, ShieldCheck, Sparkles } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
import { Brand } from "@/components/app-shell/brand";
|
|
5
|
+
|
|
6
|
+
type AuthLayoutProps = PropsWithChildren<{
|
|
7
|
+
title: string;
|
|
8
|
+
description: string;
|
|
9
|
+
footer?: ReactNode;
|
|
10
|
+
}>;
|
|
11
|
+
|
|
12
|
+
export function AuthLayout({
|
|
13
|
+
title,
|
|
14
|
+
description,
|
|
15
|
+
footer,
|
|
16
|
+
children,
|
|
17
|
+
}: AuthLayoutProps) {
|
|
18
|
+
return (
|
|
19
|
+
<div className="grid min-h-svh md:grid-cols-[minmax(420px,44%)_1fr]">
|
|
20
|
+
<main className="grid place-items-center bg-card px-6 py-10 sm:px-12">
|
|
21
|
+
<div className="w-full max-w-sm">
|
|
22
|
+
<Brand className="mb-14" logoClassName="h-10" />
|
|
23
|
+
<h1 className="text-3xl font-semibold tracking-[-0.035em]">
|
|
24
|
+
{title}
|
|
25
|
+
</h1>
|
|
26
|
+
<p className="mb-8 mt-2 text-sm text-muted-foreground">
|
|
27
|
+
{description}
|
|
28
|
+
</p>
|
|
29
|
+
{children}
|
|
30
|
+
{footer && <div className="mt-8 text-sm">{footer}</div>}
|
|
31
|
+
</div>
|
|
32
|
+
</main>
|
|
33
|
+
|
|
34
|
+
<section className="relative hidden overflow-hidden bg-neutral-950 p-12 text-white md:grid md:place-items-center">
|
|
35
|
+
<div className="absolute inset-0 opacity-[0.08] [background-image:linear-gradient(currentColor_1px,transparent_1px),linear-gradient(90deg,currentColor_1px,transparent_1px)] [background-size:48px_48px]" />
|
|
36
|
+
<div className="relative w-full max-w-xl">
|
|
37
|
+
<div className="text-xs font-semibold uppercase tracking-[0.14em] text-primary-foreground/60">
|
|
38
|
+
AI-native application platform
|
|
39
|
+
</div>
|
|
40
|
+
<h2 className="mt-3 max-w-lg text-5xl font-semibold leading-[1.08] tracking-[-0.04em]">
|
|
41
|
+
Let AI build freely. NocoBase keeps it reliable.
|
|
42
|
+
</h2>
|
|
43
|
+
<p className="mt-5 max-w-lg text-base leading-7 text-white/60">
|
|
44
|
+
Give AI a flexible frontend framework to shape each experience,
|
|
45
|
+
while NocoBase secures the data, permissions, workflows and
|
|
46
|
+
governance underneath.
|
|
47
|
+
</p>
|
|
48
|
+
|
|
49
|
+
<div className="mt-10 max-w-md rounded-xl border border-white/15 bg-white p-5 text-neutral-900 shadow-2xl shadow-black/20">
|
|
50
|
+
<div className="flex items-center gap-3">
|
|
51
|
+
<div className="grid size-10 place-items-center rounded-lg bg-neutral-100">
|
|
52
|
+
<Sparkles className="size-4" />
|
|
53
|
+
</div>
|
|
54
|
+
<div>
|
|
55
|
+
<div className="font-semibold">AI-native frontend</div>
|
|
56
|
+
<div className="text-sm text-neutral-500">
|
|
57
|
+
Compose interfaces freely on a flexible framework.
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
<div className="my-5 h-px bg-neutral-200" />
|
|
62
|
+
<div className="flex items-center gap-3">
|
|
63
|
+
<div className="grid size-10 place-items-center rounded-lg bg-neutral-100">
|
|
64
|
+
<ShieldCheck className="size-4" />
|
|
65
|
+
</div>
|
|
66
|
+
<div>
|
|
67
|
+
<div className="font-semibold">NocoBase foundation</div>
|
|
68
|
+
<div className="text-sm text-neutral-500">
|
|
69
|
+
Reliable data, access control, workflows and governance.
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
<div className="mt-5 flex items-center gap-2 rounded-lg bg-neutral-100 px-3 py-2 text-xs font-medium text-neutral-600">
|
|
74
|
+
<Blocks className="size-3.5" />
|
|
75
|
+
Freedom above. Confidence below.
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
</section>
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { ArrowLeft } from "lucide-react";
|
|
5
|
+
import { useForgotPassword, useLink } from "@refinedev/core";
|
|
6
|
+
|
|
7
|
+
import { AuthLayout } from "@/components/auth/auth-layout";
|
|
8
|
+
import { Button } from "@/components/ui/button";
|
|
9
|
+
import { Input } from "@/components/ui/input";
|
|
10
|
+
import { Label } from "@/components/ui/label";
|
|
11
|
+
|
|
12
|
+
export const ForgotPasswordForm = () => {
|
|
13
|
+
const [email, setEmail] = useState("");
|
|
14
|
+
const Link = useLink();
|
|
15
|
+
const { mutate: forgotPassword } = useForgotPassword();
|
|
16
|
+
|
|
17
|
+
const handleForgotPassword = (event: React.FormEvent<HTMLFormElement>) => {
|
|
18
|
+
event.preventDefault();
|
|
19
|
+
forgotPassword({ email });
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<AuthLayout
|
|
24
|
+
title="Forgot password"
|
|
25
|
+
description="Enter your email to reset your password."
|
|
26
|
+
footer={
|
|
27
|
+
<Link
|
|
28
|
+
to="/login"
|
|
29
|
+
className="inline-flex items-center gap-2 text-muted-foreground transition-colors hover:text-foreground"
|
|
30
|
+
>
|
|
31
|
+
<ArrowLeft className="size-4" />
|
|
32
|
+
Back to sign in
|
|
33
|
+
</Link>
|
|
34
|
+
}
|
|
35
|
+
>
|
|
36
|
+
<form onSubmit={handleForgotPassword} className="space-y-5">
|
|
37
|
+
<div className="space-y-2">
|
|
38
|
+
<Label htmlFor="email">Email</Label>
|
|
39
|
+
<Input
|
|
40
|
+
id="email"
|
|
41
|
+
type="email"
|
|
42
|
+
value={email}
|
|
43
|
+
onChange={(event) => setEmail(event.target.value)}
|
|
44
|
+
autoComplete="email"
|
|
45
|
+
autoFocus
|
|
46
|
+
required
|
|
47
|
+
className="h-11 rounded-lg"
|
|
48
|
+
/>
|
|
49
|
+
</div>
|
|
50
|
+
<Button type="submit" size="lg" className="h-11 w-full rounded-lg">
|
|
51
|
+
Send reset link
|
|
52
|
+
</Button>
|
|
53
|
+
</form>
|
|
54
|
+
</AuthLayout>
|
|
55
|
+
);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
ForgotPasswordForm.displayName = "ForgotPasswordForm";
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { Eye, EyeOff } from "lucide-react";
|
|
5
|
+
import { Input } from "@/components/ui/input";
|
|
6
|
+
import { cn } from "@/lib/utils";
|
|
7
|
+
|
|
8
|
+
type InputPasswordProps = React.ComponentProps<"input">;
|
|
9
|
+
|
|
10
|
+
export const InputPassword = ({ className, ...props }: InputPasswordProps) => {
|
|
11
|
+
const [showPassword, setShowPassword] = useState(false);
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div className={cn("relative")}>
|
|
15
|
+
<Input
|
|
16
|
+
type={showPassword ? "text" : "password"}
|
|
17
|
+
className={cn(className)}
|
|
18
|
+
{...props}
|
|
19
|
+
/>
|
|
20
|
+
<button
|
|
21
|
+
type="button"
|
|
22
|
+
className={cn(
|
|
23
|
+
"appearance-none",
|
|
24
|
+
"absolute right-3 top-1/2 -translate-y-1/2"
|
|
25
|
+
)}
|
|
26
|
+
onClick={() => setShowPassword(!showPassword)}
|
|
27
|
+
>
|
|
28
|
+
{showPassword ? (
|
|
29
|
+
<EyeOff size={18} className={cn("text-gray-500")} />
|
|
30
|
+
) : (
|
|
31
|
+
<Eye size={18} className={cn("text-gray-500")} />
|
|
32
|
+
)}
|
|
33
|
+
</button>
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
InputPassword.displayName = "InputPassword";
|