@arch-cadre/panel 1.0.6 → 1.0.9

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 (108) hide show
  1. package/dist/actions/activity-log/index.cjs +1 -1
  2. package/dist/actions/activity-log/index.mjs +1 -1
  3. package/dist/actions/index.cjs +2 -2
  4. package/dist/actions/index.d.ts +2 -2
  5. package/dist/actions/index.mjs +2 -2
  6. package/dist/actions/profile.d.ts +1 -1
  7. package/dist/index.cjs +7 -7
  8. package/dist/index.mjs +7 -7
  9. package/dist/routes.cjs +10 -10
  10. package/dist/routes.mjs +10 -10
  11. package/dist/schema.cjs +1 -1
  12. package/dist/schema.d.ts +1 -1
  13. package/dist/schema.mjs +1 -1
  14. package/dist/ui/activity-log/components/ActivityStatsWidget.cjs +1 -1
  15. package/dist/ui/activity-log/components/ActivityStatsWidget.mjs +1 -1
  16. package/dist/ui/activity-log/components/RecentLogsWidget.cjs +1 -1
  17. package/dist/ui/activity-log/components/RecentLogsWidget.mjs +1 -1
  18. package/dist/ui/activity-log/pages/log-list.cjs +2 -2
  19. package/dist/ui/activity-log/pages/log-list.mjs +1 -1
  20. package/dist/ui/components/app-content.cjs +1 -1
  21. package/dist/ui/components/app-content.mjs +1 -1
  22. package/dist/ui/components/app-sidebar.cjs +3 -3
  23. package/dist/ui/components/app-sidebar.mjs +2 -2
  24. package/dist/ui/components/app-user.cjs +5 -5
  25. package/dist/ui/components/app-user.mjs +4 -4
  26. package/dist/ui/components/breadcrumb-slot.cjs +2 -2
  27. package/dist/ui/components/breadcrumb-slot.mjs +1 -1
  28. package/dist/ui/components/manager/module-card.cjs +3 -3
  29. package/dist/ui/components/manager/module-card.mjs +2 -2
  30. package/dist/ui/components/manager/module-list.cjs +3 -3
  31. package/dist/ui/components/manager/module-list.mjs +2 -2
  32. package/dist/ui/components/manager/module-upload.cjs +3 -3
  33. package/dist/ui/components/manager/module-upload.mjs +2 -2
  34. package/dist/ui/components/profile/components.cjs +7 -7
  35. package/dist/ui/components/profile/components.d.ts +1 -1
  36. package/dist/ui/components/profile/components.mjs +3 -3
  37. package/dist/ui/components/profile/link.cjs +2 -2
  38. package/dist/ui/components/profile/link.mjs +1 -1
  39. package/dist/ui/components/profile/page.cjs +2 -6
  40. package/dist/ui/components/profile/page.mjs +3 -10
  41. package/dist/ui/components/sidebar-slot.cjs +1 -1
  42. package/dist/ui/components/sidebar-slot.mjs +1 -1
  43. package/dist/ui/dashboard/page.cjs +3 -3
  44. package/dist/ui/dashboard/page.mjs +1 -1
  45. package/dist/ui/dashboard/widgets/WelcomeBackUserWidget.cjs +1 -0
  46. package/dist/ui/dashboard/widgets/WelcomeBackUserWidget.mjs +5 -4
  47. package/dist/ui/error.cjs +2 -2
  48. package/dist/ui/error.mjs +1 -1
  49. package/dist/ui/layout.cjs +3 -3
  50. package/dist/ui/layout.mjs +3 -3
  51. package/dist/ui/modules/docs/page.cjs +1 -1
  52. package/dist/ui/modules/docs/page.mjs +1 -1
  53. package/dist/ui/modules/page.cjs +3 -3
  54. package/dist/ui/modules/page.mjs +3 -3
  55. package/dist/ui/rbac/pages/rbac-admin.cjs +16 -16
  56. package/dist/ui/rbac/pages/rbac-admin.mjs +2 -2
  57. package/dist/ui/router.cjs +1 -1
  58. package/dist/ui/router.mjs +1 -1
  59. package/dist/ui/session-manager/components/sessions-list.cjs +7 -7
  60. package/dist/ui/session-manager/components/sessions-list.mjs +3 -3
  61. package/dist/ui/session-manager/pages/sessions-page.cjs +4 -4
  62. package/dist/ui/session-manager/pages/sessions-page.mjs +3 -3
  63. package/dist/ui/settings-page.cjs +3 -3
  64. package/dist/ui/settings-page.mjs +2 -2
  65. package/package.json +7 -6
  66. package/src/actions/actions.ts +17 -0
  67. package/src/actions/activity-log/index.ts +17 -0
  68. package/src/actions/index.ts +2 -0
  69. package/src/actions/manager.ts +168 -0
  70. package/src/actions/profile.ts +173 -0
  71. package/src/actions/rbac/index.ts +131 -0
  72. package/src/actions/session-manager/index.ts +87 -0
  73. package/src/actions/settings.ts +34 -0
  74. package/src/index.ts +135 -0
  75. package/src/intl.d.ts +9 -0
  76. package/src/navigation.ts +57 -0
  77. package/src/routes.ts +107 -0
  78. package/src/schema/activity-log.ts +16 -0
  79. package/src/schema.ts +1 -0
  80. package/src/types.ts +18 -0
  81. package/src/ui/activity-log/components/ActivityStatsWidget.tsx +37 -0
  82. package/src/ui/activity-log/components/RecentLogsWidget.tsx +74 -0
  83. package/src/ui/activity-log/pages/log-list.tsx +91 -0
  84. package/src/ui/components/app-content.tsx +51 -0
  85. package/src/ui/components/app-header.tsx +65 -0
  86. package/src/ui/components/app-sidebar.tsx +249 -0
  87. package/src/ui/components/app-user.tsx +126 -0
  88. package/src/ui/components/breadcrumb-slot.tsx +52 -0
  89. package/src/ui/components/manager/module-card.tsx +327 -0
  90. package/src/ui/components/manager/module-list.tsx +59 -0
  91. package/src/ui/components/manager/module-upload.tsx +84 -0
  92. package/src/ui/components/profile/components.tsx +311 -0
  93. package/src/ui/components/profile/link.tsx +36 -0
  94. package/src/ui/components/profile/page.tsx +45 -0
  95. package/src/ui/components/sidebar-slot.tsx +47 -0
  96. package/src/ui/dashboard/page.tsx +17 -0
  97. package/src/ui/dashboard/widgets/WelcomeBackUserWidget.tsx +47 -0
  98. package/src/ui/error.tsx +82 -0
  99. package/src/ui/layout.tsx +54 -0
  100. package/src/ui/modules/docs/page.tsx +105 -0
  101. package/src/ui/modules/page.tsx +30 -0
  102. package/src/ui/page.tsx +15 -0
  103. package/src/ui/rbac/pages/rbac-admin.tsx +551 -0
  104. package/src/ui/router.tsx +69 -0
  105. package/src/ui/session-manager/components/sessions-list.tsx +303 -0
  106. package/src/ui/session-manager/pages/sessions-page.tsx +22 -0
  107. package/src/ui/settings/page.tsx +73 -0
  108. package/src/ui/settings-page.tsx +97 -0
