@motor-cms/ui-admin 1.1.0-alpha.4 → 1.1.0-alpha.5

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 (73) hide show
  1. package/app/assets/css/v-onboarding.css +64 -0
  2. package/app/components/OnboardingStep.vue +42 -0
  3. package/app/components/UsersOnboarding.vue +84 -0
  4. package/app/components/client/FooterSlotCard.vue +313 -0
  5. package/app/components/client/GlobalComponentsSection.vue +65 -0
  6. package/app/components/dashboard/DashboardActivity.vue +71 -0
  7. package/app/components/dashboard/DashboardActivityItem.vue +96 -0
  8. package/app/components/dashboard/DashboardAnnouncementModal.vue +327 -0
  9. package/app/components/dashboard/DashboardAnnouncements.vue +93 -0
  10. package/app/components/dashboard/DashboardOnboarding.vue +285 -0
  11. package/app/components/dashboard/DashboardPublishingQueue.vue +47 -0
  12. package/app/components/dashboard/DashboardQuickActions.vue +44 -0
  13. package/app/components/dashboard/DashboardStats.vue +63 -0
  14. package/app/components/form/inputs/CategoryTreePicker.vue +265 -109
  15. package/app/components/form/inputs/EntityConfigurationsPanel.vue +235 -0
  16. package/app/composables/useClientFormExtensions.ts +89 -0
  17. package/app/composables/useClientLanguages.ts +81 -0
  18. package/app/composables/useDashboardData.ts +169 -0
  19. package/app/composables/useOnboardingState.ts +151 -0
  20. package/app/data/footerTemplate.ts +283 -0
  21. package/app/lang/de/motor-admin/ai_system_prompts.json +1 -0
  22. package/app/lang/de/motor-admin/categories.json +1 -0
  23. package/app/lang/de/motor-admin/category_trees.json +2 -1
  24. package/app/lang/de/motor-admin/clients.json +17 -1
  25. package/app/lang/de/motor-admin/config_variables.json +1 -0
  26. package/app/lang/de/motor-admin/dashboard.json +83 -0
  27. package/app/lang/de/motor-admin/domains.json +6 -1
  28. package/app/lang/de/motor-admin/email_templates.json +1 -0
  29. package/app/lang/de/motor-admin/entity_configurations.json +12 -0
  30. package/app/lang/de/motor-admin/languages.json +1 -0
  31. package/app/lang/de/motor-admin/onboarding.json +60 -0
  32. package/app/lang/de/motor-admin/permissions.json +1 -0
  33. package/app/lang/de/motor-admin/roles.json +1 -0
  34. package/app/lang/de/motor-admin/users.json +1 -0
  35. package/app/lang/en/motor-admin/ai_system_prompts.json +1 -0
  36. package/app/lang/en/motor-admin/categories.json +1 -0
  37. package/app/lang/en/motor-admin/category_trees.json +2 -1
  38. package/app/lang/en/motor-admin/clients.json +17 -1
  39. package/app/lang/en/motor-admin/config_variables.json +1 -0
  40. package/app/lang/en/motor-admin/dashboard.json +83 -0
  41. package/app/lang/en/motor-admin/domains.json +6 -1
  42. package/app/lang/en/motor-admin/email_templates.json +1 -0
  43. package/app/lang/en/motor-admin/entity_configurations.json +12 -0
  44. package/app/lang/en/motor-admin/languages.json +1 -0
  45. package/app/lang/en/motor-admin/onboarding.json +60 -0
  46. package/app/lang/en/motor-admin/permissions.json +1 -0
  47. package/app/lang/en/motor-admin/roles.json +1 -0
  48. package/app/lang/en/motor-admin/users.json +1 -0
  49. package/app/pages/index.vue +119 -22
  50. package/app/pages/login.vue +6 -0
  51. package/app/pages/motor-admin/ai-system-prompts/[id]/edit.vue +4 -4
  52. package/app/pages/motor-admin/category-trees/[id]/categories/[categoryId]/edit.vue +4 -3
  53. package/app/pages/motor-admin/category-trees/[id]/edit.vue +4 -4
  54. package/app/pages/motor-admin/clients/[id]/edit.vue +146 -6
  55. package/app/pages/motor-admin/clients/create.vue +34 -2
  56. package/app/pages/motor-admin/config-variables/[id]/edit.vue +4 -4
  57. package/app/pages/motor-admin/domains/[id]/edit.vue +18 -5
  58. package/app/pages/motor-admin/email-templates/[id]/edit.vue +4 -4
  59. package/app/pages/motor-admin/email-templates/index.vue +36 -25
  60. package/app/pages/motor-admin/languages/[id]/edit.vue +17 -4
  61. package/app/pages/motor-admin/languages/create.vue +13 -0
  62. package/app/pages/motor-admin/permission-groups/[id]/edit.vue +4 -4
  63. package/app/pages/motor-admin/roles/[id]/edit.vue +4 -4
  64. package/app/pages/motor-admin/roles/create.vue +4 -1
  65. package/app/pages/motor-admin/users/[id]/edit.vue +4 -3
  66. package/app/pages/motor-admin/users/index.vue +1 -0
  67. package/app/pages/profile.vue +47 -1
  68. package/app/pages/search.vue +13 -3
  69. package/app/types/generated/form-meta.ts +24 -20
  70. package/app/types/generated/grid-meta.ts +5 -3
  71. package/nuxt.config.ts +15 -1
  72. package/package.json +6 -2
  73. package/app/pages/dashboard.vue +0 -5
