@simple-auth-kit/cli 1.3.1 → 1.4.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 (77) hide show
  1. package/README.md +6 -5
  2. package/package.json +1 -1
  3. package/registry/admin-apps/nextjs/shared/.husky/commit-msg +1 -0
  4. package/registry/admin-apps/nextjs/shared/.husky/pre-commit +1 -0
  5. package/registry/admin-apps/nextjs/shared/.lintstagedrc.json +4 -0
  6. package/registry/admin-apps/nextjs/shared/.prettierrc.json +3 -0
  7. package/registry/admin-apps/nextjs/shared/commitlint.config.js +5 -0
  8. package/registry/admin-apps/nextjs/shared/eslint.config.js +28 -0
  9. package/registry/admin-apps/nextjs/variants/base/package.json +11 -2
  10. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/audit-log/page.tsx +72 -16
  11. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/customers/[id]/edit/page.tsx +107 -26
  12. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/customers/[id]/page.tsx +131 -29
  13. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/languages/[id]/edit/page.tsx +111 -25
  14. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/languages/[id]/page.tsx +97 -22
  15. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/permissions/[id]/edit/page.tsx +129 -46
  16. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/permissions/[id]/page.tsx +64 -14
  17. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/roles/[id]/page.tsx +91 -22
  18. package/registry/admin-apps/nextjs/variants/base/src/app/(console)/users/[id]/edit/page.tsx +94 -23
  19. package/registry/admin-apps/nextjs/variants/workspaces/package.json +11 -2
  20. package/registry/admin-apps/nextjs/variants/workspaces/src/app/(console)/account/page.tsx +179 -41
  21. package/registry/admin-apps/react/shared/.husky/commit-msg +1 -0
  22. package/registry/admin-apps/react/shared/.husky/pre-commit +1 -0
  23. package/registry/admin-apps/react/shared/.lintstagedrc.json +4 -0
  24. package/registry/admin-apps/react/shared/.prettierrc.json +3 -0
  25. package/registry/admin-apps/react/shared/commitlint.config.js +3 -0
  26. package/registry/admin-apps/react/shared/eslint.config.js +38 -0
  27. package/registry/admin-apps/react/variants/base/package.json +15 -2
  28. package/registry/admin-apps/react/variants/base/src/pages/CountryDetailPage.tsx +76 -19
  29. package/registry/admin-apps/react/variants/base/src/pages/CustomerDetailPage.tsx +110 -26
  30. package/registry/admin-apps/react/variants/base/src/pages/EditCountryPage.tsx +115 -29
  31. package/registry/admin-apps/react/variants/base/src/pages/EditCustomerPage.tsx +123 -31
  32. package/registry/admin-apps/react/variants/base/src/pages/EditLanguagePage.tsx +100 -24
  33. package/registry/admin-apps/react/variants/base/src/pages/EditRolePage.tsx +99 -26
  34. package/registry/admin-apps/react/variants/base/src/pages/EditUserPage.tsx +126 -31
  35. package/registry/admin-apps/react/variants/base/src/pages/LanguageDetailPage.tsx +75 -19
  36. package/registry/admin-apps/react/variants/base/src/pages/PermissionDetailPage.tsx +58 -13
  37. package/registry/admin-apps/react/variants/base/src/pages/PermissionsPage.tsx +132 -29
  38. package/registry/admin-apps/react/variants/base/src/pages/RoleDetailPage.tsx +70 -17
  39. package/registry/admin-apps/react/variants/base/src/pages/RolesPage.tsx +277 -59
  40. package/registry/admin-apps/react/variants/workspaces/package.json +15 -2
  41. package/registry/combos/express-drizzle/eslint.config.js +18 -0
  42. package/registry/combos/express-drizzle/package.json +5 -1
  43. package/registry/combos/express-drizzle/shared/src/request-context.ts +3 -0
  44. package/registry/combos/express-prisma/eslint.config.js +18 -0
  45. package/registry/combos/express-prisma/package.json +5 -1
  46. package/registry/combos/express-prisma/shared/src/request-context.ts +3 -0
  47. package/registry/combos/nestjs-drizzle/eslint.config.js +18 -0
  48. package/registry/combos/nestjs-drizzle/package.json +5 -1
  49. package/registry/combos/nestjs-drizzle/shared/src/auth.controller.ts +138 -33
  50. package/registry/combos/nestjs-drizzle/shared/src/route-tiers.ts +71 -18
  51. package/registry/combos/nestjs-prisma/eslint.config.js +18 -0
  52. package/registry/combos/nestjs-prisma/package.json +5 -1
  53. package/registry/combos/nestjs-prisma/shared/src/auth.controller.ts +148 -35
  54. package/registry/combos/nestjs-prisma/shared/src/route-tiers.ts +72 -18
  55. package/registry/mobile-apps/bare-rn/shared/.husky/commit-msg +1 -0
  56. package/registry/mobile-apps/bare-rn/shared/.husky/pre-commit +1 -0
  57. package/registry/mobile-apps/bare-rn/shared/.lintstagedrc.json +4 -0
  58. package/registry/mobile-apps/bare-rn/shared/.prettierrc.json +5 -0
  59. package/registry/mobile-apps/bare-rn/shared/commitlint.config.js +3 -0
  60. package/registry/mobile-apps/bare-rn/shared/eslint.config.js +19 -0
  61. package/registry/mobile-apps/bare-rn/variants/base/package.json +17 -12
  62. package/registry/mobile-apps/bare-rn/variants/workspaces/package.json +17 -12
  63. package/registry/mobile-apps/expo/shared/.husky/commit-msg +1 -0
  64. package/registry/mobile-apps/expo/shared/.husky/pre-commit +1 -0
  65. package/registry/mobile-apps/expo/shared/.lintstagedrc.json +4 -0
  66. package/registry/mobile-apps/expo/shared/.prettierrc.json +5 -0
  67. package/registry/mobile-apps/expo/shared/commitlint.config.js +3 -0
  68. package/registry/mobile-apps/expo/shared/eslint.config.js +9 -0
  69. package/registry/mobile-apps/expo/shared/src/screens/SessionsScreen.tsx +6 -17
  70. package/registry/mobile-apps/expo/shared/src/store/sessionsStore.ts +35 -0
  71. package/registry/mobile-apps/expo/variants/base/package.json +12 -3
  72. package/registry/mobile-apps/expo/variants/workspaces/package.json +12 -3
  73. package/registry/mobile-apps/expo/variants/workspaces/src/screens/WorkspaceSelectScreen.tsx +1 -1
  74. package/registry.json +13 -36
  75. package/simple-auth-kit.ts +13 -10
  76. package/registry/mobile-apps/bare-rn/shared/.eslintrc.js +0 -4
  77. package/registry/mobile-apps/bare-rn/shared/.prettierrc.js +0 -5
