@frigear-nu/account 0.0.10 → 0.1.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 (33) hide show
  1. package/README.md +7 -4
  2. package/dist/module.d.mts +5 -0
  3. package/dist/module.json +1 -1
  4. package/dist/module.mjs +32 -26
  5. package/dist/runtime/app/components/Account/AccountApiTokenSlideover.d.vue.ts +11 -0
  6. package/dist/runtime/app/components/Account/AccountApiTokenSlideover.vue +95 -0
  7. package/dist/runtime/app/components/Account/AccountApiTokenSlideover.vue.d.ts +11 -0
  8. package/dist/runtime/app/components/Account/AccountHeader.vue +14 -34
  9. package/dist/runtime/app/components/Account/AccountMenu.d.vue.ts +10 -1
  10. package/dist/runtime/app/components/Account/AccountMenu.vue +100 -51
  11. package/dist/runtime/app/components/Account/AccountMenu.vue.d.ts +10 -1
  12. package/dist/runtime/app/composables/index.d.ts +2 -0
  13. package/dist/runtime/app/composables/index.js +1 -0
  14. package/dist/runtime/app/composables/useAccountNav.d.ts +20 -0
  15. package/dist/runtime/app/composables/useAccountNav.js +18 -0
  16. package/dist/runtime/app/layouts/account.d.vue.ts +13 -0
  17. package/dist/runtime/app/layouts/account.vue +54 -0
  18. package/dist/runtime/app/layouts/account.vue.d.ts +13 -0
  19. package/dist/runtime/app/pages/api-tokens.vue +193 -261
  20. package/dist/runtime/app/pages/documents.vue +25 -26
  21. package/dist/runtime/app/pages/index.vue +73 -17
  22. package/dist/runtime/app/pages/profile.vue +152 -169
  23. package/dist/runtime/app/pages/security.vue +156 -151
  24. package/dist/runtime/app/utils/api-tokens.d.ts +19 -0
  25. package/dist/runtime/app/utils/api-tokens.js +0 -0
  26. package/dist/runtime/i18n/lang/da.json +41 -9
  27. package/dist/runtime/i18n/lang/en.json +41 -9
  28. package/dist/runtime/menus.d.ts +1 -1
  29. package/dist/runtime/menus.js +10 -0
  30. package/package.json +21 -15
  31. package/dist/runtime/app/pages/layout.d.vue.ts +0 -3
  32. package/dist/runtime/app/pages/layout.vue +0 -10
  33. package/dist/runtime/app/pages/layout.vue.d.ts +0 -3
@@ -1,24 +1,80 @@
1
1
  <script setup>
2
2
  defineOptions({ name: "AccountIndexPage" });
3
- const { data: tiles } = useMenu("account");
3
+ definePageMeta({ heading: false });
4
+ const t = useModuleT("account");
5
+ const { user } = useUserSession();
6
+ const { localize } = useLocaleLink();
7
+ const { items } = await useAccountNav();
8
+ const index = useRuntimeConfig().public.account?.pages?.index;
9
+ const WidgetArea = resolveComponent("WidgetArea");
10
+ const widgetArea = typeof WidgetArea === "string" ? void 0 : WidgetArea;
11
+ const shortcuts = computed(() => items.value.filter((item) => item.to !== index));
12
+ const profile = computed(() => {
13
+ const value = user.value;
14
+ const name = value?.name || value?.email || t("title", "Account");
15
+ return { name, email: value?.email ?? void 0, avatar: { src: value?.avatar || void 0, alt: name } };
16
+ });
4
17
  </script>
5
18
 
6
19
  <template>
