@motor-cms/ui-admin 1.7.0 → 1.8.1
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.
- package/app/assets/css/v-onboarding.css +64 -0
- package/app/components/UsersOnboarding.vue +65 -0
- package/app/components/dashboard/DashboardAnnouncements.vue +5 -0
- package/app/components/dashboard/DashboardOnboarding.vue +223 -0
- package/app/composables/useOnboardingState.ts +101 -0
- package/app/lang/de/motor-admin/onboarding.json +57 -0
- package/app/lang/en/motor-admin/onboarding.json +57 -0
- package/app/pages/index.vue +20 -1
- package/app/pages/motor-admin/users/index.vue +1 -0
- package/app/pages/profile.vue +46 -1
- package/nuxt.config.ts +8 -1
- package/package.json +2 -2
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/* ==========================================================================
|
|
2
|
+
v-onboarding — Nuxt UI v4 theme integration
|
|
3
|
+
========================================================================== */
|
|
4
|
+
|
|
5
|
+
/* The library (v-onboarding/nuxt) injects its own inline <style> tag after
|
|
6
|
+
our stylesheet, so we need either !important or higher specificity to win.
|
|
7
|
+
We use both: [data-v-onboarding-wrapper] adds an attribute selector that
|
|
8
|
+
raises specificity above the library's class-only selectors. */
|
|
9
|
+
|
|
10
|
+
:root {
|
|
11
|
+
--v-onboarding-overlay-z: 99998;
|
|
12
|
+
--v-onboarding-step-z: 99999;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
[data-v-onboarding-wrapper] .v-onboarding-item {
|
|
16
|
+
background-color: var(--ui-bg-elevated) !important;
|
|
17
|
+
border: 1px solid var(--ui-border) !important;
|
|
18
|
+
border-radius: calc(var(--ui-radius) * 2) !important;
|
|
19
|
+
color: var(--ui-text) !important;
|
|
20
|
+
box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.15), 0 8px 10px -6px rgb(0 0 0 / 0.1) !important;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
[data-v-onboarding-wrapper] .v-onboarding-item__header-title {
|
|
24
|
+
color: var(--ui-text-highlighted) !important;
|
|
25
|
+
font-weight: 600;
|
|
26
|
+
font-size: 0.9375rem;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
[data-v-onboarding-wrapper] .v-onboarding-item__header-close:hover {
|
|
30
|
+
background-color: var(--ui-bg-accented) !important;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
[data-v-onboarding-wrapper] .v-onboarding-item__description {
|
|
34
|
+
color: var(--ui-text-muted) !important;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
[data-v-onboarding-wrapper] .v-onboarding-item__actions button.v-onboarding-btn-primary {
|
|
38
|
+
background-color: var(--ui-primary) !important;
|
|
39
|
+
border-color: transparent !important;
|
|
40
|
+
color: var(--ui-text-inverted) !important;
|
|
41
|
+
border-radius: calc(var(--ui-radius) * 2) !important;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
[data-v-onboarding-wrapper] .v-onboarding-item__actions button.v-onboarding-btn-primary:hover {
|
|
45
|
+
background-color: color-mix(in srgb, var(--ui-primary) 88%, black) !important;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
[data-v-onboarding-wrapper] .v-onboarding-item__actions button.v-onboarding-btn-primary:focus {
|
|
49
|
+
outline-color: var(--ui-primary) !important;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
[data-v-onboarding-wrapper] .v-onboarding-item__actions button.v-onboarding-btn-secondary {
|
|
53
|
+
border-color: var(--ui-border-accented) !important;
|
|
54
|
+
color: var(--ui-text) !important;
|
|
55
|
+
border-radius: calc(var(--ui-radius) * 2) !important;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
[data-v-onboarding-wrapper] .v-onboarding-item__actions button.v-onboarding-btn-secondary:hover {
|
|
59
|
+
background-color: var(--ui-bg-accented) !important;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
[data-v-onboarding-wrapper] [data-popper-arrow]::before {
|
|
63
|
+
background: var(--ui-bg-elevated) !important;
|
|
64
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
const { t } = useI18n()
|
|
3
|
+
|
|
4
|
+
const { isCompleted: adminGridCompleted, markCompleted: markAdminGridDone } = useOnboardingState('admin-grid')
|
|
5
|
+
const { getPending, clearPending, setPending } = usePendingOnboarding()
|
|
6
|
+
const { completeOnboarding } = useProfileApi()
|
|
7
|
+
const router = useRouter()
|
|
8
|
+
|
|
9
|
+
const wrapper = ref(null)
|
|
10
|
+
const { start } = useVOnboarding(wrapper)
|
|
11
|
+
|
|
12
|
+
const steps = computed(() => [
|
|
13
|
+
{
|
|
14
|
+
attachTo: { element: '#users-grid' },
|
|
15
|
+
content: {
|
|
16
|
+
title: t('motor-admin.onboarding.admin_grid.step1_title'),
|
|
17
|
+
description: t('motor-admin.onboarding.admin_grid.step1_desc'),
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
])
|
|
21
|
+
|
|
22
|
+
const options = computed(() => ({
|
|
23
|
+
scrollToStep: { enabled: true, options: { behavior: 'smooth' as ScrollBehavior, block: 'center' as ScrollLogicalPosition } },
|
|
24
|
+
popper: { strategy: 'fixed' as const },
|
|
25
|
+
labels: {
|
|
26
|
+
previousButton: t('motor-admin.onboarding.previous'),
|
|
27
|
+
nextButton: t('motor-admin.onboarding.next'),
|
|
28
|
+
finishButton: t('motor-admin.onboarding.finish'),
|
|
29
|
+
},
|
|
30
|
+
}))
|
|
31
|
+
|
|
32
|
+
function onFinish() {
|
|
33
|
+
markAdminGridDone()
|
|
34
|
+
completeOnboarding().catch(() => {})
|
|
35
|
+
setPending('builder-pages')
|
|
36
|
+
router.push('/motor-builder/builder-pages')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Auto-start when the page was reached via the admin-nav onboarding chain.
|
|
40
|
+
// The pending flag is set in DashboardOnboarding.vue after the admin-nav tour finishes.
|
|
41
|
+
const stopWatch = watch(
|
|
42
|
+
wrapper,
|
|
43
|
+
async (w) => {
|
|
44
|
+
if (!w) return
|
|
45
|
+
stopWatch()
|
|
46
|
+
|
|
47
|
+
if (getPending() === 'admin-grid' && !adminGridCompleted.value) {
|
|
48
|
+
clearPending()
|
|
49
|
+
// Brief delay to let the grid data load before the tooltip appears
|
|
50
|
+
await new Promise(resolve => setTimeout(resolve, 600))
|
|
51
|
+
start()
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
{ immediate: true },
|
|
55
|
+
)
|
|
56
|
+
</script>
|
|
57
|
+
|
|
58
|
+
<template>
|
|
59
|
+
<VOnboardingWrapper
|
|
60
|
+
ref="wrapper"
|
|
61
|
+
:steps="steps"
|
|
62
|
+
:options="options"
|
|
63
|
+
@finish="onFinish"
|
|
64
|
+
/>
|
|
65
|
+
</template>
|
|
@@ -23,6 +23,7 @@ const borderColors: Record<string, string> = {
|
|
|
23
23
|
</script>
|
|
24
24
|
|
|
25
25
|
<template>
|
|
26
|
+
<div id="onboarding-announcements-card">
|
|
26
27
|
<UPageCard :ui="{ root: 'border-0 shadow-none' }">
|
|
27
28
|
<template #header>
|
|
28
29
|
<div class="flex items-center justify-between w-full">
|
|
@@ -40,6 +41,7 @@ const borderColors: Record<string, string> = {
|
|
|
40
41
|
</div>
|
|
41
42
|
<UButton
|
|
42
43
|
v-if="canCreate"
|
|
44
|
+
id="onboarding-announcements-create"
|
|
43
45
|
icon="i-lucide-plus"
|
|
44
46
|
size="xs"
|
|
45
47
|
variant="ghost"
|
|
@@ -48,6 +50,7 @@ const borderColors: Record<string, string> = {
|
|
|
48
50
|
/>
|
|
49
51
|
</div>
|
|
50
52
|
</template>
|
|
53
|
+
<div id="onboarding-announcements-body">
|
|
51
54
|
<div v-if="loading" class="flex items-center justify-center py-6 text-muted">
|
|
52
55
|
<UIcon name="i-lucide-loader-2" class="size-5 animate-spin" />
|
|
53
56
|
</div>
|
|
@@ -84,5 +87,7 @@ const borderColors: Record<string, string> = {
|
|
|
84
87
|
</div>
|
|
85
88
|
</div>
|
|
86
89
|
</div>
|
|
90
|
+
</div>
|
|
87
91
|
</UPageCard>
|
|
92
|
+
</div>
|
|
88
93
|
</template>
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { User } from '@motor-cms/ui-core/app/types/auth'
|
|
3
|
+
|
|
4
|
+
const { t } = useI18n()
|
|
5
|
+
const { can } = usePermissions()
|
|
6
|
+
const { user } = useSanctumAuth<User>()
|
|
7
|
+
const router = useRouter()
|
|
8
|
+
|
|
9
|
+
const { isCompleted: announcementsCompleted, markCompleted: markAnnouncementsDone } = useOnboardingState('dashboard-announcements')
|
|
10
|
+
const { isCompleted: notificationsCompleted, markCompleted: markNotificationsDone } = useOnboardingState('notifications')
|
|
11
|
+
const { isCompleted: searchCompleted, markCompleted: markSearchDone } = useOnboardingState('search')
|
|
12
|
+
const { isCompleted: adminNavCompleted, markCompleted: markAdminNavDone } = useOnboardingState('admin-nav')
|
|
13
|
+
const { resetAll: resetOnboardingState } = useOnboardingResetAll()
|
|
14
|
+
const { setPending } = usePendingOnboarding()
|
|
15
|
+
|
|
16
|
+
// ── Wrappers ──────────────────────────────────────────────────────────────────
|
|
17
|
+
const announcementsWrapper = ref(null)
|
|
18
|
+
const notificationsWrapper = ref(null)
|
|
19
|
+
const searchWrapper = ref(null)
|
|
20
|
+
const adminNavWrapper = ref(null)
|
|
21
|
+
|
|
22
|
+
const { start: startAnnouncements } = useVOnboarding(announcementsWrapper)
|
|
23
|
+
const { start: startNotifications } = useVOnboarding(notificationsWrapper)
|
|
24
|
+
const { start: startSearch } = useVOnboarding(searchWrapper)
|
|
25
|
+
const { start: startAdminNav } = useVOnboarding(adminNavWrapper)
|
|
26
|
+
|
|
27
|
+
// ── Shared options ────────────────────────────────────────────────────────────
|
|
28
|
+
const options = computed(() => ({
|
|
29
|
+
scrollToStep: { enabled: true, options: { behavior: 'smooth' as ScrollBehavior, block: 'center' as ScrollLogicalPosition } },
|
|
30
|
+
popper: { strategy: 'fixed' as const },
|
|
31
|
+
labels: {
|
|
32
|
+
previousButton: t('motor-admin.onboarding.previous'),
|
|
33
|
+
nextButton: t('motor-admin.onboarding.next'),
|
|
34
|
+
finishButton: t('motor-admin.onboarding.finish'),
|
|
35
|
+
},
|
|
36
|
+
}))
|
|
37
|
+
|
|
38
|
+
// ── Announcements steps ───────────────────────────────────────────────────────
|
|
39
|
+
const canCreate = computed(() => can('dashboard-announcements.write'))
|
|
40
|
+
|
|
41
|
+
const announcementSteps = computed(() => {
|
|
42
|
+
const steps = [
|
|
43
|
+
{
|
|
44
|
+
attachTo: { element: '#onboarding-announcements-card' },
|
|
45
|
+
content: {
|
|
46
|
+
title: t('motor-admin.onboarding.announcements.step1_title'),
|
|
47
|
+
description: t('motor-admin.onboarding.announcements.step1_desc'),
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
attachTo: { element: '#onboarding-announcements-body' },
|
|
52
|
+
content: {
|
|
53
|
+
title: t('motor-admin.onboarding.announcements.step2_title'),
|
|
54
|
+
description: t('motor-admin.onboarding.announcements.step2_desc'),
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
if (canCreate.value) {
|
|
60
|
+
steps.push({
|
|
61
|
+
attachTo: { element: '#onboarding-announcements-create' },
|
|
62
|
+
content: {
|
|
63
|
+
title: t('motor-admin.onboarding.announcements.step3_title'),
|
|
64
|
+
description: t('motor-admin.onboarding.announcements.step3_desc'),
|
|
65
|
+
},
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return steps
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
// ── Notifications steps ───────────────────────────────────────────────────────
|
|
73
|
+
// Single step only: opening the slideover during an active v-onboarding tour
|
|
74
|
+
// creates an unresolvable z-index conflict — v-onboarding's own overlay covers
|
|
75
|
+
// the slideover panel, and the slideover teleports to body after the tooltip,
|
|
76
|
+
// making either element hide the other. All relevant info is covered in one step.
|
|
77
|
+
const notificationsSteps = computed(() => [
|
|
78
|
+
{
|
|
79
|
+
attachTo: { element: '#onboarding-notification-bell' },
|
|
80
|
+
content: {
|
|
81
|
+
title: t('motor-admin.onboarding.notifications.step1_title'),
|
|
82
|
+
description: t('motor-admin.onboarding.notifications.step1_desc'),
|
|
83
|
+
},
|
|
84
|
+
options: {
|
|
85
|
+
popper: {
|
|
86
|
+
placement: 'left' as const,
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
])
|
|
91
|
+
|
|
92
|
+
// ── Search steps ──────────────────────────────────────────────────────────────
|
|
93
|
+
const searchSteps = computed(() => [
|
|
94
|
+
{
|
|
95
|
+
attachTo: { element: '#onboarding-search-button' },
|
|
96
|
+
content: {
|
|
97
|
+
title: t('motor-admin.onboarding.search.step1_title'),
|
|
98
|
+
description: t('motor-admin.onboarding.search.step1_desc'),
|
|
99
|
+
},
|
|
100
|
+
options: { popper: { placement: 'right' as const } },
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
attachTo: { element: '#onboarding-search-button' },
|
|
104
|
+
content: {
|
|
105
|
+
title: t('motor-admin.onboarding.search.step2_title'),
|
|
106
|
+
description: t('motor-admin.onboarding.search.step2_desc'),
|
|
107
|
+
},
|
|
108
|
+
options: { popper: { placement: 'right' as const } },
|
|
109
|
+
},
|
|
110
|
+
])
|
|
111
|
+
|
|
112
|
+
// ── Admin navigation steps ────────────────────────────────────────────────────
|
|
113
|
+
const adminNavSteps = computed(() => [
|
|
114
|
+
{
|
|
115
|
+
attachTo: { element: '#onboarding-sidebar-nav' },
|
|
116
|
+
content: {
|
|
117
|
+
title: t('motor-admin.onboarding.admin_nav.step1_title'),
|
|
118
|
+
description: t('motor-admin.onboarding.admin_nav.step1_desc'),
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
])
|
|
122
|
+
|
|
123
|
+
// ── Event handlers ────────────────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Announcements tour finished → chain to notifications.
|
|
127
|
+
*/
|
|
128
|
+
async function onAnnouncementsFinish() {
|
|
129
|
+
markAnnouncementsDone()
|
|
130
|
+
await nextTick()
|
|
131
|
+
if (!notificationsCompleted.value) {
|
|
132
|
+
startNotifications()
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Notifications tour finished → chain to search tour.
|
|
138
|
+
*/
|
|
139
|
+
async function onNotificationsFinish() {
|
|
140
|
+
markNotificationsDone()
|
|
141
|
+
await nextTick()
|
|
142
|
+
if (!searchCompleted.value) {
|
|
143
|
+
startSearch()
|
|
144
|
+
}
|
|
145
|
+
else if (!adminNavCompleted.value) {
|
|
146
|
+
startAdminNav()
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Search tour finished → chain to admin-nav tour.
|
|
152
|
+
*/
|
|
153
|
+
async function onSearchFinish() {
|
|
154
|
+
markSearchDone()
|
|
155
|
+
await nextTick()
|
|
156
|
+
if (!adminNavCompleted.value) {
|
|
157
|
+
startAdminNav()
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Admin-nav tour finished → mark done, store pending flag, navigate to users list.
|
|
163
|
+
* UsersOnboarding on the users page detects the pending flag and auto-starts
|
|
164
|
+
* the grid explanation tour.
|
|
165
|
+
*/
|
|
166
|
+
function onAdminNavFinish() {
|
|
167
|
+
markAdminNavDone()
|
|
168
|
+
setPending('admin-grid')
|
|
169
|
+
router.push('/motor-admin/users')
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ── Start logic ───────────────────────────────────────────────────────────────
|
|
173
|
+
// Announcements is the ONLY entry point. Each tour chains to the next via @finish.
|
|
174
|
+
// VOnboardingWrapper is registered as client-only, so wrapper refs are null
|
|
175
|
+
// until after hydration.
|
|
176
|
+
const stopWatch = watch(
|
|
177
|
+
[announcementsWrapper, notificationsWrapper, searchWrapper, adminNavWrapper],
|
|
178
|
+
async ([announcementsW, notificationsW, searchW, adminNavW]) => {
|
|
179
|
+
if (!announcementsW || !notificationsW || !searchW || !adminNavW) return
|
|
180
|
+
stopWatch()
|
|
181
|
+
|
|
182
|
+
// If the backend flag is set, clear localStorage so the tour runs again
|
|
183
|
+
if (user.value?.data?.show_onboarding) {
|
|
184
|
+
resetOnboardingState()
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Allow initial toasts to clear before starting
|
|
188
|
+
await new Promise(resolve => setTimeout(resolve, 2500))
|
|
189
|
+
|
|
190
|
+
if (!announcementsCompleted.value) {
|
|
191
|
+
startAnnouncements()
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
{ immediate: true },
|
|
195
|
+
)
|
|
196
|
+
</script>
|
|
197
|
+
|
|
198
|
+
<template>
|
|
199
|
+
<VOnboardingWrapper
|
|
200
|
+
ref="announcementsWrapper"
|
|
201
|
+
:steps="announcementSteps"
|
|
202
|
+
:options="options"
|
|
203
|
+
@finish="onAnnouncementsFinish"
|
|
204
|
+
/>
|
|
205
|
+
<VOnboardingWrapper
|
|
206
|
+
ref="notificationsWrapper"
|
|
207
|
+
:steps="notificationsSteps"
|
|
208
|
+
:options="options"
|
|
209
|
+
@finish="onNotificationsFinish"
|
|
210
|
+
/>
|
|
211
|
+
<VOnboardingWrapper
|
|
212
|
+
ref="searchWrapper"
|
|
213
|
+
:steps="searchSteps"
|
|
214
|
+
:options="options"
|
|
215
|
+
@finish="onSearchFinish"
|
|
216
|
+
/>
|
|
217
|
+
<VOnboardingWrapper
|
|
218
|
+
ref="adminNavWrapper"
|
|
219
|
+
:steps="adminNavSteps"
|
|
220
|
+
:options="options"
|
|
221
|
+
@finish="onAdminNavFinish"
|
|
222
|
+
/>
|
|
223
|
+
</template>
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { User } from '@motor-cms/ui-core/app/types/auth'
|
|
2
|
+
|
|
3
|
+
// Extend this union as new areas are added
|
|
4
|
+
export type OnboardingArea =
|
|
5
|
+
| 'dashboard-announcements'
|
|
6
|
+
| 'notifications'
|
|
7
|
+
| 'search'
|
|
8
|
+
| 'admin-nav'
|
|
9
|
+
| 'admin-grid'
|
|
10
|
+
| 'user-profile'
|
|
11
|
+
| 'builder-pages'
|
|
12
|
+
|
|
13
|
+
const STORAGE_KEY = 'motor-onboarding-completed'
|
|
14
|
+
const PENDING_KEY = 'motor-onboarding-pending'
|
|
15
|
+
|
|
16
|
+
function getCompleted(): Set<string> {
|
|
17
|
+
try {
|
|
18
|
+
const raw = localStorage.getItem(STORAGE_KEY)
|
|
19
|
+
return new Set(raw ? JSON.parse(raw) : [])
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return new Set()
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function persistCompleted(ids: Set<string>) {
|
|
27
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify([...ids]))
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function useOnboardingState(area: OnboardingArea) {
|
|
31
|
+
const { user } = useSanctumAuth<User>()
|
|
32
|
+
const userId = computed(() => user.value?.data?.id?.toString() ?? 'anonymous')
|
|
33
|
+
const key = computed(() => `${userId.value}:${area}`)
|
|
34
|
+
|
|
35
|
+
const isCompleted = computed(() => getCompleted().has(key.value))
|
|
36
|
+
|
|
37
|
+
function markCompleted() {
|
|
38
|
+
const completed = getCompleted()
|
|
39
|
+
completed.add(key.value)
|
|
40
|
+
persistCompleted(completed)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function reset() {
|
|
44
|
+
const completed = getCompleted()
|
|
45
|
+
completed.delete(key.value)
|
|
46
|
+
persistCompleted(completed)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return { isCompleted, markCompleted, reset }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Clears all completed onboarding areas for the current user from localStorage.
|
|
54
|
+
* Call this before navigating to dashboard to restart the full tour.
|
|
55
|
+
*/
|
|
56
|
+
export function useOnboardingResetAll() {
|
|
57
|
+
const { user } = useSanctumAuth<User>()
|
|
58
|
+
const userId = computed(() => user.value?.data?.id?.toString() ?? 'anonymous')
|
|
59
|
+
|
|
60
|
+
function resetAll() {
|
|
61
|
+
const completed = getCompleted()
|
|
62
|
+
const areas: OnboardingArea[] = [
|
|
63
|
+
'dashboard-announcements',
|
|
64
|
+
'notifications',
|
|
65
|
+
'search',
|
|
66
|
+
'admin-nav',
|
|
67
|
+
'admin-grid',
|
|
68
|
+
'user-profile',
|
|
69
|
+
'builder-pages',
|
|
70
|
+
]
|
|
71
|
+
for (const area of areas) {
|
|
72
|
+
completed.delete(`${userId.value}:${area}`)
|
|
73
|
+
}
|
|
74
|
+
persistCompleted(completed)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return { resetAll }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Cross-page trigger: marks which onboarding area should auto-start
|
|
82
|
+
* on the next page load. Scoped by user ID to prevent cross-user leakage.
|
|
83
|
+
*/
|
|
84
|
+
export function usePendingOnboarding() {
|
|
85
|
+
const { user } = useSanctumAuth<User>()
|
|
86
|
+
const userId = computed(() => user.value?.data?.id?.toString() ?? 'anonymous')
|
|
87
|
+
|
|
88
|
+
function setPending(area: OnboardingArea) {
|
|
89
|
+
localStorage.setItem(`${PENDING_KEY}:${userId.value}`, area)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function getPending(): OnboardingArea | null {
|
|
93
|
+
return (localStorage.getItem(`${PENDING_KEY}:${userId.value}`) as OnboardingArea) || null
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function clearPending() {
|
|
97
|
+
localStorage.removeItem(`${PENDING_KEY}:${userId.value}`)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return { setPending, getPending, clearPending }
|
|
101
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"next": "Weiter",
|
|
3
|
+
"previous": "Zurück",
|
|
4
|
+
"finish": "Fertig",
|
|
5
|
+
"announcements": {
|
|
6
|
+
"step1_title": "Meldungen",
|
|
7
|
+
"step1_desc": "Dieses Panel zeigt wichtige Meldungen von Ihrem Team oder Systemadministratoren.",
|
|
8
|
+
"step2_title": "Meldungen lesen",
|
|
9
|
+
"step2_desc": "Jede Karte zeigt eine Nachricht mit ihrem Typ (Info, Warnung oder Fehler). Klicken Sie auf ×, um sie nach dem Lesen auszublenden.",
|
|
10
|
+
"step3_title": "Meldung erstellen",
|
|
11
|
+
"step3_desc": "Als Administrator können Sie Meldungen an sich selbst, bestimmte Nutzer oder alle im Mandanten senden."
|
|
12
|
+
},
|
|
13
|
+
"notifications": {
|
|
14
|
+
"step1_title": "Benachrichtigungscenter",
|
|
15
|
+
"step1_desc": "Der rote Zähler zeigt ungelesene Benachrichtigungen. Klicken Sie auf die Glocke, um das Benachrichtigungscenter zu öffnen — hier sehen Sie alle Meldungen mit Typ und Zeitstempel, können einzelne Einträge entfernen oder alle auf einmal löschen."
|
|
16
|
+
},
|
|
17
|
+
"search": {
|
|
18
|
+
"step1_title": "Globale Suche",
|
|
19
|
+
"step1_desc": "Mit der Suche finden Sie sofort Seiten, Benutzer, Dateien und weitere Inhalte im gesamten System. Einfach tippen – Ergebnisse erscheinen direkt während der Eingabe.",
|
|
20
|
+
"step2_title": "Tastaturkürzel",
|
|
21
|
+
"step2_desc": "Drücken Sie ⌘K (oder Strg+K unter Windows) von überall im Admin, um die Suche ohne Maus zu öffnen. Halten Sie ⌘ gedrückt, um alle verfügbaren Tastenkürzel anzuzeigen."
|
|
22
|
+
},
|
|
23
|
+
"admin_nav": {
|
|
24
|
+
"step1_title": "Navigationsbereich",
|
|
25
|
+
"step1_desc": "Über die Seitenleiste erreichen Sie alle Verwaltungsbereiche. Klicken Sie auf 'Administration', um Benutzer, Rollen und weitere Einstellungen zu verwalten. Als nächstes öffnen wir die Benutzerliste."
|
|
26
|
+
},
|
|
27
|
+
"admin_grid": {
|
|
28
|
+
"step1_title": "Datensätze bearbeiten",
|
|
29
|
+
"step1_desc": "In dieser Übersicht sehen Sie alle Benutzer. Klicken Sie einfach auf eine Zeile — der Datensatz öffnet sich direkt zum Bearbeiten."
|
|
30
|
+
},
|
|
31
|
+
"builder_pages_list": {
|
|
32
|
+
"step1_title": "Seite öffnen um die Tour zu starten",
|
|
33
|
+
"step1_desc": "Klicken Sie auf eine beliebige Zeile, um die Seite im Editor zu öffnen. Die Builder-Tour startet dann automatisch."
|
|
34
|
+
},
|
|
35
|
+
"builder_pages": {
|
|
36
|
+
"step1_title": "Seiteneditor",
|
|
37
|
+
"step1_desc": "Willkommen im Seiteneditor. Hier können Sie Inhalt und Layout dieser Seite visuell bearbeiten und gestalten.",
|
|
38
|
+
"step2_title": "Veröffentlichungsstatus",
|
|
39
|
+
"step2_desc": "Dieses Abzeichen zeigt, ob die Seite veröffentlicht, geplant oder ein Entwurf ist. Klicken Sie darauf, um die Seite zu veröffentlichen, zu planen oder offline zu nehmen.",
|
|
40
|
+
"step3_title": "Änderungsverlauf",
|
|
41
|
+
"step3_desc": "Machen Sie Änderungen mit den Pfeilschaltflächen rückgängig oder wiederholen Sie sie. Das Uhrsymbol öffnet den vollständigen Verlauf dieser Bearbeitungssitzung.",
|
|
42
|
+
"step4_title": "Geräteansicht",
|
|
43
|
+
"step4_desc": "Wechseln Sie zwischen Desktop-, Tablet- und Mobilansicht, um zu sehen, wie Ihre Seite auf verschiedenen Bildschirmgrößen aussieht.",
|
|
44
|
+
"step5_title": "Seitenstruktur",
|
|
45
|
+
"step5_desc": "Das Strukturpanel zeigt alle Zeilen und Komponenten Ihrer Seite als Baumansicht. Klicken Sie auf einen Eintrag, um ihn im Editor auszuwählen.",
|
|
46
|
+
"step6_title": "Layout-Zeilen",
|
|
47
|
+
"step6_desc": "Fügen Sie über das Layout-Panel einen neuen Bereich mit 1, 2, 3 oder 4 Spalten ein. Klicken Sie auf ein Layout oder ziehen Sie es auf den Editor.",
|
|
48
|
+
"step7_title": "Inhaltskomponenten",
|
|
49
|
+
"step7_desc": "Das Komponentenpanel listet alle verfügbaren Inhaltsbausteine auf — Überschriften, Bilder, Videos, Buttons und mehr. Klicken oder ziehen Sie eine Komponente in eine Spalte.",
|
|
50
|
+
"step8_title": "Der Editor",
|
|
51
|
+
"step8_desc": "Klicken Sie auf ein Element, um es auszuwählen und seine Eigenschaften rechts anzuzeigen. Ziehen Sie den Griff links einer Zeile, um Bereiche umzusortieren.",
|
|
52
|
+
"step9_title": "Element-Eigenschaften",
|
|
53
|
+
"step9_desc": "Wenn ein Element ausgewählt ist, erscheinen seine Eigenschaften hier. Bearbeiten Sie Inhalt, Darstellung und erweiterte Einstellungen. Der Navigationspfad oben führt zu übergeordneten Elementen.",
|
|
54
|
+
"step10_title": "Speichern",
|
|
55
|
+
"step10_desc": "Klicken Sie auf Speichern (oder drücken Sie Cmd+S). Der Dropdown-Pfeil ermöglicht Speichern und Schließen in einem Schritt. Ein pulsierender Punkt zeigt ungespeicherte Änderungen an."
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"next": "Next",
|
|
3
|
+
"previous": "Previous",
|
|
4
|
+
"finish": "Done",
|
|
5
|
+
"announcements": {
|
|
6
|
+
"step1_title": "Announcements",
|
|
7
|
+
"step1_desc": "This panel shows important announcements from your team or system administrators.",
|
|
8
|
+
"step2_title": "Reading announcements",
|
|
9
|
+
"step2_desc": "Each card shows a message with its type (info, warning, or error). Click the × to dismiss it once you have read it.",
|
|
10
|
+
"step3_title": "Create an announcement",
|
|
11
|
+
"step3_desc": "As an admin you can post announcements to yourself, specific users, or everyone in your client."
|
|
12
|
+
},
|
|
13
|
+
"notifications": {
|
|
14
|
+
"step1_title": "Notification center",
|
|
15
|
+
"step1_desc": "The red counter shows unread notifications. Click the bell to open the notification center — here you can see all messages with type and timestamp, remove individual entries, or clear all at once."
|
|
16
|
+
},
|
|
17
|
+
"search": {
|
|
18
|
+
"step1_title": "Global search",
|
|
19
|
+
"step1_desc": "Use the search to instantly find pages, users, files, and other content across the entire system. Just start typing and results appear as you go.",
|
|
20
|
+
"step2_title": "Keyboard shortcut",
|
|
21
|
+
"step2_desc": "Press ⌘K (or Ctrl+K on Windows) from anywhere in the admin to open search without reaching for the mouse. Hold ⌘ to see all available shortcuts."
|
|
22
|
+
},
|
|
23
|
+
"admin_nav": {
|
|
24
|
+
"step1_title": "Navigation",
|
|
25
|
+
"step1_desc": "Use the sidebar to access all administration areas. Click on 'Administration' to manage users, roles, and other settings. Next, we will open the users list."
|
|
26
|
+
},
|
|
27
|
+
"admin_grid": {
|
|
28
|
+
"step1_title": "Editing records",
|
|
29
|
+
"step1_desc": "This overview shows all users. Simply click on any row — the record opens directly for editing."
|
|
30
|
+
},
|
|
31
|
+
"builder_pages_list": {
|
|
32
|
+
"step1_title": "Open a page to start the tour",
|
|
33
|
+
"step1_desc": "Click on any row to open the page in the builder editor. The builder tour will start automatically."
|
|
34
|
+
},
|
|
35
|
+
"builder_pages": {
|
|
36
|
+
"step1_title": "Page Builder",
|
|
37
|
+
"step1_desc": "Welcome to the page builder. Here you can visually build and edit the content and layout of this page.",
|
|
38
|
+
"step2_title": "Publishing status",
|
|
39
|
+
"step2_desc": "This badge shows whether the page is published, scheduled, or a draft. Click it to publish, schedule, or take the page offline.",
|
|
40
|
+
"step3_title": "Edit history",
|
|
41
|
+
"step3_desc": "Undo or redo your last changes with the arrow buttons. The clock icon opens the full change history for this editing session.",
|
|
42
|
+
"step4_title": "Device preview",
|
|
43
|
+
"step4_desc": "Switch between desktop, tablet, and mobile views to see how your page looks on different screen sizes.",
|
|
44
|
+
"step5_title": "Page outline",
|
|
45
|
+
"step5_desc": "The outline panel shows all rows and components on your page as a tree. Click any item to select it on the canvas.",
|
|
46
|
+
"step6_title": "Layout rows",
|
|
47
|
+
"step6_desc": "Use the layout panel to add a new section with 1, 2, 3, or 4 columns. Click a layout to insert it, or drag it onto the canvas.",
|
|
48
|
+
"step7_title": "Content components",
|
|
49
|
+
"step7_desc": "The components panel lists all available content blocks — headings, images, videos, buttons, and more. Click or drag a component to add it inside a column.",
|
|
50
|
+
"step8_title": "The canvas",
|
|
51
|
+
"step8_desc": "Click any element on the canvas to select it and reveal its properties on the right. Drag the handle on the left of a row to reorder sections.",
|
|
52
|
+
"step9_title": "Element properties",
|
|
53
|
+
"step9_desc": "When an element is selected, its properties appear here. Edit content, change styling, and configure advanced settings. The breadcrumb at the top lets you navigate to parent elements.",
|
|
54
|
+
"step10_title": "Save your work",
|
|
55
|
+
"step10_desc": "Click Save (or press Cmd+S) to save your changes. The dropdown arrow lets you save and close in one step. A pulsing dot indicates unsaved changes."
|
|
56
|
+
}
|
|
57
|
+
}
|
package/app/pages/index.vue
CHANGED
|
@@ -25,6 +25,7 @@ const {
|
|
|
25
25
|
} = useDashboardData()
|
|
26
26
|
|
|
27
27
|
const announcementModalOpen = ref(false)
|
|
28
|
+
const createPageModalOpen = ref(false)
|
|
28
29
|
|
|
29
30
|
const currentDate = computed(() => {
|
|
30
31
|
return new Date().toLocaleDateString(locale.value, {
|
|
@@ -35,6 +36,10 @@ const currentDate = computed(() => {
|
|
|
35
36
|
})
|
|
36
37
|
})
|
|
37
38
|
|
|
39
|
+
function onPageCreated(page: { id: number }) {
|
|
40
|
+
navigateTo(`/motor-builder/builder-pages/${page.id}/edit`)
|
|
41
|
+
}
|
|
42
|
+
|
|
38
43
|
function onAnnouncementCreated() {
|
|
39
44
|
refresh()
|
|
40
45
|
toast.add({ title: t('motor-admin.dashboard.announcement_created'), icon: 'i-lucide-megaphone', color: 'success' })
|
|
@@ -50,6 +55,11 @@ async function onDismiss(id: number) {
|
|
|
50
55
|
|
|
51
56
|
<template>
|
|
52
57
|
<div class="p-6">
|
|
58
|
+
<div class="flex items-center gap-2 mb-3">
|
|
59
|
+
<UDashboardSidebarToggle class="lg:hidden shrink-0 -ml-2" />
|
|
60
|
+
<SidebarToggleButton />
|
|
61
|
+
<UBreadcrumb :items="[{ label: t('motor-core.global.dashboard'), icon: 'i-lucide-home' }]" />
|
|
62
|
+
</div>
|
|
53
63
|
<div class="flex flex-col mb-6">
|
|
54
64
|
<div>
|
|
55
65
|
<h1 class="text-xl font-heading font-semibold text-highlighted">
|
|
@@ -65,7 +75,7 @@ async function onDismiss(id: number) {
|
|
|
65
75
|
color="primary"
|
|
66
76
|
size="sm"
|
|
67
77
|
:label="t('motor-admin.dashboard.quick_actions.new_page')"
|
|
68
|
-
@click="
|
|
78
|
+
@click="createPageModalOpen = true"
|
|
69
79
|
/>
|
|
70
80
|
<UButton
|
|
71
81
|
icon="i-lucide-upload"
|
|
@@ -113,5 +123,14 @@ async function onDismiss(id: number) {
|
|
|
113
123
|
v-model:open="announcementModalOpen"
|
|
114
124
|
@created="onAnnouncementCreated"
|
|
115
125
|
/>
|
|
126
|
+
|
|
127
|
+
<DashboardOnboarding />
|
|
128
|
+
|
|
129
|
+
<BuilderPageSettingsModal
|
|
130
|
+
v-model:open="createPageModalOpen"
|
|
131
|
+
:page-data="null"
|
|
132
|
+
:is-create-mode="true"
|
|
133
|
+
@created="onPageCreated"
|
|
134
|
+
/>
|
|
116
135
|
</div>
|
|
117
136
|
</template>
|
package/app/pages/profile.vue
CHANGED
|
@@ -9,8 +9,10 @@ definePageMeta({
|
|
|
9
9
|
})
|
|
10
10
|
|
|
11
11
|
const { t } = useI18n()
|
|
12
|
+
const router = useRouter()
|
|
12
13
|
const { user, refreshIdentity } = useSanctumAuth<User>()
|
|
13
|
-
const { updateProfile } = useProfileApi()
|
|
14
|
+
const { updateProfile, resetOnboarding } = useProfileApi()
|
|
15
|
+
const { resetAll: resetOnboardingState } = useOnboardingResetAll()
|
|
14
16
|
const { success, error: notifyError, info } = useNotify()
|
|
15
17
|
|
|
16
18
|
// Test function to demonstrate error notifications
|
|
@@ -172,6 +174,33 @@ const passwordState = reactive({
|
|
|
172
174
|
|
|
173
175
|
const passwordLoading = ref(false)
|
|
174
176
|
|
|
177
|
+
// ============================================
|
|
178
|
+
// Onboarding Tour
|
|
179
|
+
// ============================================
|
|
180
|
+
|
|
181
|
+
const onboardingLoading = ref(false)
|
|
182
|
+
|
|
183
|
+
async function onRestartTour() {
|
|
184
|
+
onboardingLoading.value = true
|
|
185
|
+
try {
|
|
186
|
+
await resetOnboarding()
|
|
187
|
+
resetOnboardingState()
|
|
188
|
+
success(t('motor-core.profile.toast_tour_reset_title'), t('motor-core.profile.toast_tour_reset_message'))
|
|
189
|
+
await router.push('/')
|
|
190
|
+
}
|
|
191
|
+
catch (err: unknown) {
|
|
192
|
+
const message = err instanceof Error ? err.message : t('motor-core.profile.toast_tour_reset_error')
|
|
193
|
+
notifyError(t('motor-core.profile.toast_tour_reset_error'), message, {
|
|
194
|
+
message,
|
|
195
|
+
stack: err instanceof Error ? err.stack : undefined,
|
|
196
|
+
url: '/api/profile/reset-onboarding',
|
|
197
|
+
})
|
|
198
|
+
}
|
|
199
|
+
finally {
|
|
200
|
+
onboardingLoading.value = false
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
175
204
|
async function onPasswordSubmit(event: FormSubmitEvent<PasswordSchema>) {
|
|
176
205
|
passwordLoading.value = true
|
|
177
206
|
|
|
@@ -293,6 +322,22 @@ async function onPasswordSubmit(event: FormSubmitEvent<PasswordSchema>) {
|
|
|
293
322
|
</UForm>
|
|
294
323
|
</UPageCard>
|
|
295
324
|
|
|
325
|
+
<!-- Onboarding Tour Card -->
|
|
326
|
+
<UPageCard
|
|
327
|
+
:title="t('motor-core.profile.onboarding_title')"
|
|
328
|
+
:description="t('motor-core.profile.onboarding_description')"
|
|
329
|
+
>
|
|
330
|
+
<div class="flex justify-end">
|
|
331
|
+
<UButton
|
|
332
|
+
:loading="onboardingLoading"
|
|
333
|
+
icon="i-lucide-graduation-cap"
|
|
334
|
+
@click="onRestartTour"
|
|
335
|
+
>
|
|
336
|
+
{{ t('motor-core.profile.restart_tour') }}
|
|
337
|
+
</UButton>
|
|
338
|
+
</div>
|
|
339
|
+
</UPageCard>
|
|
340
|
+
|
|
296
341
|
<!-- Change Password Card -->
|
|
297
342
|
<UPageCard
|
|
298
343
|
:title="t('motor-core.profile.change_password_title')"
|
package/nuxt.config.ts
CHANGED
|
@@ -1 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
import { fileURLToPath } from 'node:url'
|
|
2
|
+
import { dirname, resolve } from 'node:path'
|
|
3
|
+
|
|
4
|
+
const __layerDir = dirname(fileURLToPath(import.meta.url))
|
|
5
|
+
|
|
6
|
+
export default defineNuxtConfig({
|
|
7
|
+
css: [resolve(__layerDir, 'app/assets/css/v-onboarding.css')],
|
|
8
|
+
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@motor-cms/ui-admin",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./nuxt.config.ts",
|
|
6
6
|
"files": [
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"@vueuse/core": "^14.0.0",
|
|
18
18
|
"sortablejs": "^1.15.0",
|
|
19
19
|
"zod": "^4.0.0",
|
|
20
|
-
"@motor-cms/ui-core": "1.
|
|
20
|
+
"@motor-cms/ui-core": "1.8.1"
|
|
21
21
|
},
|
|
22
22
|
"peerDependencies": {
|
|
23
23
|
"nuxt": "^4.0.0",
|