@@ -1,6 +1,16 @@
1
- import { type FormEvent, useCallback, useEffect, useState } from "react";
1
+ import {
2
+ type FormEvent,
3
+ startTransition,
4
+ useCallback,
5
+ useEffect,
6
+ useState,
7
+ } from "react";
2
8
  import { useNavigate, useParams } from "react-router-dom";
3
- import type { RoleSummary, UpdateUserInput, UserSummary } from "@simple-auth-kit/auth-client";
9
+ import type {
10
+ RoleSummary,
11
+ UpdateUserInput,
12
+ UserSummary,
13
+ } from "@simple-auth-kit/auth-client";
4
14
  import { AuthApiError } from "@simple-auth-kit/auth-client";
5
15
  import { toast } from "sonner";
6
16
  import { PERMISSIONS, useAbility } from "@/lib/ability";
@@ -11,13 +21,31 @@ import { FormErrorAlert } from "@/components/form-error-alert";
11
21
  import { PhotoUpload } from "@/components/photo-upload";
12
22
  import { RoleMultiSelect } from "@/components/role-multi-select";
13
23
  import { Button } from "@/components/ui/button";
14
- import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
24
+ import {
25
+ Card,
26
+ CardContent,
27
+ CardDescription,
28
+ CardFooter,
29
+ CardHeader,
30
+ CardTitle,
31
+ } from "@/components/ui/card";
15
32
  import { Heading } from "@/components/ui/heading";
16
33
  import { Input } from "@/components/ui/input";
17
34
  import { Label } from "@/components/ui/label";