@@ -0,0 +1,285 @@
1
+ <script setup lang="ts">
2
+ import { useVOnboarding } from 'v-onboarding'
3
+ import type { User } from '@motor-cms/ui-core/app/types/auth'
4
+
5
+ const { t } = useI18n()
6
+ const { can } = usePermissions()
7
+ const { user } = useSanctumAuth<User>()
8
+ const router = useRouter()
9
+ const { completeOnboarding } = useProfileApi()
10
+ const { commitDone } = useOnboardingDone()
11
+ const { isEnabled } = useOnboardingEnabled()
12
+
13
+ const { isCompleted: announcementsCompleted, markCompleted: markAnnouncementsDone } = useOnboardingState('dashboard-announcements')
14
+ const { isCompleted: notificationsCompleted, markCompleted: markNotificationsDone } = useOnboardingState('notifications')
15
+ const { isCompleted: searchCompleted, markCompleted: markSearchDone } = useOnboardingState('search')
16
+ const { isCompleted: adminNavCompleted, markCompleted: markAdminNavDone } = useOnboardingState('admin-nav')
17
+ const { resetAll: resetOnboardingState } = useOnboardingResetAll()
18
+ const { setPending } = usePendingOnboarding()
19
+
20
+ // ── Wrappers ──────────────────────────────────────────────────────────────────
21
+ const announcementsWrapper = ref(null)
22
+ const notificationsWrapper = ref(null)
23
+ const searchWrapper = ref(null)
24
+ const adminNavWrapper = ref(null)
25
+
26
+ const { start: startAnnouncements, finish: finishAnnouncements } = useVOnboarding(announcementsWrapper)
27
+ const { start: startNotifications, finish: finishNotifications } = useVOnboarding(notificationsWrapper)
28
+ const { start: startSearch, finish: finishSearch } = useVOnboarding(searchWrapper)
29
+ const { start: startAdminNav, finish: finishAdminNav } = useVOnboarding(adminNavWrapper)
30
+
31
+ // ── Shared options ────────────────────────────────────────────────────────────
32
+ const options = computed(() => ({
33
+ scrollToStep: { enabled: true, options: { behavior: 'smooth' as ScrollBehavior, block: 'center' as ScrollLogicalPosition } },
34
+ popper: { strategy: 'fixed' as const },
35
+ labels: {
36
+ previousButton: t('motor-admin.onboarding.previous'),
37
+ nextButton: t('motor-admin.onboarding.next'),
38
+ finishButton: t('motor-admin.onboarding.finish'),
39
+ },
40
+ }))
41
+
42
+ // ── Announcements steps ───────────────────────────────────────────────────────
43
+ const canCreate = computed(() => can('dashboard-announcements.write'))
44
+
45
+ const announcementSteps = computed(() => {
46
+ const steps = [
47
+ {
48
+ attachTo: { element: '#onboarding-announcements-card' },
49
+ content: {
50
+ title: t('motor-admin.onboarding.announcements.step1_title'),
51
+ description: t('motor-admin.onboarding.announcements.step1_desc'),
52
+ },
53
+ },
54
+ {
55
+ attachTo: { element: '#onboarding-announcements-body' },
56
+ content: {
57
+ title: t('motor-admin.onboarding.announcements.step2_title'),
58
+ description: t('motor-admin.onboarding.announcements.step2_desc'),
59
+ },
60
+ },
61
+ ]
62
+
63
+ if (canCreate.value) {
64
+ steps.push({
65
+ attachTo: { element: '#onboarding-announcements-create' },
66
+ content: {
67
+ title: t('motor-admin.onboarding.announcements.step3_title'),
68
+ description: t('motor-admin.onboarding.announcements.step3_desc'),
69
+ },
70
+ })
71
+ }
72
+
73
+ return steps
74
+ })
75
+
76
+ // ── Notifications steps ───────────────────────────────────────────────────────
77
+ // Single step only: opening the slideover during an active v-onboarding tour
78
+ // creates an unresolvable z-index conflict — v-onboarding's own overlay covers
79
+ // the slideover panel, and the slideover teleports to body after the tooltip,
80
+ // making either element hide the other. All relevant info is covered in one step.
81
+ const notificationsSteps = computed(() => [
82
+ {
83
+ attachTo: { element: '#onboarding-notification-bell' },
84
+ content: {
85
+ title: t('motor-admin.onboarding.notifications.step1_title'),
86
+ description: t('motor-admin.onboarding.notifications.step1_desc'),
87
+ },
88
+ options: {
89
+ popper: {
90
+ placement: 'left' as const,
91
+ },
92
+ },
93
+ },
94
+ ])
95
+
96
+ // ── Search steps ──────────────────────────────────────────────────────────────
97
+ const searchSteps = computed(() => [
98
+ {
99
+ attachTo: { element: '#onboarding-search-button' },
100
+ content: {
101
+ title: t('motor-admin.onboarding.search.step1_title'),
102
+ description: t('motor-admin.onboarding.search.step1_desc'),
103
+ },
104
+ options: { popper: { placement: 'right' as const } },
105
+ },
106
+ {
107
+ attachTo: { element: '#onboarding-search-button' },
108
+ content: {
109
+ title: t('motor-admin.onboarding.search.step2_title'),
110
+ description: t('motor-admin.onboarding.search.step2_desc'),
111
+ },
112
+ options: { popper: { placement: 'right' as const } },
113
+ },
114
+ ])
115
+
116
+ // ── Admin navigation steps ────────────────────────────────────────────────────
117
+ const adminNavSteps = computed(() => [
118
+ {
119
+ attachTo: { element: '#onboarding-sidebar-nav' },
120
+ content: {
121
+ title: t('motor-admin.onboarding.admin_nav.step1_title'),
122
+ description: t('motor-admin.onboarding.admin_nav.step1_desc'),
123
+ },
124
+ },
125
+ ])
126
+
127
+ // ── Finish handlers ───────────────────────────────────────────────────────────
128
+
129
+ /**
130
+ * Announcements tour finished → chain to notifications.
131
+ */
132
+ async function onAnnouncementsFinish() {
133
+ markAnnouncementsDone()
134
+ await nextTick()
135
+ if (!notificationsCompleted.value) {
136
+ startNotifications()
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Notifications tour finished → chain to search tour.
142
+ */
143
+ async function onNotificationsFinish() {
144
+ markNotificationsDone()
145
+ await nextTick()
146
+ if (!searchCompleted.value) {
147
+ startSearch()
148
+ }
149
+ else if (!adminNavCompleted.value) {
150
+ startAdminNav()
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Search tour finished → chain to admin-nav tour.
156
+ */
157
+ async function onSearchFinish() {
158
+ markSearchDone()
159
+ await nextTick()
160
+ if (!adminNavCompleted.value) {
161
+ startAdminNav()
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Admin-nav tour finished → mark done, store pending flag, navigate to users list.
167
+ * UsersOnboarding on the users page detects the pending flag and auto-starts
168
+ * the grid explanation tour.
169
+ */
170
+ function onAdminNavFinish() {
171
+ markAdminNavDone()
172
+ setPending('admin-grid')
173
+ router.push('/motor-admin/users')
174
+ }
175
+
176
+ // ── Skip handlers ─────────────────────────────────────────────────────────────
177
+ // Pre-mark all remaining tours as done so @finish chain conditions are false,
178
+ // then call finish() to close the wrapper.
179
+ //
180
+ // finish() is required — exit() from the slot only emits @exit without
181
+ // changing the wrapper's internal currentIndex, so the tooltip stays open.
182
+ //
183
+ // completeOnboarding() clears show_onboarding on the backend so the watch
184
+ // does not trigger another resetOnboardingState() on the next dashboard visit.
185
+ // We also set user.value.data.show_onboarding = false immediately so the
186
+ // in-memory auth cache does not cause another reset before the API response
187
+ // arrives.
188
+
189
+ function skipAll() {
190
+ // commitDone() must run BEFORE the API call — if the browser cancels the
191
+ // in-flight request on a hard reload the localStorage flag still survives
192
+ // and prevents the show_onboarding watch from resetting state.
193
+ commitDone()
194
+ markAnnouncementsDone()
195
+ markNotificationsDone()
196
+ markSearchDone()
197
+ markAdminNavDone()
198
+ if (user.value?.data) user.value.data.show_onboarding = false
199
+ completeOnboarding().catch(() => {})
200
+ }
201
+
202
+ function skipAnnouncements() { skipAll(); finishAnnouncements() }
203
+ function skipNotifications() { skipAll(); finishNotifications() }
204
+ function skipSearch() { skipAll(); finishSearch() }
205
+ function skipAdminNav() { skipAll(); finishAdminNav() }
206
+
207
+ // ── Start logic ───────────────────────────────────────────────────────────────
208
+ // Watch both the 4 wrappers AND user.value.data so the show_onboarding check
209
+ // never runs against a null/stale user — solving the race between wrapper mount
210
+ // and the sanctum /api/user response.
211
+ //
212
+ // After resetting we immediately set user.value.data.show_onboarding = false so
213
+ // that if DashboardOnboarding is unmounted and remounted (navigation away/back)
214
+ // the same session does not trigger a second reset from the in-memory cache.
215
+ // completeOnboarding() would clear the backend flag, but that is fire-and-forget
216
+ // and may resolve after a re-render; the local mutation is the reliable guard.
217
+ const stopWatch = watch(
218
+ () => [
219
+ announcementsWrapper.value,
220
+ notificationsWrapper.value,
221
+ searchWrapper.value,
222
+ adminNavWrapper.value,
223
+ user.value?.data,
224
+ ] as const,
225
+ async ([announcementsW, notificationsW, searchW, adminNavW, userData]) => {
226
+ if (!announcementsW || !notificationsW || !searchW || !adminNavW || !userData) return
227
+ stopWatch()
228
+
229
+ const shouldRun = isEnabled.value
230
+
231
+ if (shouldRun) {
232
+ userData.show_onboarding = false
233
+ resetOnboardingState()
234
+ }
235
+
236
+ if (shouldRun && !announcementsCompleted.value) {
237
+ startAnnouncements()
238
+ }
239
+ },
240
+ { immediate: true },
241
+ )
242
+ </script>
243
+
244
+ <template>
245
+ <VOnboardingWrapper
246
+ ref="announcementsWrapper"
247
+ :steps="announcementSteps"
248
+ :options="options"
249
+ @finish="onAnnouncementsFinish"
250
+ >
251
+ <template #default="{ step, next, previous, isFirst, isLast }">
252
+ <OnboardingStep :step="step" :next="next" :previous="previous" :skip="skipAnnouncements" :is-first="isFirst" :is-last="isLast" />
253
+ </template>
254
+ </VOnboardingWrapper>
255
+ <VOnboardingWrapper
256
+ ref="notificationsWrapper"
257
+ :steps="notificationsSteps"
258
+ :options="options"
259
+ @finish="onNotificationsFinish"
260
+ >
261
+ <template #default="{ step, next, previous, isFirst, isLast }">
262
+ <OnboardingStep :step="step" :next="next" :previous="previous" :skip="skipNotifications" :is-first="isFirst" :is-last="isLast" />
263
+ </template>
264
+ </VOnboardingWrapper>
265
+ <VOnboardingWrapper
266
+ ref="searchWrapper"
267
+ :steps="searchSteps"
268
+ :options="options"
269
+ @finish="onSearchFinish"
270
+ >
271
+ <template #default="{ step, next, previous, isFirst, isLast }">
272
+ <OnboardingStep :step="step" :next="next" :previous="previous" :skip="skipSearch" :is-first="isFirst" :is-last="isLast" />
273
+ </template>
274
+ </VOnboardingWrapper>
275
+ <VOnboardingWrapper
276
+ ref="adminNavWrapper"
277
+ :steps="adminNavSteps"
278
+ :options="options"
279
+ @finish="onAdminNavFinish"
280
+ >
281
+ <template #default="{ step, next, previous, isFirst, isLast }">
282
+ <OnboardingStep :step="step" :next="next" :previous="previous" :skip="skipAdminNav" :is-first="isFirst" :is-last="isLast" />
283
+ </template>
284
+ </VOnboardingWrapper>
285
+ </template>
@@ -0,0 +1,47 @@
1
+ <script setup lang="ts">
2
+ import type { PublishingQueueItem } from '../../composables/useDashboardData'
3
+
4
+ const { t, locale } = useI18n()
5
+
6
+ defineProps<{
7
+ items: PublishingQueueItem[]
8
+ loading: boolean
9
+ }>()
10
+
11
+ function formatDate(isoString: string): string {
12
+ return new Date(isoString).toLocaleDateString(locale.value, {
13
+ day: 'numeric',
14
+ month: 'long',
15
+ hour: '2-digit',
16
+ minute: '2-digit',
17
+ })
18
+ }
19
+ </script>
20
+
21
+ <template>
22
+ <UPageCard :ui="{ root: 'border-0 shadow-none' }">
23
+ <template #header>
24
+ <div class="flex items-center gap-2">
25
+ <UIcon name="i-lucide-clock" class="size-4 text-primary" />
26
+ <span class="text-sm font-heading font-semibold text-highlighted">{{ t('motor-admin.dashboard.publishing_queue.title') }}</span>
27
+ </div>
28
+ </template>
29
+ <div v-if="loading" class="flex items-center justify-center py-6 text-muted">
30
+ <UIcon name="i-lucide-loader-2" class="size-5 animate-spin" />
31
+ </div>
32
+ <div v-else-if="items.length === 0" class="text-sm text-muted py-4 text-center">
33
+ {{ t('motor-admin.dashboard.publishing_queue.empty') }}
34
+ </div>
35
+ <div v-else class="flex flex-col gap-2">
36
+ <NuxtLink
37
+ v-for="item in items"
38
+ :key="item.id"
39
+ :to="`/motor-builder/builder-pages/${item.publishable_id}/edit`"
40
+ class="p-3 rounded-lg bg-muted hover:bg-accented transition-colors"
41
+ >
42
+ <div class="text-sm font-medium text-default">{{ item.name }}</div>
43
+ <div class="text-xs text-dimmed mt-1">{{ formatDate(item.to_be_published_at) }}</div>
44
+ </NuxtLink>
45
+ </div>
46
+ </UPageCard>
47
+ </template>
@@ -0,0 +1,44 @@
1
+ <script setup lang="ts">
2
+ const { t } = useI18n()
3
+
4
+ const emit = defineEmits<{
5
+ 'create-page': []
6
+ 'upload-media': []
7
+ }>()
8
+ </script>
9
+
10
+ <template>
11
+ <UPageCard>
12
+ <template #header>
13
+ <span class="text-sm font-heading font-semibold text-highlighted">{{ t('motor-admin.dashboard.quick_actions.title') }}</span>
14
+ </template>
15
+ <div class="flex flex-col gap-2">
16
+ <UButton
17
+ icon="i-lucide-plus"
18
+ color="primary"
19
+ block
20
+ @click="emit('create-page')"
21
+ >
22
+ {{ t('motor-admin.dashboard.quick_actions.new_page') }}
23
+ </UButton>
24
+ <UButton
25
+ icon="i-lucide-upload"
26
+ color="neutral"
27
+ variant="outline"
28
+ block
29
+ @click="emit('upload-media')"
30
+ >
31
+ {{ t('motor-admin.dashboard.quick_actions.upload_media') }}
32
+ </UButton>
33
+ <UButton
34
+ icon="i-lucide-menu"
35
+ color="neutral"
36
+ variant="outline"
37
+ block
38
+ to="/motor-builder/navigation-trees"
39
+ >
40
+ {{ t('motor-admin.dashboard.quick_actions.navigation') }}
41
+ </UButton>
42
+ </div>
43
+ </UPageCard>
44
+ </template>
@@ -0,0 +1,63 @@
1
+ <script setup lang="ts">
2
+ import type { DashboardStats } from '../../composables/useDashboardData'
3
+
4
+ const { t } = useI18n()
5
+
6
+ const props = defineProps<{
7
+ stats: DashboardStats
8
+ loading: boolean
9
+ }>()
10
+
11
+ const cards = computed(() => [
12
+ {
13
+ label: t('motor-admin.dashboard.stats.pages'),
14
+ value: props.stats.pages_total,
15
+ subtitle: t('motor-admin.dashboard.stats.pages_total'),
16
+ icon: 'i-lucide-file-text',
17
+ },
18
+ {
19
+ label: t('motor-admin.dashboard.stats.drafts'),
20
+ value: props.stats.pages_draft,
21
+ subtitle: t('motor-admin.dashboard.stats.drafts_subtitle'),
22
+ icon: 'i-lucide-pencil',
23
+ },
24
+ {
25
+ label: t('motor-admin.dashboard.stats.scheduled'),
26
+ value: props.stats.pages_scheduled,
27
+ subtitle: t('motor-admin.dashboard.stats.scheduled_subtitle'),
28
+ icon: 'i-lucide-clock',
29
+ },
30
+ {
31
+ label: t('motor-admin.dashboard.stats.media'),
32
+ value: props.stats.media_total,
33
+ subtitle: t('motor-admin.dashboard.stats.media_subtitle'),
34
+ icon: 'i-lucide-image',
35
+ },
36
+ ])
37
+ </script>
38
+
39
+ <template>
40
+ <div class="grid grid-cols-2 lg:grid-cols-4 gap-4">
41
+ <UPageCard
42
+ v-for="card in cards"
43
+ :key="card.label"
44
+ :ui="{ root: 'border-0 shadow-none' }"
45
+ >
46
+ <div class="flex items-center gap-3 mb-2">
47
+ <div class="flex items-center justify-center size-8 rounded-lg bg-primary/10">
48
+ <UIcon
49
+ :name="card.icon"
50
+ class="size-4 text-primary"
51
+ />
52
+ </div>
53
+ <span class="text-sm text-muted">{{ card.label }}</span>
54
+ </div>
55
+ <div class="text-3xl font-heading font-bold text-highlighted">
56
+ {{ loading ? '—' : card.value }}
57
+ </div>
58
+ <div class="text-xs text-dimmed mt-1">
59
+ {{ card.subtitle }}
60
+ </div>
61
+ </UPageCard>
62
+ </div>
63
+ </template>