7
- <UContainer>
8
- <UPage>
9
- <UPageBody>
10
- <UPageGrid>
11
- <UPageCard
12
- v-for="tile in tiles"
13
- :key="tile.id"
14
- :title="tile.label"
15
- :description="tile.description"
16
- :icon="tile.icon"
17
- :to="tile.to"
18
- orientation="horizontal"
20
+ <div class="flex flex-col gap-4">
21
+ <PageHeading
22
+ :title="t('overview.title', 'Account')"
23
+ :description="t('overview.description', 'Your details and everything that belongs to your account.')"
24
+ />
25
+
26
+ <SectionCard>
27
+ <div class="flex flex-wrap items-center justify-between gap-4">
28
+ <UUser
29
+ :name="profile.name"
30
+ :description="profile.email"
31
+ :avatar="profile.avatar"
32
+ size="lg"
33
+ :ui="{ name: 'truncate', description: 'truncate' }"
34
+ />
35
+ <AccountHeaderRight />
36
+ </div>
37
+ </SectionCard>
38
+
39
+ <component
40
+ :is="widgetArea"
41
+ v-if="widgetArea"
42
+ name="account.overview"
43
+ :context="{ user }"
44
+ />
45
+
46
+ <ListCard
47
+ :title="t('overview.shortcuts', 'Your account')"
48
+ :is-empty="!shortcuts.length"
49
+ :empty="t('overview.empty', 'Nothing here yet.')"
50
+ >
51
+ <li
52
+ v-for="item in shortcuts"
53
+ :key="item.id"
54
+ >
55
+ <NuxtLink
56
+ :to="item.external ? item.to : localize(item.to)"
57
+ :external="item.external"
58
+ class="group flex items-center gap-3"
59
+ >
60
+ <UIcon
61
+ v-if="item.icon"
62
+ :name="item.icon"
63
+ class="size-5 shrink-0 text-muted"
64
+ />
65
+ <span class="min-w-0 flex-1">
66
+ <span class="block font-medium text-highlighted group-hover:text-primary">{{ item.label }}</span>
67
+ <span
68
+ v-if="item.description"
69
+ class="block truncate text-muted"
70
+ >{{ item.description }}</span>
71
+ </span>
72
+ <UIcon
73
+ name="i-lucide-chevron-right"
74
+ class="size-4 shrink-0 text-dimmed"
19
75
  />
20
- </UPageGrid>
21
- </UPageBody>
22
- </UPage>
23
- </UContainer>
76
+ </NuxtLink>
77
+ </li>
78
+ </ListCard>
79
+ </div>
24
80
  </template>
@@ -5,7 +5,7 @@ const t = useModuleT("account.profile");
5
5
  const toast = useToast();
6
6
  const route = useRoute();
7
7
  const { user } = useUserSession();