18
- import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
35
+ import {
36
+ Select,
37
+ SelectContent,
38
+ SelectItem,
39
+ SelectTrigger,
40
+ SelectValue,
41
+ } from "@/components/ui/select";
19
42
  import { Separator } from "@/components/ui/separator";
20
- import { emptyUserFields, GENDER_OPTIONS, toDateInputValue, type UserFormFields } from "./user-schema";
43
+ import {
44
+ emptyUserFields,
45
+ GENDER_OPTIONS,
46
+ toDateInputValue,
47
+ type UserFormFields,
48
+ } from "./user-schema";
21
49
 
22
50
  function apiErrorMessage(err: unknown, fallback: string): string {
23
51
  return err instanceof AuthApiError ? err.message : fallback;
@@ -36,7 +64,8 @@ function toForm(user: UserSummary): UserFormFields {
36
64
  };
37
65
  }
38
66
 
39
- const inputClassName = "bg-background border-2 focus:border-purple-500 transition-colors";
67
+ const inputClassName =
68
+ "bg-background border-2 focus:border-purple-500 transition-colors";
40
69
 
41
70
  export function EditUserPage() {
42
71
  const { id } = useParams<{ id: string }>();
@@ -71,7 +100,7 @@ export function EditUserPage() {
71
100
  }, [id]);
72
101
 
73
102
  useEffect(() => {
74
- void load();
103
+ startTransition(() => void load());
75
104
  }, [load]);
76
105
 
