includio-cms 0.1.0 → 0.1.2

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 (59) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/ROADMAP.md +17 -15
  3. package/dist/admin/auth-client.d.ts +1165 -5
  4. package/dist/admin/auth-client.js +4 -1
  5. package/dist/admin/client/account/sessions-section.svelte +1 -21
  6. package/dist/admin/client/index.d.ts +1 -0
  7. package/dist/admin/client/index.js +1 -0
  8. package/dist/admin/client/users/accept-invite-page.svelte +118 -0
  9. package/dist/admin/client/users/accept-invite-page.svelte.d.ts +4 -0
  10. package/dist/admin/client/users/create-user-dialog.svelte +157 -0
  11. package/dist/admin/client/users/create-user-dialog.svelte.d.ts +8 -0
  12. package/dist/admin/client/users/delete-user-dialog.svelte +53 -0
  13. package/dist/admin/client/users/delete-user-dialog.svelte.d.ts +10 -0
  14. package/dist/admin/client/users/edit-user-dialog.svelte +127 -0
  15. package/dist/admin/client/users/edit-user-dialog.svelte.d.ts +16 -0
  16. package/dist/admin/client/users/invite-user-dialog.svelte +107 -0
  17. package/dist/admin/client/users/invite-user-dialog.svelte.d.ts +8 -0
  18. package/dist/admin/client/users/lang.d.ts +57 -0
  19. package/dist/admin/client/users/lang.js +114 -0
  20. package/dist/admin/client/users/pending-invitations.svelte +145 -0
  21. package/dist/admin/client/users/pending-invitations.svelte.d.ts +6 -0
  22. package/dist/admin/client/users/user-sessions-sheet.svelte +141 -0
  23. package/dist/admin/client/users/user-sessions-sheet.svelte.d.ts +8 -0
  24. package/dist/admin/client/users/users-page.svelte +262 -0
  25. package/dist/admin/client/users/users-page.svelte.d.ts +6 -0
  26. package/dist/admin/components/fields/array-field.svelte +68 -22
  27. package/dist/admin/components/fields/field-renderer.svelte +25 -2
  28. package/dist/admin/components/fields/number-field.svelte +1 -1
  29. package/dist/admin/components/fields/text-field-wrapper.svelte +56 -1
  30. package/dist/admin/components/fields/text-field.svelte +2 -2
  31. package/dist/admin/components/layout/lang.d.ts +1 -0
  32. package/dist/admin/components/layout/lang.js +4 -2
  33. package/dist/admin/components/layout/nav-main.svelte +15 -1
  34. package/dist/admin/remote/invite.d.ts +44 -0
  35. package/dist/admin/remote/invite.js +44 -0
  36. package/dist/admin/remote/middleware/auth.d.ts +5 -0
  37. package/dist/admin/remote/middleware/auth.js +7 -0
  38. package/dist/admin/utils/parseUserAgent.d.ts +5 -0
  39. package/dist/admin/utils/parseUserAgent.js +26 -0
  40. package/dist/components/ui/input-group/input-group-input.svelte.d.ts +1 -1
  41. package/dist/components/ui/sidebar/sidebar-input.svelte.d.ts +1 -1
  42. package/dist/core/cms.d.ts +1 -1
  43. package/dist/core/cms.js +1 -1
  44. package/dist/core/fields/fieldSchemaToTs.js +18 -4
  45. package/dist/core/server/forms/submissions/operations/create.js +1 -1
  46. package/dist/email-nodemailer/index.d.ts +1 -0
  47. package/dist/server/auth.d.ts +8 -8
  48. package/dist/server/db/schema/auth-schema.d.ts +143 -0
  49. package/dist/server/db/schema/auth-schema.js +12 -0
  50. package/dist/sveltekit/server/handle.js +13 -0
  51. package/dist/types/cms.d.ts +2 -2
  52. package/dist/types/roles.d.ts +1 -0
  53. package/dist/types/roles.js +1 -0
  54. package/dist/updates/0.1.1/index.d.ts +2 -0
  55. package/dist/updates/0.1.1/index.js +17 -0
  56. package/dist/updates/0.1.2/index.d.ts +2 -0
  57. package/dist/updates/0.1.2/index.js +36 -0
  58. package/dist/updates/index.js +3 -1
  59. package/package.json +2 -2
