@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,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";