@@ -0,0 +1,69 @@
1
+ import { checkSecurity, getCurrentSession } from "@arch-cadre/core/server";
2
+ import { getKryoModuleRoutes } from "@arch-cadre/modules/server";
3
+ import { redirect } from "next/navigation";
4
+ import * as React from "react";
5
+ import KryoLayout from "./layout.js";
6
+
7
+ function matchRoute(pattern: string, path: string) {
8
+ const paramNames: string[] = [];
9
+ const regexPattern = pattern
10
+ .replace(/:([^/]+)/g, (_, name) => {
11
+ paramNames.push(name);
12
+ return "([^/]+)";
13
+ })
14
+ .replace(/\//g, "\\/");
15
+
16
+ const regex = new RegExp(`^${regexPattern}$`);
17
+ const match = path.match(regex);
18
+
19
+ if (!match) return null;
20
+
21
+ const params: Record<string, string> = {};
22
+ paramNames.forEach((name, index) => {
23
+ params[name] = match[index + 1];
24
+ });
25
+
26
+ return params;
27
+ }
28
+
29
+ export async function KryoRouter({
30
+ path,
31
+ searchParams,
32
+ }: {
33
+ path: string;
34
+ searchParams: any;
35
+ }) {
36
+ const routes = await getKryoModuleRoutes();
37
+
38
+ for (const route of routes) {
39
+ const params = matchRoute(route.path, path);
40
+
41
+ if (params) {
42
+ if (route.auth) {
43
+ const { user, session } = await getCurrentSession();
44
+ if (!user || !session) {
45
+ return redirect("/signin");
46
+ }
47
+
48
+ const security = await checkSecurity(
49
+ session,
50
+ user,
51
+ route.roles,
52
+ route.permissions,
53
+ );
54
+
55
+ if (!security.satisfied) {
56
+ return redirect(security.redirect || "/signin");
57
+ }
58
+ }
59
+
60
+ const Component = route.component;
61
+ const Layout = route.layout || KryoLayout;
62
+ const content = <Component params={params} searchParams={searchParams} />;
63
+
64
+ return <Layout>{content}</Layout>;
65
+ }
66
+ }
67
+
68
+ return null; // Delegate back to 404
69
+ }
@@ -0,0 +1,303 @@
1
+ "use client";
2
+
3
+ import { useTranslation } from "@arch-cadre/intl";
4
+ import { cn, Icon, toast } from "@arch-cadre/ui";
5
+ import {
6
+ AlertDialog,
7
+ AlertDialogAction,
8
+ AlertDialogCancel,
9
+ AlertDialogContent,
10
+ AlertDialogDescription,
11
+ AlertDialogFooter,
12
+ AlertDialogHeader,
13
+ AlertDialogTitle,
14
+ AlertDialogTrigger,
15
+ } from "@arch-cadre/ui/components/alert-dialog";
16
+ import { Badge } from "@arch-cadre/ui/components/badge";
17
+ import { Button } from "@arch-cadre/ui/components/button";
18
+ import { useRouter } from "next/navigation";
19
+ import * as React from "react";
20
+ import { useState, useTransition } from "react";
21
+ import {
22
+ revokeAllOtherSessionsAction,
23
+ revokeSessionAction,
24
+ } from "../../../actions/session-manager/index.js";
25
+
26
+ type Session = {
27
+ id: string;
28
+ createdAt: Date;
29
+ expiresAt: Date;
30
+ isCurrent: boolean;
31
+ [key: string]: any;
32
+ };
33
+
34
+ interface SessionsListProps {
35
+ sessions: Session[];
36
+ }
37
+
38
+ const CORE_SESSION_KEYS = [
39
+ "id",
40
+ "createdAt",
41
+ "expiresAt",
42
+ "isCurrent",
43
+ "userId",
44
+ ];
45
+
46
+ export function SessionsList({ sessions }: SessionsListProps) {
47
+ const router = useRouter();
48
+ const [isPending, startTransition] = useTransition();
49
+ const [revokingSessionId, setRevokingSessionId] = useState<string | null>(
50
+ null,
51
+ );
52
+ const { t } = useTranslation();
53
+
54
+ // Helper to get augmentation badges
55
+ const renderAugmentations = (session: Session) => {
56
+ return Object.entries(session)
57
+ .filter(([key]) => !CORE_SESSION_KEYS.includes(key))
58
+ .map(([key, value]) => {
59
+ // Special formatting for known augmentations like 2FA
60
+ if (key === "twoFactorVerified" || key === "two_factor_verified") {
61
+ return (
62
+ <Badge
63
+ key={key}
64
+ variant="outline"
65
+ className={cn(
66
+ value
67
+ ? "text-[10px] px-1.5 text-green-600 border-green-200"
68
+ : "text-[10px] px-1.5 text-red-600 border-red-200",
69
+ )}
70
+ >
71
+ <Icon
72
+ icon="solar:shield-check-broken"
73
+ className="h-2.5 w-2.5 mr-0.5"
74
+ />
75
+ {t("2FA")}
76
+ </Badge>
77
+ );
78
+ }
79
+
80
+ // Generic badge for other augmentations
81
+ if (typeof value === "boolean" && value === false) return null;
82
+
83
+ return (
84
+ <Badge
85
+ key={key}
86
+ variant="outline"
87
+ className="text-[10px] px-1.5 text-muted-foreground uppercase font-black tracking-widest"
88
+ >
89
+ {t(key)}
90
+ {typeof value !== "boolean" && `: ${value}`}
91
+ </Badge>
92
+ );
93
+ });
94
+ };
95
+
96
+ const handleRevokeSession = (sessionId: string) => {
97
+ setRevokingSessionId(sessionId);
98
+ startTransition(async () => {
99
+ const result = await revokeSessionAction(sessionId);
100
+ if (result.error) {
101
+ toast.error(result.error);
102
+ } else {
103
+ toast.success(t("Session revoked successfully"));
104
+ router.refresh();
105
+ }
106
+ setRevokingSessionId(null);
107
+ });
108
+ };
109
+
110
+ const handleRevokeAllOther = () => {
111
+ startTransition(async () => {
112
+ const result = await revokeAllOtherSessionsAction();
113
+ if (result.error) {
114
+ toast.error(result.error);
115
+ } else {
116
+ toast.success(t("All other sessions revoked successfully"));
117
+ router.refresh();
118
+ }
119
+ });
120
+ };
121
+
122
+ const isExpiringSoon = (expiresAt: Date) => {
123
+ const now = new Date();
124
+ const diffDays =
125
+ (expiresAt.getTime() - now.getTime()) / (1000 * 60 * 60 * 24);
126
+ return diffDays < 7;
127
+ };
128
+
129
+ const otherSessions = sessions.filter((s) => !s.isCurrent);
130
+
131
+ return (
132
+ <div className="space-y-4">
133
+ {/* Revoke All Button */}
134
+ {otherSessions.length > 0 && (
135
+ <div className="flex justify-end">
136
+ <AlertDialog>
137
+ <AlertDialogTrigger asChild>
138
+ <Button variant="outline" size="sm" disabled={isPending}>
139
+ <Icon icon="solar:logout-2-broken" className="h-4 w-4 mr-2" />
140
+ {t("Revoke All Other Sessions")}
141
+ </Button>
142
+ </AlertDialogTrigger>
143
+ <AlertDialogContent>
144
+ <AlertDialogHeader>
145
+ <AlertDialogTitle>
146
+ {t("Revoke All Other Sessions?")}
147
+ </AlertDialogTitle>
148
+ <AlertDialogDescription>
149
+ {t(
150
+ "This will sign out all other devices. You will remain signed in on this device. This action cannot be undone.",
151
+ )}
152
+ </AlertDialogDescription>
153
+ </AlertDialogHeader>
154
+ <AlertDialogFooter>
155
+ <AlertDialogCancel>{t("Cancel")}</AlertDialogCancel>
156
+ <AlertDialogAction
157
+ onClick={handleRevokeAllOther}
158
+ disabled={isPending}
159
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
160
+ >
161
+ {isPending ? t("Revoking...") : t("Revoke All")}
162
+ </AlertDialogAction>
163
+ </AlertDialogFooter>
164
+ </AlertDialogContent>
165
+ </AlertDialog>
166
+ </div>
167
+ )}
168
+
169
+ {/* Sessions List */}
170
+ <div className="space-y-3">
171
+ {sessions.map((session) => (
172
+ <div
173
+ key={session.id}
174
+ className={cn(
175
+ "flex items-center justify-between p-4 rounded-lg border bg-card",
176
+ session.isCurrent && "ring-2 ring-primary",
177
+ )}
178
+ >
179
+ <div className="flex items-center gap-4">
180
+ <div
181
+ className={cn(
182
+ "flex h-10 w-10 items-center justify-center rounded-full",
183
+ session.isCurrent
184
+ ? "bg-primary/10 text-primary"
185
+ : "bg-muted text-muted-foreground",
186
+ )}
187
+ >
188
+ <Icon icon="solar:monitor-broken" className="h-5 w-5" />
189
+ </div>
190
+ <div className="space-y-1">
191
+ <div className="flex items-center gap-2">
192
+ <span className="font-medium text-sm">
193
+ {session.isCurrent ? t("Current Session") : t("Session")}
194
+ </span>
195
+ {session.isCurrent && (
196
+ <Badge variant="secondary" className="text-[10px] px-1.5">
197
+ {t("This device")}
198
+ </Badge>
199
+ )}
200
+
201
+ {renderAugmentations(session)}
202
+ </div>
203
+ <div className="flex items-center gap-3 text-xs text-muted-foreground">
204
+ <span className="flex items-center gap-1">
205
+ <Icon
206
+ icon="solar:clock-circle-broken"
207
+ className="h-3 w-3"
208
+ />
209
+ {t("Started {{ time }}", {
210
+ time: session.createdAt,
211
+ })}
212
+ </span>
213
+ <span className="flex items-center gap-1">
214
+ <Icon
215
+ icon="solar:hourglass-line-broken"
216
+ className={cn(
217
+ "h-3 w-3",
218
+ isExpiringSoon(session.expiresAt) && "text-orange-500",
219
+ )}
220
+ />
221
+ {t("Expires {{ time }}", {
222
+ time: session.expiresAt,
223
+ })}
224
+ </span>
225
+ </div>
226
+ </div>
227
+ </div>
228
+
229
+ {/* Actions */}
230
+ {!session.isCurrent && (
231
+ <AlertDialog>
232
+ <AlertDialogTrigger asChild>
233
+ <Button
234
+ variant="ghost"
235
+ size="sm"
236
+ className="text-muted-foreground hover:text-destructive"
237
+ disabled={isPending && revokingSessionId === session.id}
238
+ >
239
+ {isPending && revokingSessionId === session.id ? (
240
+ <Icon
241
+ icon="solar:refresh-broken"
242
+ className="h-4 w-4 animate-spin"
243
+ />
244
+ ) : (
245
+ <>
246
+ <Icon
247
+ icon="solar:logout-2-broken"
248
+ className="h-4 w-4 mr-1"
249
+ />
250
+ {t("Revoke")}
251
+ </>
252
+ )}
253
+ </Button>
254
+ </AlertDialogTrigger>
255
+ <AlertDialogContent>
256
+ <AlertDialogHeader>
257
+ <AlertDialogTitle>{t("Revoke Session?")}</AlertDialogTitle>
258
+ <AlertDialogDescription>
259
+ {t(
260
+ "This will sign out this device immediately. The session was started {{ time }}.",
261
+ {
262
+ time: session.createdAt,
263
+ },
264
+ )}
265
+ </AlertDialogDescription>
266
+ </AlertDialogHeader>
267
+ <AlertDialogFooter>
268
+ <AlertDialogCancel>{t("Cancel")}</AlertDialogCancel>
269
+ <AlertDialogAction
270
+ onClick={() => handleRevokeSession(session.id)}
271
+ disabled={isPending}
272
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
273
+ >
274
+ {isPending ? t("Revoking...") : t("Revoke")}
275
+ </AlertDialogAction>
276
+ </AlertDialogFooter>
277
+ </AlertDialogContent>
278
+ </AlertDialog>
279
+ )}
280
+ </div>
281
+ ))}
282
+ </div>
283
+
284
+ {/* Empty State */}
285
+ {sessions.length === 0 && (
286
+ <div className="text-center py-8 text-muted-foreground">
287
+ <Icon
288
+ icon="solar:devices-broken"
289
+ className="h-12 w-12 mx-auto mb-3 opacity-50"
290
+ />
291
+ <p>{t("No active sessions found")}</p>
292
+ </div>
293
+ )}
294
+
295
+ {/* Only Current Session */}
296
+ {sessions.length === 1 && sessions[0].isCurrent && (
297
+ <p className="text-sm text-muted-foreground text-center py-4">
298
+ {t("You only have one active session (this device).")}
299
+ </p>
300
+ )}
301
+ </div>
302
+ );
303
+ }
@@ -0,0 +1,22 @@
1
+ import { getTranslation } from "@arch-cadre/intl/server";
2
+ import * as React from "react";
3
+ import { getSessionsAction } from "../../../actions/session-manager/index.js";
4
+ import { SessionsList } from "../components/sessions-list.js";
5
+
6
+ export default async function SessionsSettingsPage() {
7
+ const { t } = await getTranslation();
8
+ const { sessions } = await getSessionsAction();
9
+
10
+ return (
11
+ <div className="space-y-6">
12
+ <div>
13
+ <h2 className="text-2xl font-bold">{t("Active Sessions")}</h2>
14
+ <p className="text-muted-foreground">
15
+ {t("Manage your active sessions and sign out from other devices.")}
16
+ </p>
17
+ </div>
18
+
19
+ <SessionsList sessions={sessions} />
20
+ </div>
21
+ );
22
+ }
@@ -0,0 +1,73 @@
1
+ import { getTranslation } from "@arch-cadre/intl/server";
2
+ import { getKryoModuleNavigationGrouped } from "@arch-cadre/modules/server";
3
+ import {
4
+ Card,
5
+ CardContent,
6
+ CardDescription,
7
+ CardHeader,
8
+ CardTitle,
9
+ } from "@arch-cadre/ui/components/card";
10
+ import { Icon } from "@iconify/react";
11
+ import Link from "next/link";
12
+ import * as React from "react";
13
+
14
+ export default async function GlobalSettingsPage() {
15
+ const groups = await getKryoModuleNavigationGrouped("settings");
16
+ const { t } = await getTranslation();
17
+
18
+ return (
19
+ <div className="container mx-auto py-10 px-4 space-y-8">
20
+ <div>
21
+ <h1 className="text-3xl font-bold tracking-tight">
22
+ {t("Settings Overview")}
23
+ </h1>
24
+ <p className="text-muted-foreground">
25
+ {t("Manage your global settings")}
26
+ </p>
27
+ </div>
28
+
29
+ <div className="grid gap-6">
30
+ {Object.entries(groups).map(([groupName, items]) => (
31
+ <div key={groupName} className="space-y-4">
32
+ <h2 className="text-xl font-semibold tracking-tight px-1">
33
+ {groupName}
34
+ </h2>
35
+ <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
36
+ {items.map((item) => (
37
+ <Link key={item.url} href={item.url} className="block group">
38
+ <Card className="h-full transition-colors hover:border-primary/50 hover:bg-accent/5">
39
+ <CardHeader className="flex flex-row items-center space-y-0 gap-4">
40
+ <div className="size-10 flex items-center justify-center rounded-lg bg-primary/10 text-primary group-hover:bg-primary group-hover:text-primary-foreground transition-colors">
41
+ {item.icon ? (
42
+ <Icon icon={item.icon} className="size-5" />
43
+ ) : (
44
+ <Icon
45
+ icon="solar:settings-linear"
46
+ className="size-5"
47
+ />
48
+ )}
49
+ </div>
50
+ <div className="space-y-1">
51
+ <CardTitle className="text-base">
52
+ {t(item.title as any)}
53
+ </CardTitle>
54
+ </div>
55
+ </CardHeader>
56
+ </Card>
57
+ </Link>
58
+ ))}
59
+ </div>
60
+ </div>
61
+ ))}
62
+
63
+ {Object.keys(groups).length === 0 && (
64
+ <Card className="border-dashed">
65
+ <CardContent className="py-10 text-center text-muted-foreground">
66
+ {t("No settings available")}
67
+ </CardContent>
68
+ </Card>
69
+ )}
70
+ </div>
71
+ </div>
72
+ );
73
+ }
@@ -0,0 +1,97 @@
1
+ "use client";
2
+
3
+ import { useTranslation } from "@arch-cadre/intl";
4
+
5
+ import { Button } from "@arch-cadre/ui/components/button";
6
+ import {
7
+ Card,
8
+ CardContent,
9
+ CardDescription,
10
+ CardHeader,
11
+ CardTitle,
12
+ } from "@arch-cadre/ui/components/card";
13
+ import { Input } from "@arch-cadre/ui/components/input";
14
+ import { Label } from "@arch-cadre/ui/components/label";
15
+ import * as React from "react";
16
+ import { useEffect, useState } from "react";
17
+ import { toast } from "sonner";
18
+ import {
19
+ getKryoConfig,
20
+ type KryoPanelConfig,
21
+ updateKryoConfig,
22
+ } from "../actions/settings.js";
23
+
24
+ export default function KryoPanelSettingsPage() {
25
+ const [config, setConfig] = useState<KryoPanelConfig>({
26
+ pathPrefix: "/kryo",
27
+ });
28
+
29
+ const { t } = useTranslation();
30
+
31
+ const [loading, setLoading] = useState(true);
32
+
33
+ useEffect(() => {
34
+ getKryoConfig().then((data) => {
35
+ setConfig(data);
36
+ setLoading(false);
37
+ });
38
+ }, []);
39
+
40
+ const handleSave = async (e: React.FormEvent) => {
41
+ e.preventDefault();
42
+ try {
43
+ await updateKryoConfig(config);
44
+ toast.success(t("Panel settings saved. Your admin path has changed!"));
45
+
46
+ // Redirect to the settings page under the new prefix
47
+ if (typeof window !== "undefined") {
48
+ const newPath = config.pathPrefix.endsWith("/")
49
+ ? `${config.pathPrefix}config`
50
+ : `${config.pathPrefix}/config`;
51
+ window.location.href = newPath;
52
+ }
53
+ } catch (error: any) {
54
+ toast.error(error.message || t("Failed to save settings"));
55
+ }
56
+ };
57
+
58
+ if (loading) return <div className="p-10 text-center">{t("Loading...")}</div>;
59
+
60
+ return (
61
+ <div className="container mx-auto py-10 max-w-2xl px-4">
62
+ <Card className="border-primary/10">
63
+ <CardHeader>
64
+ <CardTitle>{t("Kryo Panel Settings")}</CardTitle>
65
+ <CardDescription>{t("Manage kryo-panel settings")}</CardDescription>
66
+ </CardHeader>
67
+ <CardContent>
68
+ <form onSubmit={handleSave} className="space-y-6">
69
+ <div className="space-y-2">
70
+ <Label htmlFor="pathPrefix">{t("Path Prefix")}</Label>
71
+ <div className="flex gap-2">
72
+ <Input
73
+ id="pathPrefix"
74
+ value={config.pathPrefix}
75
+ onChange={(e) =>
76
+ setConfig({ ...config, pathPrefix: e.target.value })
77
+ }
78
+ placeholder="/kryo"
79
+ />
80
+ </div>
81
+ <p className="text-[11px] italic bg-amber-50 p-2 rounded squircle border border-amber-100 text-amber-800">
82
+ <strong>{t("Warning")}</strong>{" "}
83
+ {t(
84
+ "Warning changing the path prefix may affect existing links and bookmarks. Make sure to update any references to the admin panel URL accordingly.",
85
+ )}
86
+ </p>
87
+ </div>
88
+
89
+ <Button type="submit" className="w-full">
90
+ {t("Save Configuration")}
91
+ </Button>
92
+ </form>
93
+ </CardContent>
94
+ </Card>
95
+ </div>
96
+ );
97
+ }