@@ -1,2 +1,5 @@
1
1
  import { createAuthClient } from 'better-auth/svelte';
2
- export const authClient = createAuthClient();
2
+ import { adminClient } from 'better-auth/client/plugins';
3
+ export const authClient = createAuthClient({
4
+ plugins: [adminClient()]
5
+ });
@@ -10,6 +10,7 @@
10
10
  import DeviceMobile from '@tabler/icons-svelte/icons/device-mobile';
11
11
  import Loader2 from '@tabler/icons-svelte/icons/loader-2';
12
12
  import Button from '../../../components/ui/button/button.svelte';
13
+ import { parseUserAgent } from '../../utils/parseUserAgent.js';
13
14
 
14
15
  const interfaceLanguage = useInterfaceLanguage();
15
16
 
@@ -65,27 +66,6 @@
65
66
  }
66
67
  }
67
68
 
68
- function parseUserAgent(ua: string | null | undefined): { browser: string; os: string; isMobile: boolean } {
69
- if (!ua) return { browser: 'Unknown', os: 'Unknown', isMobile: false };
70
-
71
- const isMobile = /mobile|android|iphone|ipad/i.test(ua);
72
-
73
- let browser = 'Unknown';
74
- if (ua.includes('Firefox')) browser = 'Firefox';
75
- else if (ua.includes('Edg')) browser = 'Edge';
76
- else if (ua.includes('Chrome')) browser = 'Chrome';
77
- else if (ua.includes('Safari')) browser = 'Safari';
78
-
79
- let os = 'Unknown';
80
- if (ua.includes('Windows')) os = 'Windows';
81
- else if (ua.includes('Mac')) os = 'macOS';
82
- else if (ua.includes('Linux')) os = 'Linux';
83
- else if (ua.includes('Android')) os = 'Android';
84
- else if (ua.includes('iPhone') || ua.includes('iPad')) os = 'iOS';
85
-
86
- return { browser, os, isMobile };
87
- }
88
-
89
69
  function formatDate(date: Date | string): string {
90
70
  return new Date(date).toLocaleString(toLocaleCode(interfaceLanguage.current), {
91
71
  year: 'numeric',
@@ -7,3 +7,4 @@ export { default as LoginPage } from './login/login-page.svelte';
7
7
  export { default as MediaPage } from './media/media-page.svelte';
8
8
  export { default as FormPage } from './form/form-page.svelte';
9
9
  export { default as FormSubmissionPage } from './form/form-submission/form-submission-page.svelte';
10
+ export { default as UsersPage } from './users/users-page.svelte';
@@ -7,3 +7,4 @@ export { default as LoginPage } from './login/login-page.svelte';
7
7
  export { default as MediaPage } from './media/media-page.svelte';
8
8
  export { default as FormPage } from './form/form-page.svelte';
9
9
  export { default as FormSubmissionPage } from './form/form-submission/form-submission-page.svelte';
10
+ export { default as UsersPage } from './users/users-page.svelte';
@@ -0,0 +1,118 @@
1
+ <script lang="ts">
2
+ import '../../styles/admin.css';
3
+ import { Input } from '../../../components/ui/input/index.js';
4
+ import Label from '../../../components/ui/label/label.svelte';
5
+ import Button from '../../../components/ui/button/button.svelte';
6
+ import Loader2 from '@tabler/icons-svelte/icons/loader-2';
7
+ import { page } from '$app/state';
8
+ import { goto } from '$app/navigation';
9
+ import { usersLang } from './lang.js';
10
+ import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
11
+
12
+ const interfaceLanguage = useInterfaceLanguage();
13
+ const lang = $derived(usersLang[interfaceLanguage.current]);
14
+
15
+ const token = $derived(page.url.searchParams.get('token'));
16
+
17
+ let name = $state('');
18
+ let password = $state('');
19
+ let confirmPassword = $state('');
20
+ let loading = $state(false);
21
+ let error = $state('');
22
+ let success = $state(false);
23
+
24
+ async function handleSubmit(e: Event) {
25
+ e.preventDefault();
26
+ if (password !== confirmPassword) {
27
+ error = lang.invite.confirmPassword + ' — mismatch';
28
+ return;
29
+ }
30
+ if (password.length < 6) {
31
+ error = 'Password must be at least 6 characters';
32
+ return;
33
+ }
34
+
35
+ loading = true;
36
+ error = '';
37
+
38
+ try {
39
+ const res = await fetch('/admin/api/accept-invite', {
40
+ method: 'POST',
41
+ headers: { 'Content-Type': 'application/json' },
42
+ body: JSON.stringify({ token, name, password })
43
+ });
44
+
45
+ const data = await res.json();
46
+
47
+ if (!res.ok) {
48
+ error = data.error || 'Error';
49
+ loading = false;
50
+ return;
51
+ }
52
+
53
+ success = true;
54
+ setTimeout(() => goto('/admin/login'), 2000);
55
+ } catch {
56
+ error = 'Network error';
57
+ loading = false;
58
+ }
59
+ }
60
+ </script>
61
+
62
+ <div
63
+ class="relative flex min-h-svh flex-col items-center justify-center overflow-hidden p-6 md:p-10"
64
+ style="background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 50%, #cbd5e1 100%);"
65
+ >
66
+ <div
67
+ class="pointer-events-none absolute inset-0 hidden dark:block"
68
+ style="background: linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #2D4A77 100%);"
69
+ ></div>
70
+
71
+ <div class="relative z-10 w-full max-w-sm">
72
+ <div
73
+ class="overflow-hidden rounded-3xl border border-slate-200/50 bg-white/80 p-8 shadow-2xl backdrop-blur-xl dark:border-white/10 dark:bg-slate-900/60"
74
+ >
75
+ <div class="mb-6 text-center">
76
+ <div class="mb-2 text-3xl font-bold" style="background: linear-gradient(135deg, #2D4A77, #4975AE); -webkit-background-clip: text; -webkit-text-fill-color: transparent;">
77
+ includio
78
+ </div>
79
+ <h1 class="text-lg font-semibold">{lang.invite.acceptTitle}</h1>
80
+ <p class="text-muted-foreground mt-1 text-sm">{lang.invite.acceptDescription}</p>
81
+ </div>
82
+
83
+ {#if success}
84
+ <div class="rounded-lg bg-green-50 p-4 text-center text-sm text-green-700 dark:bg-green-900/20 dark:text-green-400">
85
+ {lang.invite.accepted}
86
+ </div>
87
+ {:else if !token}
88
+ <div class="rounded-lg bg-red-50 p-4 text-center text-sm text-red-700 dark:bg-red-900/20 dark:text-red-400">
89
+ {lang.invite.invalidToken}
90
+ </div>
91
+ {:else}
92
+ <form onsubmit={handleSubmit} class="space-y-4">
93
+ <div class="space-y-2">
94
+ <Label for="invite-name">{lang.name}</Label>
95
+ <Input id="invite-name" bind:value={name} required class="rounded-xl" />
96
+ </div>
97
+ <div class="space-y-2">
98
+ <Label for="invite-password">{lang.password}</Label>
99
+ <Input id="invite-password" type="password" bind:value={password} required minlength={6} class="rounded-xl" />
100
+ </div>
101
+ <div class="space-y-2">
102
+ <Label for="invite-confirm">{lang.invite.confirmPassword}</Label>
103
+ <Input id="invite-confirm" type="password" bind:value={confirmPassword} required minlength={6} class="rounded-xl" />
104
+ </div>
105
+ {#if error}
106
+ <p class="text-destructive text-sm">{error}</p>
107
+ {/if}
108
+ <Button type="submit" class="w-full rounded-xl" disabled={loading}>
109
+ {#if loading}
110
+ <Loader2 class="mr-2 size-4 animate-spin" />
111
+ {/if}
112
+ {lang.invite.acceptInvite}
113
+ </Button>
114
+ </form>
115
+ {/if}
116
+ </div>
117
+ </div>
118
+ </div>
@@ -0,0 +1,4 @@
1
+ import '../../styles/admin.css';
2
+ declare const AcceptInvitePage: import("svelte").Component<Record<string, never>, {}, "">;
3
+ type AcceptInvitePage = ReturnType<typeof AcceptInvitePage>;
4
+ export default AcceptInvitePage;
@@ -0,0 +1,157 @@
1
+ <script lang="ts">
2
+ import * as Dialog from '../../../components/ui/dialog/index.js';
3
+ import * as Select from '../../../components/ui/select/index.js';
4
+ import { Input } from '../../../components/ui/input/index.js';
5
+ import Label from '../../../components/ui/label/label.svelte';
6
+ import Button from '../../../components/ui/button/button.svelte';
7
+ import EyeIcon from '@tabler/icons-svelte/icons/eye';
8
+ import EyeOffIcon from '@tabler/icons-svelte/icons/eye-off';
9
+ import DiceIcon from '@tabler/icons-svelte/icons/dice-3';
10
+ import { authClient } from '../../auth-client.js';
11
+ import { toast } from 'svelte-sonner';
12
+ import { usersLang } from './lang.js';
13
+ import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
14
+ import type { UserRole } from '../../../types/roles.js';
15
+
16
+ type Props = {
17
+ open: boolean;
18
+ onOpenChange: (open: boolean) => void;
19
+ onCreated: () => void;
20
+ };
21
+
22
+ let { open = $bindable(), onOpenChange, onCreated }: Props = $props();
23
+
24
+ const interfaceLanguage = useInterfaceLanguage();
25
+ const lang = $derived(usersLang[interfaceLanguage.current]);
26
+
27
+ let name = $state('');
28
+ let email = $state('');
29
+ let password = $state('');
30
+ let role = $state<UserRole>('user');
31
+ let loading = $state(false);
32
+ let error = $state('');
33
+ let showPassword = $state(false);
34
+
35
+ function generatePassword(length = 20): string {
36
+ const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%&*';
37
+ const array = new Uint8Array(length);
38
+ crypto.getRandomValues(array);
39
+ return Array.from(array, (b) => chars[b % chars.length]).join('');
40
+ }
41
+
42
+ function reset() {
43
+ name = '';
44
+ email = '';
45
+ password = '';
46
+ role = 'user';
47
+ error = '';
48
+ showPassword = false;
49
+ }
50
+
51
+ async function handleSubmit(e: Event) {
52
+ e.preventDefault();
53
+ loading = true;
54
+ error = '';
55
+
56
+ const { error: apiError } = await authClient.admin.createUser({
57
+ email,
58
+ password,
59
+ name,
60
+ role
61
+ });
62
+
63
+ if (apiError) {
64
+ error = apiError.message || 'Error';
65
+ loading = false;
66
+ return;
67
+ }
68
+
69
+ toast.success(lang.userCreated);
70
+ loading = false;
71
+ reset();
72
+ onOpenChange(false);
73
+ onCreated();
74
+ }
75
+ </script>
76
+
77
+ <Dialog.Root
78
+ {open}
79
+ onOpenChange={(v) => {
80
+ if (!v) reset();
81
+ onOpenChange(v);
82
+ }}
83
+ >
84
+ <Dialog.Content>
85
+ <Dialog.Header>
86
+ <Dialog.Title>{lang.createUser}</Dialog.Title>
87
+ </Dialog.Header>
88
+ <form onsubmit={handleSubmit} class="space-y-4">
89
+ <div class="space-y-2">
90
+ <Label for="create-name">{lang.name}</Label>
91
+ <Input id="create-name" bind:value={name} required />
92
+ </div>
93
+ <div class="space-y-2">
94
+ <Label for="create-email">{lang.email}</Label>
95
+ <Input id="create-email" type="email" bind:value={email} required />
96
+ </div>
97
+ <div class="space-y-2">
98
+ <Label for="create-password">{lang.password}</Label>
99
+ <div class="flex gap-1">
100
+ <div class="relative flex-1">
101
+ <Input
102
+ id="create-password"
103
+ type={showPassword ? 'text' : 'password'}
104
+ bind:value={password}
105
+ required
106
+ minlength={6}
107
+ class="pr-9"
108
+ />
109
+ <button
110
+ type="button"
111
+ class="text-muted-foreground hover:text-foreground absolute right-2 top-1/2 -translate-y-1/2"
112
+ onclick={() => (showPassword = !showPassword)}
113
+ >
114
+ {#if showPassword}
115
+ <EyeOffIcon class="size-4" />
116
+ {:else}
117
+ <EyeIcon class="size-4" />
118
+ {/if}
119
+ </button>
120
+ </div>
121
+ <Button
122
+ type="button"
123
+ variant="outline"
124
+ size="icon"
125
+ class="shrink-0"
126
+ onclick={() => {
127
+ password = generatePassword();
128
+ showPassword = true;
129
+ }}
130
+ title={lang.generatePassword}
131
+ >
132
+ <DiceIcon class="size-4" />
133
+ </Button>
134
+ </div>
135
+ </div>
136
+ <div class="space-y-2">
137
+ <Label>{lang.role}</Label>
138
+ <Select.Root type="single" value={role} onValueChange={(v) => v && (role = v as UserRole)}>
139
+ <Select.Trigger class="w-full">
140
+ {role === 'admin' ? lang.roleAdmin : lang.roleUser}
141
+ </Select.Trigger>
142
+ <Select.Content>
143
+ <Select.Item value="user">{lang.roleUser}</Select.Item>
144
+ <Select.Item value="admin">{lang.roleAdmin}</Select.Item>
145
+ </Select.Content>
146
+ </Select.Root>
147
+ </div>
148
+ {#if error}
149
+ <p class="text-destructive text-sm">{error}</p>
150
+ {/if}
151
+ <Dialog.Footer>
152
+ <Button type="button" variant="outline" onclick={() => onOpenChange(false)}>{lang.cancel}</Button>
153
+ <Button type="submit" disabled={loading}>{lang.createUser}</Button>
154
+ </Dialog.Footer>
155
+ </form>
156
+ </Dialog.Content>
157
+ </Dialog.Root>
@@ -0,0 +1,8 @@
1
+ type Props = {
2
+ open: boolean;
3
+ onOpenChange: (open: boolean) => void;
4
+ onCreated: () => void;
5
+ };
6
+ declare const CreateUserDialog: import("svelte").Component<Props, {}, "open">;
7
+ type CreateUserDialog = ReturnType<typeof CreateUserDialog>;
8
+ export default CreateUserDialog;
@@ -0,0 +1,53 @@
1
+ <script lang="ts">
2
+ import * as AlertDialog from '../../../components/ui/alert-dialog/index.js';
3
+ import { authClient } from '../../auth-client.js';
4
+ import { toast } from 'svelte-sonner';
5
+ import { usersLang } from './lang.js';
6
+ import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
7
+
8
+ type Props = {
9
+ open: boolean;
10
+ onOpenChange: (open: boolean) => void;
11
+ onDeleted: () => void;
12
+ userId: string | null;
13
+ currentUserId: string;
14
+ };
15
+
16
+ let { open = $bindable(), onOpenChange, onDeleted, userId, currentUserId }: Props = $props();
17
+
18
+ const interfaceLanguage = useInterfaceLanguage();
19
+ const lang = $derived(usersLang[interfaceLanguage.current]);
20
+
21
+ async function handleDelete() {
22
+ if (!userId) return;
23
+
24
+ if (userId === currentUserId) {
25
+ toast.error(lang.cannotDeleteSelf);
26
+ onOpenChange(false);
27
+ return;
28
+ }
29
+
30
+ const { error } = await authClient.admin.removeUser({ userId });
31
+
32
+ if (error) {
33
+ toast.error(error.message || 'Error');
34
+ onOpenChange(false);
35
+ return;
36
+ }
37
+
38
+ toast.success(lang.userDeleted);
39
+ onOpenChange(false);
40
+ onDeleted();
41
+ }
42
+ </script>
43
+
44
+ <AlertDialog.Root {open} onOpenChange={(v) => onOpenChange(v)}>
45
+ <AlertDialog.Content>
46
+ <AlertDialog.Title>{lang.deleteConfirmTitle}</AlertDialog.Title>
47
+ <AlertDialog.Description>{lang.deleteConfirmDescription}</AlertDialog.Description>
48
+ <AlertDialog.Footer>
49
+ <AlertDialog.Cancel>{lang.cancel}</AlertDialog.Cancel>
50
+ <AlertDialog.Action onclick={handleDelete}>{lang.deleteUser}</AlertDialog.Action>
51
+ </AlertDialog.Footer>
52
+ </AlertDialog.Content>
53
+ </AlertDialog.Root>
@@ -0,0 +1,10 @@
1
+ type Props = {
2
+ open: boolean;
3
+ onOpenChange: (open: boolean) => void;
4
+ onDeleted: () => void;
5
+ userId: string | null;
6
+ currentUserId: string;
7
+ };
8
+ declare const DeleteUserDialog: import("svelte").Component<Props, {}, "open">;
9
+ type DeleteUserDialog = ReturnType<typeof DeleteUserDialog>;
10
+ export default DeleteUserDialog;
@@ -0,0 +1,127 @@
1
+ <script lang="ts">
2
+ import * as Dialog from '../../../components/ui/dialog/index.js';
3
+ import * as Select from '../../../components/ui/select/index.js';
4
+ import { Input } from '../../../components/ui/input/index.js';
5
+ import Label from '../../../components/ui/label/label.svelte';
6
+ import Button from '../../../components/ui/button/button.svelte';
7
+ import { authClient } from '../../auth-client.js';
8
+ import { toast } from 'svelte-sonner';
9
+ import { usersLang } from './lang.js';
10
+ import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
11
+ import type { UserRole } from '../../../types/roles.js';
12
+
13
+ type UserData = {
14
+ id: string;
15
+ name: string;
16
+ email: string;
17
+ role: string;
18
+ };
19
+
20
+ type Props = {
21
+ open: boolean;
22
+ onOpenChange: (open: boolean) => void;
23
+ onUpdated: () => void;
24
+ user: UserData | null;
25
+ currentUserId: string;
26
+ };
27
+
28
+ let { open = $bindable(), onOpenChange, onUpdated, user, currentUserId }: Props = $props();
29
+
30
+ const interfaceLanguage = useInterfaceLanguage();
31
+ const lang = $derived(usersLang[interfaceLanguage.current]);
32
+
33
+ let name = $state('');
34
+ let email = $state('');
35
+ let role = $state<UserRole>('user');
36
+ let loading = $state(false);
37
+ let error = $state('');
38
+
39
+ const isSelf = $derived(user?.id === currentUserId);
40
+
41
+ $effect(() => {
42
+ if (user) {
43
+ name = user.name;
44
+ email = user.email;
45
+ role = (user.role as UserRole) || 'user';
46
+ }
47
+ });
48
+
49
+ async function handleSubmit(e: Event) {
50
+ e.preventDefault();
51
+ if (!user) return;
52
+ loading = true;
53
+ error = '';
54
+
55
+ // Update name/email
56
+ const { error: updateError } = await authClient.admin.updateUser({
57
+ userId: user.id,
58
+ data: { name, email }
59
+ });
60
+
61
+ if (updateError) {
62
+ error = updateError.message || 'Error';
63
+ loading = false;
64
+ return;
65
+ }
66
+
67
+ // Update role if changed and not self
68
+ if (role !== user.role && !isSelf) {
69
+ const { error: roleError } = await authClient.admin.setRole({
70
+ userId: user.id,
71
+ role
72
+ });
73
+
74
+ if (roleError) {
75
+ error = roleError.message || 'Error';
76
+ loading = false;
77
+ return;
78
+ }
79
+ }
80
+
81
+ toast.success(lang.userUpdated);
82
+ loading = false;
83
+ onOpenChange(false);
84
+ onUpdated();
85
+ }
86
+ </script>
87
+
88
+ <Dialog.Root {open} onOpenChange={(v) => onOpenChange(v)}>
89
+ <Dialog.Content>
90
+ <Dialog.Header>
91
+ <Dialog.Title>{lang.editUser}</Dialog.Title>
92
+ </Dialog.Header>
93
+ <form onsubmit={handleSubmit} class="space-y-4">
94
+ <div class="space-y-2">
95
+ <Label for="edit-name">{lang.name}</Label>
96
+ <Input id="edit-name" bind:value={name} required />
97
+ </div>
98
+ <div class="space-y-2">
99
+ <Label for="edit-email">{lang.email}</Label>
100
+ <Input id="edit-email" type="email" bind:value={email} required />
101
+ </div>
102
+ <div class="space-y-2">
103
+ <Label>{lang.role}</Label>
104
+ {#if isSelf}
105
+ <p class="text-muted-foreground text-sm">{lang.cannotChangeSelfRole}</p>
106
+ {:else}
107
+ <Select.Root type="single" value={role} onValueChange={(v) => v && (role = v as UserRole)}>
108
+ <Select.Trigger class="w-full">
109
+ {role === 'admin' ? lang.roleAdmin : lang.roleUser}
110
+ </Select.Trigger>
111
+ <Select.Content>
112
+ <Select.Item value="user">{lang.roleUser}</Select.Item>
113
+ <Select.Item value="admin">{lang.roleAdmin}</Select.Item>
114
+ </Select.Content>
115
+ </Select.Root>
116
+ {/if}
117
+ </div>
118
+ {#if error}
119
+ <p class="text-destructive text-sm">{error}</p>
120
+ {/if}
121
+ <Dialog.Footer>
122
+ <Button type="button" variant="outline" onclick={() => onOpenChange(false)}>{lang.cancel}</Button>
123
+ <Button type="submit" disabled={loading}>{lang.save}</Button>
124
+ </Dialog.Footer>
125
+ </form>
126
+ </Dialog.Content>
127
+ </Dialog.Root>
@@ -0,0 +1,16 @@
1
+ type UserData = {
2
+ id: string;
3
+ name: string;
4
+ email: string;
5
+ role: string;
6
+ };
7
+ type Props = {
8
+ open: boolean;
9
+ onOpenChange: (open: boolean) => void;
10
+ onUpdated: () => void;
11
+ user: UserData | null;
12
+ currentUserId: string;
13
+ };
14
+ declare const EditUserDialog: import("svelte").Component<Props, {}, "open">;
15
+ type EditUserDialog = ReturnType<typeof EditUserDialog>;
16
+ export default EditUserDialog;
@@ -0,0 +1,107 @@
1
+ <script lang="ts">
2
+ import * as Dialog from '../../../components/ui/dialog/index.js';
3
+ import * as Select from '../../../components/ui/select/index.js';
4
+ import { Input } from '../../../components/ui/input/index.js';
5
+ import Label from '../../../components/ui/label/label.svelte';
6
+ import Button from '../../../components/ui/button/button.svelte';
7
+ import { toast } from 'svelte-sonner';
8
+ import { usersLang } from './lang.js';
9
+ import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
10
+ import type { UserRole } from '../../../types/roles.js';
11
+
12
+ type Props = {
13
+ open: boolean;
14
+ onOpenChange: (open: boolean) => void;
15
+ onInvited: () => void;
16
+ };
17
+
18
+ let { open = $bindable(), onOpenChange, onInvited }: Props = $props();
19
+
20
+ const interfaceLanguage = useInterfaceLanguage();
21
+ const lang = $derived(usersLang[interfaceLanguage.current]);
22
+
23
+ let email = $state('');
24
+ let role = $state<UserRole>('user');
25
+ let loading = $state(false);
26
+ let error = $state('');
27
+
28
+ function reset() {
29
+ email = '';
30
+ role = 'user';
31
+ error = '';
32
+ }
33
+
34
+ async function handleSubmit(e: Event) {
35
+ e.preventDefault();
36
+ loading = true;
37
+ error = '';
38
+
39
+ try {
40
+ const res = await fetch('/admin/api/invite', {
41
+ method: 'POST',
42
+ headers: { 'Content-Type': 'application/json' },
43
+ body: JSON.stringify({ email, role })
44
+ });
45
+
46
+ const data = await res.json();
47
+
48
+ if (!res.ok) {
49
+ if (res.status === 409) {
50
+ error = lang.invite.emailAlreadyRegistered;
51
+ } else {
52
+ error = data.error || 'Error';
53
+ }
54
+ loading = false;
55
+ return;
56
+ }
57
+
58
+ toast.success(lang.invite.inviteSent);
59
+ loading = false;
60
+ reset();
61
+ onOpenChange(false);
62
+ onInvited();
63
+ } catch {
64
+ error = 'Network error';
65
+ loading = false;
66
+ }
67
+ }
68
+ </script>
69
+
70
+ <Dialog.Root
71
+ {open}
72
+ onOpenChange={(v) => {
73
+ if (!v) reset();
74
+ onOpenChange(v);
75
+ }}
76
+ >
77
+ <Dialog.Content>
78
+ <Dialog.Header>
79
+ <Dialog.Title>{lang.invite.inviteUser}</Dialog.Title>
80
+ </Dialog.Header>
81
+ <form onsubmit={handleSubmit} class="space-y-4">
82
+ <div class="space-y-2">
83
+ <Label for="invite-email">{lang.email}</Label>
84
+ <Input id="invite-email" type="email" bind:value={email} required />
85
+ </div>
86
+ <div class="space-y-2">
87
+ <Label>{lang.role}</Label>
88
+ <Select.Root type="single" value={role} onValueChange={(v) => v && (role = v as UserRole)}>
89
+ <Select.Trigger class="w-full">
90
+ {role === 'admin' ? lang.roleAdmin : lang.roleUser}
91
+ </Select.Trigger>
92
+ <Select.Content>
93
+ <Select.Item value="user">{lang.roleUser}</Select.Item>
94
+ <Select.Item value="admin">{lang.roleAdmin}</Select.Item>
95
+ </Select.Content>
96
+ </Select.Root>
97
+ </div>
98
+ {#if error}
99
+ <p class="text-destructive text-sm">{error}</p>
100
+ {/if}
101
+ <Dialog.Footer>
102
+ <Button type="button" variant="outline" onclick={() => onOpenChange(false)}>{lang.cancel}</Button>
103
+ <Button type="submit" disabled={loading}>{lang.invite.sendInvite}</Button>
104
+ </Dialog.Footer>
105
+ </form>
106
+ </Dialog.Content>
107
+ </Dialog.Root>
@@ -0,0 +1,8 @@
1
+ type Props = {
2
+ open: boolean;
3
+ onOpenChange: (open: boolean) => void;
4
+ onInvited: () => void;
5
+ };
6
+ declare const InviteUserDialog: import("svelte").Component<Props, {}, "open">;
7
+ type InviteUserDialog = ReturnType<typeof InviteUserDialog>;
8
+ export default InviteUserDialog;