8
- const { updateProfileDetails, changeEmailAddress, error } = useAccount();
8
+ const { updateProfileDetails, changeEmailAddress } = useAccount();
9
9
  const schema = z.object({
10
10
  name: z.string().trim().min(1, t("name.required", "Please enter your name.")),
11
11
  avatar: z.union([z.literal(""), z.url(t("avatar.invalid", "Enter a valid URL."))]).optional()
@@ -25,194 +25,177 @@ watch(saved, (next) => {
25
25
  }
26
26
  });
27
27
  const dirty = computed(() => state.name !== saved.value.name || state.avatar !== saved.value.avatar);
28
- const saving = ref(false);
29
- async function onSubmitProfile(event) {
30
- saving.value = true;
31
- try {
32
- await updateProfileDetails({ name: event.data.name, avatar: event.data.avatar || void 0 });
33
- toast.add({ title: t("saved", "Profile updated"), color: "success" });
34
- } catch (e) {
35
- toast.add({ title: e.statusMessage ?? t("saveFailed", "Could not update your profile."), color: "error" });
36
- } finally {
37
- saving.value = false;
38
- }
28
+ const profileAction = useAction();
29
+ function onSubmitProfile(event) {
30
+ return profileAction.run(
31
+ () => updateProfileDetails({ name: event.data.name, avatar: event.data.avatar || void 0 }),
32
+ { success: t("saved", "Profile updated"), error: t("saveFailed", "Could not update your profile.") }
33
+ );
39
34
  }
40
35
  const emailModalOpen = ref(false);
41
36
  const newEmail = ref("");
42
- const emailPending = ref(false);
43
37
  const emailVerificationSent = ref(false);
44
38
  const emailChanged = computed(() => route.query["email-changed"] === "1");
45
39
  const emailChangeFailed = computed(() => route.query.error === "email-change");
46
40
  const emailSchema = z.object({ email: z.email(t("email.invalid", "Enter a valid email address.")) });
41
+ const emailAction = useAction();
47
42
  async function onSubmitEmail() {
48
- emailPending.value = true;
49
- try {
50
- const result = await changeEmailAddress({ email: newEmail.value });
51
- emailVerificationSent.value = result.verificationRequired;
52
- emailModalOpen.value = false;
53
- newEmail.value = "";
54
- toast.add({ title: result.verificationRequired ? t("email.change.sent.title", "Check your inbox") : t("email.change.done", "Your email address was changed."), color: "success" });
55
- } catch (e) {
56
- toast.add({ title: e.statusMessage ?? t("email.change.error", "Could not change your email address."), color: "error" });
57
- } finally {
58
- emailPending.value = false;
59
- }
43
+ const result = await emailAction.run(
44
+ () => changeEmailAddress({ email: newEmail.value }),
45
+ { error: t("email.change.error", "Could not change your email address.") }
46
+ );
47
+ if (!result) return;
48
+ emailVerificationSent.value = result.verificationRequired === true;
49
+ emailModalOpen.value = false;
50
+ newEmail.value = "";
51
+ toast.add({ title: result.verificationRequired ? t("email.change.sent.title", "Check your inbox") : t("email.change.done", "Your email address was changed."), color: "success" });
60
52
  }
61
53
  </script>
62
54
 
63
55
  <template>
64
- <UContainer>
65
- <UPage>
66
- <UPageBody>
67
- <UAlert
68
- v-if="emailChanged"
69
- color="success"
70
- variant="subtle"
71
- :title="t('email.change.done', 'Your email address was changed.')"
72
- />
73
- <UAlert
74
- v-if="emailChangeFailed"
75
- color="error"
76
- variant="subtle"
77
- :title="t('email.change.failed', 'This confirmation link is invalid or has expired.')"
78
- />
79
- <UAlert
80
- v-if="emailVerificationSent"
81
- variant="subtle"
82
- :title="t('email.change.sent.title', 'Check your inbox')"
83
- :description="t('email.change.sent.description', 'We sent a link to your new address. It takes effect once you confirm it.')"
84
- />
56
+ <div class="flex flex-col gap-4">
57
+ <UAlert
58
+ v-if="emailChanged"
59
+ color="success"
60
+ variant="subtle"
61
+ :title="t('email.change.done', 'Your email address was changed.')"
62
+ />
63
+ <UAlert
64
+ v-if="emailChangeFailed"
65
+ color="error"
66
+ variant="subtle"
67
+ :title="t('email.change.failed', 'This confirmation link is invalid or has expired.')"
68
+ />
69
+ <UAlert
70
+ v-if="emailVerificationSent"
71
+ variant="subtle"
72
+ :title="t('email.change.sent.title', 'Check your inbox')"
73
+ :description="t('email.change.sent.description', 'We sent a link to your new address. It takes effect once you confirm it.')"
74
+ />
85
75
 
86
- <UForm
87
- id="profile"
88
- :schema="schema"
89
- :state="state"
90
- @submit="onSubmitProfile"
76
+ <SectionCard
77
+ :title="t('title', 'Profile')"
78
+ :description="t('description', 'Change your information.')"
79
+ >
80
+ <UForm
81
+ id="account-profile"
82
+ :schema="schema"
83
+ :state="state"
84
+ class="flex flex-col gap-4"
85
+ @submit="onSubmitProfile"
86
+ >
87
+ <UFormField
88
+ name="name"
89
+ :label="t('name.label', 'Name')"
90
+ :description="t('name.description', 'Your full name.')"
91
+ required
92
+ class="flex items-start justify-between gap-4 max-sm:flex-col"
91
93
  >
92
- <UPageCard
93
- variant="subtle"
94
- :title="t('title', 'Profile')"
95
- :description="t('description', 'Change your information.')"
96
- >
97
- <UFormField
98
- name="name"
99
- :label="t('name.label', 'Name')"
100
- :description="t('name.description', 'Your full name.')"
101
- required
102
- class="flex items-start justify-between gap-4 max-sm:flex-col"
103
- >
104
- <UInput
105
- v-model="state.name"
106
- autocomplete="name"
107
- />
108
- </UFormField>
109
- <USeparator />
110
- <UFormField
111
- name="avatar"
112
- :label="t('avatar.label', 'Avatar')"
113
- :description="t('avatar.description', 'Link to a square image.')"
114
- class="flex items-start justify-between gap-4 max-sm:flex-col"
115
- >
116
- <div class="flex items-center gap-3">
117
- <UAvatar
118
- :src="state.avatar || void 0"
119
- :alt="state.name"
120
- size="xl"
121
- />
122
- <UInput
123
- v-model="state.avatar"
124
- type="url"
125
- placeholder="https://"
126
- :aria-label="t('avatar.url', 'Avatar URL')"
127
- />
128
- </div>
129
- </UFormField>
130
- <USeparator />
131
- <div class="flex justify-end">
132
- <UButton
133
- type="submit"
134
- :loading="saving"
135
- :disabled="!dirty"
136
- :label="t('save', 'Save profile')"
137
- />
138
- </div>
139
- </UPageCard>
140
- </UForm>
141
-
142
- <UPageCard
143
- variant="subtle"
144
- :title="t('email.label', 'Email')"
145
- :description="t('email.description', 'Your email address.')"
94
+ <UInput
95
+ v-model="state.name"
96
+ autocomplete="name"
97
+ />
98
+ </UFormField>
99
+ <USeparator />
100
+ <UFormField
101
+ name="avatar"
102
+ :label="t('avatar.label', 'Avatar')"
103
+ :description="t('avatar.description', 'Link to a square image.')"
104
+ class="flex items-start justify-between gap-4 max-sm:flex-col"
146
105
  >
147
- <div class="flex items-center justify-between gap-4 max-sm:flex-col max-sm:items-start">
148
- <UInput
149
- :model-value="user?.email ?? ''"
150
- type="email"
151
- readonly
152
- disabled
153
- class="w-full sm:max-w-xs"
154
- :aria-label="t('email.label', 'Email')"
106
+ <div class="flex items-center gap-3">
107
+ <UAvatar
108
+ :src="state.avatar || void 0"
109
+ :alt="state.name"
110
+ size="xl"
155
111
  />
156
- <UButton
157
- variant="subtle"
158
- trailing-icon="i-lucide-pencil"
159
- :label="t('email.change.title', 'Change email')"
160
- @click="emailModalOpen = true"
112
+ <UInput
113
+ v-model="state.avatar"
114
+ type="url"
115
+ placeholder="https://"
116
+ :aria-label="t('avatar.url', 'Avatar URL')"
161
117
  />
162
118
  </div>
163
- </UPageCard>
119
+ </UFormField>
120
+ </UForm>
121
+ <template #footer>
122
+ <div class="flex justify-end">
123
+ <UButton
124
+ type="submit"
125
+ form="account-profile"
126
+ :loading="profileAction.busy.value"
127
+ :disabled="!dirty"
128
+ :label="t('save', 'Save profile')"
129
+ />
130
+ </div>
131
+ </template>
132
+ </SectionCard>
164
133
 
165
- <UAlert
166
- v-if="error"
167
- color="error"
168
- variant="subtle"
169
- :title="error"
134
+ <SectionCard
135
+ :title="t('email.label', 'Email')"
136
+ :description="t('email.description', 'Your email address.')"
137
+ >
138
+ <template #actions>
139
+ <UButton
140
+ color="neutral"
141
+ variant="outline"
142
+ icon="i-lucide-pencil"
143
+ :label="t('email.change.title', 'Change email')"
144
+ @click="emailModalOpen = true"
170
145
  />
146
+ </template>
147
+ <UInput
148
+ :model-value="user?.email ?? ''"
149
+ type="email"
150
+ readonly
151
+ disabled
152
+ class="w-full sm:max-w-xs"
153
+ :aria-label="t('email.label', 'Email')"
154
+ />
155
+ </SectionCard>
171
156
 
172
- <UModal
173
- v-model:open="emailModalOpen"
174
- :title="t('email.change.title', 'Change email')"
157
+ <UModal
158
+ v-model:open="emailModalOpen"
159
+ :title="t('email.change.title', 'Change email')"
160
+ >
161
+ <template #body>
162
+ <UForm
163
+ id="account-change-email"
164
+ :schema="emailSchema"
165
+ :state="{ email: newEmail }"
166
+ class="flex flex-col gap-4"
167
+ @submit="onSubmitEmail"
175
168
  >
176
- <template #body>
177
- <UForm
178
- id="change-email"
179
- :schema="emailSchema"
180
- :state="{ email: newEmail }"
181
- class="flex flex-col gap-4"
182
- @submit="onSubmitEmail"
183
- >
184
- <UFormField
185
- name="email"
186
- :label="t('email.change.label', 'New e-mail')"
187
- required
188
- >
189
- <UInput
190
- v-model="newEmail"
191
- type="email"
192
- autocomplete="email"
193
- class="w-full"
194
- />
195
- </UFormField>
196
- </UForm>
197
- </template>
198
- <template #footer>
199
- <div class="flex w-full justify-end gap-2">
200
- <UButton
201
- color="neutral"
202
- variant="outline"
203
- :label="t('cancel', 'Cancel')"
204
- @click="emailModalOpen = false"
205
- />
206
- <UButton
207
- type="submit"
208
- form="change-email"
209
- :loading="emailPending"
210
- :label="t('email.change.title', 'Change email')"
211
- />
212
- </div>
213
- </template>
214
- </UModal>
215
- </UPageBody>
216
- </UPage>
217
- </UContainer>
169
+ <UFormField
170
+ name="email"
171
+ :label="t('email.change.label', 'New e-mail')"
172
+ required
173
+ >
174
+ <UInput
175
+ v-model="newEmail"
176
+ type="email"
177
+ autocomplete="email"
178
+ class="w-full"
179
+ />
180
+ </UFormField>
181
+ </UForm>
182
+ </template>
183
+ <template #footer>
184
+ <div class="flex w-full justify-end gap-2">
185
+ <UButton
186
+ color="neutral"
187
+ variant="outline"
188
+ :label="t('cancel', 'Cancel')"
189
+ @click="emailModalOpen = false"
190
+ />
191
+ <UButton
192
+ type="submit"
193
+ form="account-change-email"
194
+ :loading="emailAction.busy.value"
195
+ :label="t('email.change.title', 'Change email')"
196
+ />
197
+ </div>
198
+ </template>
199
+ </UModal>
200
+ </div>
218
201
  </template>