@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,177 @@
1
+ import { useForm } from "@refinedev/react-hook-form";
2
+ import { useSelect } from "@refinedev/core";
3
+ import { useNavigate } from "react-router";
4
+ import { Textarea } from "@/components/ui/textarea";
5
+
6
+ import { CreateView } from "@/components/resources/views/create-view";
7
+ import { Button } from "@/components/ui/button";
8
+ import { Input } from "@/components/ui/input";
9
+ import {
10
+ Select,
11
+ SelectContent,
12
+ SelectItem,
13
+ SelectTrigger,
14
+ SelectValue,
15
+ } from "@/components/ui/select";
16
+ import {
17
+ Form,
18
+ FormControl,
19
+ FormField,
20
+ FormItem,
21
+ FormLabel,
22
+ FormMessage,
23
+ } from "@/components/ui/form";
24
+
25
+ export const BlogPostCreate = () => {
26
+ const navigate = useNavigate();
27
+
28
+ const {
29
+ refineCore: { onFinish },
30
+ ...form
31
+ } = useForm({
32
+ refineCoreProps: {},
33
+ });
34
+
35
+ const { options: categoryOptions } = useSelect({
36
+ resource: "categories",
37
+ optionLabel: "title",
38
+ optionValue: "id",
39
+ pagination: {
40
+ currentPage: 1,
41
+ pageSize: 100,
42
+ },
43
+ });
44
+
45
+ function onSubmit(values: Record<string, string>) {
46
+ onFinish(values);
47
+ }
48
+
49
+ return (
50
+ <CreateView>
51
+ <Form {...form}>
52
+ <form onSubmit={form.handleSubmit(onSubmit)} className="resource-form">
53
+ <FormField
54
+ control={form.control}
55
+ name="title"
56
+ rules={{ required: "Title is required" }}
57
+ render={({ field }) => (
58
+ <FormItem>
59
+ <FormLabel>Title</FormLabel>
60
+ <FormControl
61
+ render={<Input
62
+ {...field}
63
+ value={field.value || ""}
64
+ placeholder="Enter title"
65
+ />}
66
+ />
67
+ <FormMessage />
68
+ </FormItem>
69
+ )}
70
+ />
71
+
72
+ <FormField
73
+ control={form.control}
74
+ name="content"
75
+ rules={{ required: "Content is required" }}
76
+ render={({ field }) => (
77
+ <FormItem>
78
+ <FormLabel>Content</FormLabel>
79
+ <FormControl
80
+ render={<Textarea
81
+ {...field}
82
+ value={field.value || ""}
83
+ placeholder="Enter content"
84
+ rows={10}
85
+ />}
86
+ />
87
+ <FormMessage />
88
+ </FormItem>
89
+ )}
90
+ />
91
+
92
+ <FormField
93
+ control={form.control}
94
+ name="categoryId"
95
+ rules={{ required: "Category is required" }}
96
+ render={({ field }) => (
97
+ <FormItem>
98
+ <FormLabel>Category</FormLabel>
99
+ <Select
100
+ items={categoryOptions?.map((option) => ({
101
+ label: option.label,
102
+ value: option.value.toString(),
103
+ }))}
104
+ onValueChange={field.onChange}
105
+ value={field.value?.toString() || ""}
106
+ >
107
+ <FormControl
108
+ render={<SelectTrigger>
109
+ <SelectValue placeholder="Select a category" />
110
+ </SelectTrigger>}
111
+ />
112
+ <SelectContent>
113
+ {categoryOptions?.map((option) => (
114
+ <SelectItem key={option.value} value={option.value.toString()}>
115
+ {option.label}
116
+ </SelectItem>
117
+ ))}
118
+ </SelectContent>
119
+ </Select>
120
+ <FormMessage />
121
+ </FormItem>
122
+ )}
123
+ />
124
+
125
+ <FormField
126
+ control={form.control}
127
+ name="status"
128
+ rules={{ required: "Status is required" }}
129
+ render={({ field }) => (
130
+ <FormItem>
131
+ <FormLabel>Status</FormLabel>
132
+ <Select
133
+ items={{
134
+ draft: "Draft",
135
+ published: "Published",
136
+ rejected: "Rejected",
137
+ }}
138
+ onValueChange={field.onChange}
139
+ defaultValue={"draft"}
140
+ >
141
+ <FormControl
142
+ render={<SelectTrigger>
143
+ <SelectValue placeholder="Select status" />
144
+ </SelectTrigger>}
145
+ />
146
+ <SelectContent>
147
+ <SelectItem value="draft">Draft</SelectItem>
148
+ <SelectItem value="published">Published</SelectItem>
149
+ <SelectItem value="rejected">Rejected</SelectItem>
150
+ </SelectContent>
151
+ </Select>
152
+ <FormMessage />
153
+ </FormItem>
154
+ )}
155
+ />
156
+
157
+ <div className="flex gap-2">
158
+ <Button
159
+ type="submit"
160
+ {...form.saveButtonProps}
161
+ disabled={form.formState.isSubmitting}
162
+ >
163
+ {form.formState.isSubmitting ? "Creating..." : "Create"}
164
+ </Button>
165
+ <Button
166
+ type="button"
167
+ variant="outline"
168
+ onClick={() => navigate(-1)}
169
+ >
170
+ Cancel
171
+ </Button>
172
+ </div>
173
+ </form>
174
+ </Form>
175
+ </CreateView>
176
+ );
177
+ };
@@ -0,0 +1,197 @@
1
+ import { useForm } from "@refinedev/react-hook-form";
2
+ import { useSelect } from "@refinedev/core";
3
+ import { useEffect } from "react";
4
+ import { useNavigate } from "react-router";
5
+ import { Textarea } from "@/components/ui/textarea";
6
+
7
+ import { EditView } from "@/components/resources/views/edit-view";
8
+ import { Button } from "@/components/ui/button";
9
+ import { Input } from "@/components/ui/input";
10
+ import {
11
+ Select,
12
+ SelectContent,
13
+ SelectItem,
14
+ SelectTrigger,
15
+ SelectValue,
16
+ } from "@/components/ui/select";
17
+ import {
18
+ Form,
19
+ FormControl,
20
+ FormField,
21
+ FormItem,
22
+ FormLabel,
23
+ FormMessage,
24
+ } from "@/components/ui/form";
25
+
26
+ export const BlogPostEdit = () => {
27
+ const navigate = useNavigate();
28
+
29
+ const {
30
+ refineCore: { onFinish, query },
31
+ ...form
32
+ } = useForm({
33
+ refineCoreProps: {
34
+ meta: {
35
+ appends: ["category"],
36
+ },
37
+ },
38
+ });
39
+
40
+ const blogPostsData = query?.data?.data;
41
+ const defaultCategoryId =
42
+ blogPostsData?.categoryId ?? blogPostsData?.category?.id;
43
+
44
+ useEffect(() => {
45
+ if (defaultCategoryId != null && !form.getValues("categoryId")) {
46
+ form.setValue("categoryId", defaultCategoryId.toString(), {
47
+ shouldDirty: false,
48
+ });
49
+ }
50
+ }, [defaultCategoryId, form]);
51
+
52
+ const { options: categoryOptions } = useSelect({
53
+ resource: "categories",
54
+ defaultValue: defaultCategoryId?.toString(),
55
+ optionLabel: "title",
56
+ optionValue: "id",
57
+ pagination: {
58
+ currentPage: 1,
59
+ pageSize: 100,
60
+ },
61
+ });
62
+
63
+ function onSubmit(values: Record<string, string>) {
64
+ onFinish(values);
65
+ }
66
+
67
+ return (
68
+ <EditView>
69
+ <Form {...form}>
70
+ <form onSubmit={form.handleSubmit(onSubmit)} className="resource-form">
71
+ <FormField
72
+ control={form.control}
73
+ name="title"
74
+ rules={{ required: "Title is required" }}
75
+ render={({ field }) => (
76
+ <FormItem>
77
+ <FormLabel>Title</FormLabel>
78
+ <FormControl
79
+ render={<Input
80
+ {...field}
81
+ value={field.value || ""}
82
+ placeholder="Enter title"
83
+ />}
84
+ />
85
+ <FormMessage />
86
+ </FormItem>
87
+ )}
88
+ />
89
+
90
+ <FormField
91
+ control={form.control}
92
+ name="content"
93
+ rules={{ required: "Content is required" }}
94
+ render={({ field }) => (
95
+ <FormItem>
96
+ <FormLabel>Content</FormLabel>
97
+ <FormControl
98
+ render={<Textarea
99
+ {...field}
100
+ value={field.value || ""}
101
+ placeholder="Enter content"
102
+ rows={10}
103
+ />}
104
+ />
105
+ <FormMessage />
106
+ </FormItem>
107
+ )}
108
+ />
109
+
110
+ <FormField
111
+ control={form.control}
112
+ name="categoryId"
113
+ rules={{ required: "Category is required" }}
114
+ render={({ field }) => {
115
+ const selectedCategoryId = field.value ?? defaultCategoryId;
116
+
117
+ return <FormItem>
118
+ <FormLabel>Category</FormLabel>
119
+ <Select
120
+ items={categoryOptions?.map((option) => ({
121
+ label: option.label,
122
+ value: option.value.toString(),
123
+ }))}
124
+ onValueChange={field.onChange}
125
+ value={selectedCategoryId?.toString() || ""}
126
+ >
127
+ <FormControl
128
+ render={<SelectTrigger>
129
+ <SelectValue placeholder="Select a category" />
130
+ </SelectTrigger>}
131
+ />
132
+ <SelectContent>
133
+ {categoryOptions?.map((option) => (
134
+ <SelectItem key={option.value} value={option.value.toString()}>
135
+ {option.label}
136
+ </SelectItem>
137
+ ))}
138
+ </SelectContent>
139
+ </Select>
140
+ <FormMessage />
141
+ </FormItem>;
142
+ }}
143
+ />
144
+
145
+ <FormField
146
+ control={form.control}
147
+ name="status"
148
+ rules={{ required: "Status is required" }}
149
+ render={({ field }) => (
150
+ <FormItem>
151
+ <FormLabel>Status</FormLabel>
152
+ <Select
153
+ items={{
154
+ draft: "Draft",
155
+ published: "Published",
156
+ rejected: "Rejected",
157
+ }}
158
+ onValueChange={field.onChange}
159
+ value={field.value || ""}
160
+ >
161
+ <FormControl
162
+ render={<SelectTrigger>
163
+ <SelectValue placeholder="Select status" />
164
+ </SelectTrigger>}
165
+ />
166
+ <SelectContent>
167
+ <SelectItem value="draft">Draft</SelectItem>
168
+ <SelectItem value="published">Published</SelectItem>
169
+ <SelectItem value="rejected">Rejected</SelectItem>
170
+ </SelectContent>
171
+ </Select>
172
+ <FormMessage />
173
+ </FormItem>
174
+ )}
175
+ />
176
+
177
+ <div className="flex gap-2">
178
+ <Button
179
+ type="submit"
180
+ {...form.saveButtonProps}
181
+ disabled={form.formState.isSubmitting}
182
+ >
183
+ {form.formState.isSubmitting ? "Updating..." : "Update"}
184
+ </Button>
185
+ <Button
186
+ type="button"
187
+ variant="outline"
188
+ onClick={() => navigate(-1)}
189
+ >
190
+ Cancel
191
+ </Button>
192
+ </div>
193
+ </form>
194
+ </Form>
195
+ </EditView>
196
+ );
197
+ };
@@ -0,0 +1,4 @@
1
+ export * from "./create";
2
+ export * from "./edit";
3
+ export * from "./list";
4
+ export * from "./show";
@@ -0,0 +1,131 @@
1
+ import { useTable } from "@refinedev/react-table";
2
+ import { createColumnHelper } from "@tanstack/react-table";
3
+ import { Eye, Pencil, Trash2 } from "lucide-react";
4
+ import React from "react";
5
+
6
+ import { DataTable } from "@/components/data-table/data-table";
7
+ import { StatusBadge } from "@/components/resources/status-badge";
8
+ import { ListView } from "@/components/resources/views/list-view";
9
+ import { EditButton } from "@/components/resources/buttons/edit";
10
+ import { ShowButton } from "@/components/resources/buttons/show";
11
+ import { DeleteButton } from "@/components/resources/buttons/delete";
12
+
13
+ type BlogPost = {
14
+ id: string;
15
+ title: string;
16
+ content: string;
17
+ status: string;
18
+ createdAt: string;
19
+ category: { id: string; title: string };
20
+ };
21
+
22
+ export const BlogPostList = () => {
23
+ const columns = React.useMemo(() => {
24
+ const columnHelper = createColumnHelper<BlogPost>();
25
+
26
+ return [
27
+ columnHelper.accessor("id", {
28
+ id: "id",
29
+ header: "ID",
30
+ enableSorting: false,
31
+ }),
32
+ columnHelper.accessor("title", {
33
+ id: "title",
34
+ header: "Title",
35
+ enableSorting: true,
36
+ }),
37
+ columnHelper.accessor("content", {
38
+ id: "content",
39
+ header: "Content",
40
+ enableSorting: false,
41
+ cell: ({ getValue }) => {
42
+ const content = getValue();
43
+ if (!content) return "-";
44
+ return (
45
+ <div className="max-w-xs truncate">{content.slice(0, 80)}...</div>
46
+ );
47
+ },
48
+ }),
49
+ columnHelper.accessor("category.title", {
50
+ id: "category",
51
+ header: "Category",
52
+ enableSorting: false,
53
+ cell: ({ row }) => {
54
+ return row.original.category?.title || "-";
55
+ },
56
+ }),
57
+ columnHelper.accessor("status", {
58
+ id: "status",
59
+ header: "Status",
60
+ enableSorting: true,
61
+ cell: ({ getValue }) => {
62
+ const status = getValue();
63
+ return <StatusBadge status={status} />;
64
+ },
65
+ }),
66
+ columnHelper.accessor("createdAt", {
67
+ id: "createdAt",
68
+ header: "Created At",
69
+ enableSorting: true,
70
+ cell: ({ getValue }) => {
71
+ const date = getValue();
72
+ return date ? new Date(date).toLocaleDateString() : "-";
73
+ },
74
+ }),
75
+ columnHelper.display({
76
+ id: "actions",
77
+ header: "Actions",
78
+ cell: ({ row }) => (
79
+ <div className="flex items-center gap-1">
80
+ <EditButton
81
+ recordItemId={row.original.id}
82
+ variant="ghost"
83
+ size="icon"
84
+ aria-label="Edit post"
85
+ title="Edit"
86
+ >
87
+ <Pencil />
88
+ </EditButton>
89
+ <ShowButton
90
+ recordItemId={row.original.id}
91
+ variant="ghost"
92
+ size="icon"
93
+ aria-label="View post"
94
+ title="View"
95
+ >
96
+ <Eye />
97
+ </ShowButton>
98
+ <DeleteButton
99
+ recordItemId={row.original.id}
100
+ variant="ghost"
101
+ size="icon"
102
+ className="text-destructive hover:text-destructive"
103
+ aria-label="Delete post"
104
+ title="Delete"
105
+ >
106
+ <Trash2 />
107
+ </DeleteButton>
108
+ </div>
109
+ ),
110
+ enableSorting: false,
111
+ size: 144,
112
+ }),
113
+ ];
114
+ }, []);
115
+
116
+ const table = useTable({
117
+ columns,
118
+ refineCoreProps: {
119
+ syncWithLocation: true,
120
+ meta: {
121
+ appends: ["category"],
122
+ },
123
+ },
124
+ });
125
+
126
+ return (
127
+ <ListView>
128
+ <DataTable table={table} />
129
+ </ListView>
130
+ );
131
+ };
@@ -0,0 +1,81 @@
1
+ import { useOne, useShow } from "@refinedev/core";
2
+ import React from "react";
3
+
4
+ import { ShowView } from "@/components/resources/views/show-view";
5
+ import { StatusBadge } from "@/components/resources/status-badge";
6
+ import {
7
+ Card,
8
+ CardContent,
9
+ CardDescription,
10
+ CardHeader,
11
+ CardTitle,
12
+ } from "@/components/ui/card";
13
+ import { Separator } from "@/components/ui/separator";
14
+
15
+ export const BlogPostShow = () => {
16
+ const { result: record, query } = useShow({});
17
+
18
+ const {
19
+ result: category,
20
+ query: { isLoading: categoryIsLoading },
21
+ } = useOne({
22
+ resource: "categories",
23
+ id: record?.category?.id || "",
24
+ queryOptions: {
25
+ enabled: !!record,
26
+ },
27
+ });
28
+ const { isLoading } = query;
29
+
30
+ return (
31
+ <ShowView>
32
+ <div className="max-w-4xl space-y-6">
33
+ <Card className="resource-detail-card">
34
+ <CardHeader>
35
+ <CardTitle>{record?.title}</CardTitle>
36
+ <CardDescription>
37
+ <div className="flex items-center gap-4">
38
+ <StatusBadge status={record?.status} />
39
+ <span className="text-sm text-muted-foreground">
40
+ ID: {record?.id}
41
+ </span>
42
+ </div>
43
+ </CardDescription>
44
+ </CardHeader>
45
+ <CardContent className="space-y-4">
46
+ <div>
47
+ <h4 className="text-sm font-medium mb-2">Category</h4>
48
+ <p className="text-sm text-muted-foreground">
49
+ {categoryIsLoading ? "Loading..." : category?.title || "-"}
50
+ </p>
51
+ </div>
52
+
53
+ <Separator />
54
+
55
+ <div>
56
+ <h4 className="text-sm font-medium mb-2">Created At</h4>
57
+ <p className="text-sm text-muted-foreground">
58
+ {record?.createdAt
59
+ ? new Date(record.createdAt).toLocaleDateString()
60
+ : "-"}
61
+ </p>
62
+ </div>
63
+
64
+ <Separator />
65
+
66
+ <div>
67
+ <h4 className="text-sm font-medium mb-4">Content</h4>
68
+ <div className="prose prose-sm max-w-none">
69
+ {record?.content ? (
70
+ <div dangerouslySetInnerHTML={{ __html: record.content }} />
71
+ ) : (
72
+ <p className="text-muted-foreground">No content available</p>
73
+ )}
74
+ </div>
75
+ </div>
76
+ </CardContent>
77
+ </Card>
78
+ </div>
79
+ </ShowView>
80
+ );
81
+ };
@@ -0,0 +1,73 @@
1
+ import { useForm } from "@refinedev/react-hook-form";
2
+ import { useNavigate } from "react-router";
3
+
4
+ import { CreateView } from "@/components/resources/views/create-view";
5
+ import { Button } from "@/components/ui/button";
6
+ import { Input } from "@/components/ui/input";
7
+ import {
8
+ Form,
9
+ FormControl,
10
+ FormField,
11
+ FormItem,
12
+ FormLabel,
13
+ FormMessage,
14
+ } from "@/components/ui/form";
15
+
16
+ export const CategoryCreate = () => {
17
+ const navigate = useNavigate();
18
+
19
+ const {
20
+ refineCore: { onFinish },
21
+ ...form
22
+ } = useForm({
23
+ refineCoreProps: {},
24
+ });
25
+
26
+ function onSubmit(values: Record<string, string>) {
27
+ onFinish(values);
28
+ }
29
+
30
+ return (
31
+ <CreateView>
32
+ <Form {...form}>
33
+ <form onSubmit={form.handleSubmit(onSubmit)} className="resource-form">
34
+ <FormField
35
+ control={form.control}
36
+ name="title"
37
+ rules={{ required: "Title is required" }}
38
+ render={({ field }) => (
39
+ <FormItem>
40
+ <FormLabel>Title</FormLabel>
41
+ <FormControl
42
+ render={<Input
43
+ {...field}
44
+ value={field.value || ""}
45
+ placeholder="Enter category title"
46
+ />}
47
+ />
48
+ <FormMessage />
49
+ </FormItem>
50
+ )}
51
+ />
52
+
53
+ <div className="flex gap-2">
54
+ <Button
55
+ type="submit"
56
+ {...form.saveButtonProps}
57
+ disabled={form.formState.isSubmitting}
58
+ >
59
+ {form.formState.isSubmitting ? "Creating..." : "Create"}
60
+ </Button>
61
+ <Button
62
+ type="button"
63
+ variant="outline"
64
+ onClick={() => navigate(-1)}
65
+ >
66
+ Cancel
67
+ </Button>
68
+ </div>
69
+ </form>
70
+ </Form>
71
+ </CreateView>
72
+ );
73
+ };