77
106
  useEffect(() => {
@@ -79,7 +108,9 @@ export function EditUserPage() {
79
108
  authClient
80
109
  .listRoles()
81
110
  .then(setRoleCatalog)
82
- .catch((err) => toast.error(apiErrorMessage(err, "Couldn't load the role catalog.")));
111
+ .catch((err) =>
112
+ toast.error(apiErrorMessage(err, "Couldn't load the role catalog.")),
113
+ );
83
114
  }, [canReadRoles]);
84
115
 
85
116
  if (!id) return null;
@@ -133,7 +164,9 @@ export function EditUserPage() {
133
164
  </div>
134
165
  <Separator />
135
166
 
136
- {loading && !user && <p className="text-sm text-muted-foreground">Loading…</p>}
167
+ {loading && !user && (
168
+ <p className="text-sm text-muted-foreground">Loading…</p>
169
+ )}
137
170
 
138
171
  {user && (
139
172
  <Card>
@@ -144,12 +177,16 @@ export function EditUserPage() {
144
177
  <Card className="w-full">
145
178
  <CardHeader className="border-b bg-muted/50">
146
179
  <CardTitle className="text-2xl">User Information</CardTitle>
147
- <CardDescription className="text-base">Update user&apos;s basic information</CardDescription>
180
+ <CardDescription className="text-base">
181
+ Update user&apos;s basic information
182
+ </CardDescription>
148
183
  </CardHeader>
149
184
  <CardContent className="pt-6">
150
185
  <div className="space-y-6">
151
186
  <div className="space-y-4">
152
- <h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide">Personal Information</h3>
187
+ <h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide">
188
+ Personal Information
189
+ </h3>
153
190
  <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
154
191
  <div className="flex flex-col gap-2">
155
192
  <Label htmlFor="firstName">First Name</Label>
@@ -158,7 +195,9 @@ export function EditUserPage() {
158
195
  disabled={saving}
159
196
  placeholder="First Name"
160
197
  value={form.firstName}
161
- onChange={(e) => setForm({ ...form, firstName: e.target.value })}
198
+ onChange={(e) =>
199
+ setForm({ ...form, firstName: e.target.value })
200
+ }
162
201
  type="text"
163
202
  className={inputClassName}
164
203
  />
@@ -170,7 +209,9 @@ export function EditUserPage() {
170
209
  disabled={saving}
171
210
  placeholder="Last Name"
172
211
  value={form.lastName}
173
- onChange={(e) => setForm({ ...form, lastName: e.target.value })}
212
+ onChange={(e) =>
213
+ setForm({ ...form, lastName: e.target.value })
214
+ }
174
215
  type="text"
175
216
  className={inputClassName}
176
217
  />
@@ -182,7 +223,9 @@ export function EditUserPage() {
182
223
  disabled={saving}
183
224
  placeholder="Display Name"
184
225
  value={form.displayName}
185
- onChange={(e) => setForm({ ...form, displayName: e.target.value })}
226
+ onChange={(e) =>
227
+ setForm({ ...form, displayName: e.target.value })
228
+ }
186
229
  type="text"
187
230
  className={inputClassName}
188
231
  />
@@ -190,14 +233,18 @@ export function EditUserPage() {
190
233
  <div className="flex flex-col gap-2">
191
234
  <Label htmlFor="username">
192
235
  Username &nbsp;
193
- <span className="text-xs text-destructive dark:text-destructive-foreground">(Must be unique)</span>
236
+ <span className="text-xs text-destructive dark:text-destructive-foreground">
237
+ (Must be unique)
238
+ </span>
194
239
  </Label>
195
240
  <Input
196
241
  id="username"
197
242
  disabled={saving}
198
243
  placeholder="Username"
199
244
  value={form.username}
200
- onChange={(e) => setForm({ ...form, username: e.target.value })}
245
+ onChange={(e) =>
246
+ setForm({ ...form, username: e.target.value })
247
+ }
201
248
  className={inputClassName}
202
249
  />
203
250
  </div>
@@ -205,19 +252,25 @@ export function EditUserPage() {
205
252
  </div>
206
253
 
207
254
  <div className="space-y-4">
208
- <h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide">Contact Information</h3>
255
+ <h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide">
256
+ Contact Information
257
+ </h3>
209
258
  <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
210
259
  <div className="flex flex-col gap-2">
211
260
  <Label htmlFor="phone">
212
261
  Phone &nbsp;
213
- <span className="text-xs text-muted-foreground">(With country code)</span>
262
+ <span className="text-xs text-muted-foreground">
263
+ (With country code)
264
+ </span>
214
265
  </Label>
215
266
  <Input
216
267
  id="phone"
217
268
  type="text"
218
269
  disabled={saving}
219
270
  value={form.phone}
220
- onChange={(e) => setForm({ ...form, phone: e.target.value })}
271
+ onChange={(e) =>
272
+ setForm({ ...form, phone: e.target.value })
273
+ }
221
274
  placeholder="+1234567890"
222
275
  className={inputClassName}
223
276
  />
@@ -226,7 +279,9 @@ export function EditUserPage() {
226
279
  </div>
227
280
 
228
281
  <div className="space-y-4">
229
- <h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide">Additional Details</h3>
282
+ <h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide">
283
+ Additional Details
284
+ </h3>
230
285
  <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
231
286
  <div className="flex flex-col gap-2 p-2">
232
287
  <Label htmlFor="joinedDate">Joined Date</Label>
@@ -235,7 +290,9 @@ export function EditUserPage() {
235
290
  type="date"
236
291
  disabled={saving}
237
292
  value={form.joinedDate}
238
- onChange={(e) => setForm({ ...form, joinedDate: e.target.value })}
293
+ onChange={(e) =>
294
+ setForm({ ...form, joinedDate: e.target.value })
295
+ }
239
296
  className={inputClassName}
240
297
  />
241
298
  </div>
@@ -246,19 +303,33 @@ export function EditUserPage() {
246
303
  type="date"
247
304
  disabled={saving}
248
305
  value={form.dob}
249
- onChange={(e) => setForm({ ...form, dob: e.target.value })}
306
+ onChange={(e) =>
307
+ setForm({ ...form, dob: e.target.value })
308
+ }
250
309
  className={inputClassName}
251
310
  />
252
311
  </div>
253
312
  <div className="flex flex-col gap-2">
254
313
  <Label htmlFor="gender">Gender</Label>
255
- <Select value={form.gender || undefined} onValueChange={(value) => setForm({ ...form, gender: value })} disabled={saving}>
256
- <SelectTrigger id="gender" className={`w-full ${inputClassName}`}>
314
+ <Select
315
+ value={form.gender || undefined}
316
+ onValueChange={(value) =>
317
+ setForm({ ...form, gender: value })
318
+ }
319
+ disabled={saving}
320
+ >
321
+ <SelectTrigger
322
+ id="gender"
323
+ className={`w-full ${inputClassName}`}
324
+ >
257
325
  <SelectValue placeholder="Select Gender" />
258
326
  </SelectTrigger>
259
327
  <SelectContent>
260
328
  {GENDER_OPTIONS.map((option) => (
261
- <SelectItem key={option.value} value={option.value}>
329
+ <SelectItem
330
+ key={option.value}
331
+ value={option.value}
332
+ >
262
333
  {option.label}
263
334
  </SelectItem>
264
335
  ))}
@@ -270,11 +341,18 @@ export function EditUserPage() {
270
341
 
271
342
  {canReadRoles && (
272
343
  <div className="space-y-4">
273
- <h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide">Roles</h3>
344
+ <h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide">
345
+ Roles
346
+ </h3>
274
347
  <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
275
348
  <div className="flex flex-col gap-2">
276
349
  <Label>Roles</Label>
277
- <RoleMultiSelect roles={roleCatalog} selected={selectedRoles} onChange={setSelectedRoles} disabled={!canAssignRoles} />
350
+ <RoleMultiSelect
351
+ roles={roleCatalog}
352
+ selected={selectedRoles}
353
+ onChange={setSelectedRoles}
354
+ disabled={!canAssignRoles}
355
+ />
278
356
  </div>
279
357
  </div>
280
358
  </div>
@@ -284,17 +362,34 @@ export function EditUserPage() {
284
362
 
285
363
  <CardHeader className="border-b bg-muted/50 mt-6">
286
364
  <CardTitle className="text-2xl">Profile Photo</CardTitle>
287
- <CardDescription className="text-base">Upload user&apos;s profile picture (Max size: 2MB, Formats: JPG, PNG)</CardDescription>
365
+ <CardDescription className="text-base">
366
+ Upload user&apos;s profile picture (Max size: 2MB, Formats:
367
+ JPG, PNG)
368
+ </CardDescription>
288
369
  </CardHeader>
289
370
  <CardContent className="pt-6">
290
- <PhotoUpload photo={photo} fallback="?" onChange={(next) => setPhoto(next)} disabled={saving} />
371
+ <PhotoUpload
372
+ photo={photo}
373
+ fallback="?"
374
+ onChange={(next) => setPhoto(next)}
375
+ disabled={saving}
376
+ />
291
377
  </CardContent>
292
378
 
293
379
  <CardFooter className="flex justify-center gap-4 mt-8 pb-8">
294
- <Button type="button" variant="outline" onClick={() => navigate(`/users/${id}`)} disabled={saving}>
380
+ <Button
381
+ type="button"
382
+ variant="outline"
383
+ onClick={() => navigate(`/users/${id}`)}
384
+ disabled={saving}
385
+ >
295
386
  Cancel
296
387
  </Button>
297
- <Button disabled={saving} className="bg-purple-600 hover:bg-purple-700 dark:bg-purple-600 dark:hover:bg-purple-700 min-w-32" type="submit">
388
+ <Button
389
+ disabled={saving}
390
+ className="bg-purple-600 hover:bg-purple-700 dark:bg-purple-600 dark:hover:bg-purple-700 min-w-32"
391
+ type="submit"
392
+ >
298
393
  {saving ? "Updating..." : "Update User"}
299
394
  </Button>
300
395
  </CardFooter>
@@ -1,6 +1,9 @@
1
- import { useCallback, useEffect, useState } from "react";
1
+ import { startTransition, useCallback, useEffect, useState } from "react";
2
2
  import { Link, useNavigate, useParams } from "react-router-dom";
3
- import { AuthApiError, type LanguageSummary } from "@simple-auth-kit/auth-client";
3
+ import {
4
+ AuthApiError,
5
+ type LanguageSummary,
6
+ } from "@simple-auth-kit/auth-client";
4
7
  import { format } from "date-fns";
5
8
  import { PencilIcon } from "lucide-react";
6
9
  import { toast } from "sonner";
@@ -10,7 +13,13 @@ import { cn } from "@/lib/cn";
10
13
  import { Breadcrumb } from "@/components/breadcrumb";
11
14
  import { Badge } from "@/components/ui/badge";
12
15
  import { Button, buttonVariants } from "@/components/ui/button";
13
- import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
16
+ import {
17
+ Card,
18
+ CardContent,
19
+ CardDescription,
20
+ CardHeader,
21
+ CardTitle,
22
+ } from "@/components/ui/card";
14
23
  import { Modal } from "@/components/ui/modal";
15
24
 
16
25
  function apiErrorMessage(err: unknown, fallback: string): string {
@@ -51,7 +60,7 @@ export function LanguageDetailPage() {
51
60
  }, [id]);
52
61
 
53
62
  useEffect(() => {
54
- void load();
63
+ startTransition(() => void load());
55
64
  }, [load]);
56
65
 
57
66
  async function toggleActive() {
@@ -60,10 +69,19 @@ export function LanguageDetailPage() {
60
69
  try {
61
70
  if (language.isActive) await authClient.deactivateLanguage(id);
62
71
  else await authClient.activateLanguage(id);
63
- setLanguage((prev) => (prev ? { ...prev, isActive: !prev.isActive } : prev));
64
- toast.success(language.isActive ? "Language deactivated." : "Language activated.");
72
+ setLanguage((prev) =>
73
+ prev ? { ...prev, isActive: !prev.isActive } : prev,
74
+ );
75
+ toast.success(
76
+ language.isActive ? "Language deactivated." : "Language activated.",
77
+ );
65
78
  } catch (err) {
66
- toast.error(apiErrorMessage(err, "Couldn't change this language's status. Try again."));
79
+ toast.error(
80
+ apiErrorMessage(
81
+ err,
82
+ "Couldn't change this language's status. Try again.",
83
+ ),
84
+ );
67
85
  } finally {
68
86
  setStatusPending(false);
69
87
  }
@@ -73,9 +91,16 @@ export function LanguageDetailPage() {
73
91
 
74
92
  return (
75
93
  <div className="flex flex-col gap-4">
76
- <Breadcrumb items={[{ title: "Languages", href: "/languages" }, { title: language?.name ?? "Details", href: `/languages/${id}` }]} />
94
+ <Breadcrumb
95
+ items={[
96
+ { title: "Languages", href: "/languages" },
97
+ { title: language?.name ?? "Details", href: `/languages/${id}` },
98
+ ]}
99
+ />
77
100
 
78
- {loading && !language && <p className="text-sm text-muted-foreground">Loading…</p>}
101
+ {loading && !language && (
102
+ <p className="text-sm text-muted-foreground">Loading…</p>
103
+ )}
79
104
 
80
105
  {language && (
81
106
  <>
@@ -84,16 +109,28 @@ export function LanguageDetailPage() {
84
109
  <div>
85
110
  <CardTitle>{language.name}</CardTitle>
86
111
  <CardDescription>
87
- Created: {format(new Date(language.createdAt), "dd MMM yyyy")} · Updated: {format(new Date(language.updatedAt), "dd MMM yyyy")}
112
+ Created: {format(new Date(language.createdAt), "dd MMM yyyy")}{" "}
113
+ · Updated:{" "}
114
+ {format(new Date(language.updatedAt), "dd MMM yyyy")}
88
115
  </CardDescription>
89
116
  </div>
90
117
  <div className="flex items-center gap-1.5">
91
- {language.isDefault && <Badge variant="secondary">Default</Badge>}
92
- <Badge variant={language.isActive ? "success" : "destructive"}>{language.isActive ? "Active" : "Inactive"}</Badge>
118
+ {language.isDefault && (
119
+ <Badge variant="secondary">Default</Badge>
120
+ )}
121
+ <Badge variant={language.isActive ? "success" : "destructive"}>
122
+ {language.isActive ? "Active" : "Inactive"}
123
+ </Badge>
93
124
  <Link
94
125
  to={`/languages/${id}/edit`}
95
- className={cn(buttonVariants({ variant: "outline", size: "sm" }))}
96
- title={canManage ? undefined : `You need the "${PERMISSIONS.languagesManage}" permission to do this.`}
126
+ className={cn(
127
+ buttonVariants({ variant: "outline", size: "sm" }),
128
+ )}
129
+ title={
130
+ canManage
131
+ ? undefined
132
+ : `You need the "${PERMISSIONS.languagesManage}" permission to do this.`
133
+ }
97
134
  aria-disabled={!canManage}
98
135
  onClick={(e) => !canManage && e.preventDefault()}
99
136
  >
@@ -107,7 +144,12 @@ export function LanguageDetailPage() {
107
144
  {field("Name", language.name)}
108
145
  {field("Native name", language.nativeName)}
109
146
  {field("Code", language.code)}
110
- {field("Direction", language.direction === "rtl" ? "Right to left (RTL)" : "Left to right (LTR)")}
147
+ {field(
148
+ "Direction",
149
+ language.direction === "rtl"
150
+ ? "Right to left (RTL)"
151
+ : "Left to right (LTR)",
152
+ )}
111
153
  {field("Default language", language.isDefault ? "Yes" : "No")}
112
154
  </div>
113
155
  </CardContent>
@@ -121,7 +163,11 @@ export function LanguageDetailPage() {
121
163
  <Button
122
164
  variant={language.isActive ? "destructive" : "outline"}
123
165
  disabled={!canStatus || statusPending}
124
- title={canStatus ? undefined : `You need the "${PERMISSIONS.languagesStatus}" permission to do this.`}
166
+ title={
167
+ canStatus
168
+ ? undefined
169
+ : `You need the "${PERMISSIONS.languagesStatus}" permission to do this.`
170
+ }
125
171
  onClick={() => void toggleActive()}
126
172
  >
127
173
  {language.isActive ? "Deactivate" : "Activate"}
@@ -130,7 +176,11 @@ export function LanguageDetailPage() {
130
176
  <Button
131
177
  variant="destructive"
132
178
  disabled={!canManage}
133
- title={canManage ? undefined : `You need the "${PERMISSIONS.languagesManage}" permission to do this.`}
179
+ title={
180
+ canManage
181
+ ? undefined
182
+ : `You need the "${PERMISSIONS.languagesManage}" permission to do this.`
183
+ }
134
184
  onClick={() => setConfirmingDelete(true)}
135
185
  >
136
186
  Delete language
@@ -173,7 +223,9 @@ function DeleteLanguageDialog({
173
223
  toast.success("Language deleted.");
174
224
  onDeleted();
175
225
  } catch (err) {
176
- toast.error(apiErrorMessage(err, "Couldn't delete this language. Try again."));
226
+ toast.error(
227
+ apiErrorMessage(err, "Couldn't delete this language. Try again."),
228
+ );
177
229
  } finally {
178
230
  setSubmitting(false);
179
231
  }
@@ -188,7 +240,11 @@ function DeleteLanguageDialog({
188
240
  >
189
241
  <div className="flex flex-col gap-4">
190
242
  <div className="flex gap-2">
191
- <Button variant="destructive" disabled={submitting} onClick={() => void handleDelete()}>
243
+ <Button
244
+ variant="destructive"
245
+ disabled={submitting}
246
+ onClick={() => void handleDelete()}
247
+ >
192
248
  {submitting ? "Deleting…" : "Delete language"}
193
249
  </Button>
194
250
  <Button variant="outline" onClick={onClose}>
@@ -1,4 +1,4 @@
1
- import { useCallback, useEffect, useState } from "react";
1
+ import { startTransition, useCallback, useEffect, useState } from "react";
2
2
  import { useParams } from "react-router-dom";
3
3
  import type { PermissionSummary } from "@simple-auth-kit/auth-client";
4
4
  import { AuthApiError } from "@simple-auth-kit/auth-client";
@@ -10,7 +10,13 @@ import { DefinePermissionDialog } from "@/pages/PermissionsPage";
10
10
  import { Breadcrumb } from "@/components/breadcrumb";
11
11
  import { Badge } from "@/components/ui/badge";
12
12
  import { Button } from "@/components/ui/button";
13
- import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
13
+ import {
14
+ Card,
15
+ CardContent,
16
+ CardDescription,
17
+ CardHeader,
18
+ CardTitle,
19
+ } from "@/components/ui/card";
14
20
 
15
21
  function field(label: string, value: string | number | null) {
16
22
  return (
@@ -38,25 +44,42 @@ export function PermissionDetailPage() {
38
44
  const permissions = await authClient.listPermissions();
39
45
  setPermission(permissions.find((p) => p.id === id) ?? null);
40
46
  } catch (err) {
41
- toast.error(err instanceof AuthApiError ? err.message : "Couldn't load this permission.");
47
+ toast.error(
48
+ err instanceof AuthApiError
49
+ ? err.message
50
+ : "Couldn't load this permission.",
51
+ );
42
52
  } finally {
43
53
  setLoading(false);
44
54
  }
45
55
  }, [id]);
46
56
 
47
57
  useEffect(() => {
48
- void load();
58
+ startTransition(() => void load());
49
59
  }, [load]);
50
60
 
51
61
  async function toggleActive() {
52
62
  if (!permission) return;
53
63
  setStatusPending(true);
54
64
  try {
55
- await authClient.definePermission({ slug: permission.slug, isActive: !permission.isActive });
56
- setPermission((prev) => (prev ? { ...prev, isActive: !prev.isActive } : prev));
57
- toast.success(permission.isActive ? "Permission deactivated." : "Permission activated.");
65
+ await authClient.definePermission({
66
+ slug: permission.slug,
67
+ isActive: !permission.isActive,
68
+ });
69
+ setPermission((prev) =>
70
+ prev ? { ...prev, isActive: !prev.isActive } : prev,
71
+ );
72
+ toast.success(
73
+ permission.isActive
74
+ ? "Permission deactivated."
75
+ : "Permission activated.",
76
+ );
58
77
  } catch (err) {
59
- toast.error(err instanceof AuthApiError ? err.message : "Couldn't change this permission's status. Try again.");
78
+ toast.error(
79
+ err instanceof AuthApiError
80
+ ? err.message
81
+ : "Couldn't change this permission's status. Try again.",
82
+ );
60
83
  } finally {
61
84
  setStatusPending(false);
62
85
  }
@@ -66,9 +89,19 @@ export function PermissionDetailPage() {
66
89
 
67
90
  return (
68
91
  <div className="flex flex-col gap-4">
69
- <Breadcrumb items={[{ title: "Permissions", href: "/permissions" }, { title: permission?.displayName ?? "Details", href: `/permissions/${id}` }]} />
92
+ <Breadcrumb
93
+ items={[
94
+ { title: "Permissions", href: "/permissions" },
95
+ {
96
+ title: permission?.displayName ?? "Details",
97
+ href: `/permissions/${id}`,
98
+ },
99
+ ]}
100
+ />
70
101
 
71
- {loading && !permission && <p className="text-sm text-muted-foreground">Loading…</p>}
102
+ {loading && !permission && (
103
+ <p className="text-sm text-muted-foreground">Loading…</p>
104
+ )}
72
105
 
73
106
  {permission && (
74
107
  <>
@@ -81,12 +114,20 @@ export function PermissionDetailPage() {
81
114
  </CardDescription>
82
115
  </div>
83
116
  <div className="flex items-center gap-1.5">
84
- <Badge variant={permission.isActive ? "success" : "destructive"}>{permission.isActive ? "Active" : "Inactive"}</Badge>
117
+ <Badge
118
+ variant={permission.isActive ? "success" : "destructive"}
119
+ >
120
+ {permission.isActive ? "Active" : "Inactive"}
121
+ </Badge>
85
122
  <Button
86
123
  variant="outline"
87
124
  size="sm"
88
125
  disabled={!canDefine}
89
- title={canDefine ? undefined : `You need the "${PERMISSIONS.permissionsDefine}" permission to do this.`}
126
+ title={
127
+ canDefine
128
+ ? undefined
129
+ : `You need the "${PERMISSIONS.permissionsDefine}" permission to do this.`
130
+ }
90
131
  onClick={() => setEditing(true)}
91
132
  >
92
133
  <PencilIcon />
@@ -112,7 +153,11 @@ export function PermissionDetailPage() {
112
153
  <Button
113
154
  variant={permission.isActive ? "destructive" : "outline"}
114
155
  disabled={!canDefine || statusPending}
115
- title={canDefine ? undefined : `You need the "${PERMISSIONS.permissionsDefine}" permission to do this.`}
156
+ title={
157
+ canDefine
158
+ ? undefined
159
+ : `You need the "${PERMISSIONS.permissionsDefine}" permission to do this.`
160
+ }
116
161
  onClick={() => void toggleActive()}
117
162
  >
118
163
  {permission.isActive ? "Deactivate" : "Activate"}