@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.
- package/app/assets/css/v-onboarding.css +64 -0
- package/app/components/OnboardingStep.vue +42 -0
- package/app/components/UsersOnboarding.vue +84 -0
- package/app/components/client/FooterSlotCard.vue +313 -0
- package/app/components/client/GlobalComponentsSection.vue +65 -0
- package/app/components/dashboard/DashboardActivity.vue +71 -0
- package/app/components/dashboard/DashboardActivityItem.vue +96 -0
- package/app/components/dashboard/DashboardAnnouncementModal.vue +327 -0
- package/app/components/dashboard/DashboardAnnouncements.vue +93 -0
- package/app/components/dashboard/DashboardOnboarding.vue +285 -0
- package/app/components/dashboard/DashboardPublishingQueue.vue +47 -0
- package/app/components/dashboard/DashboardQuickActions.vue +44 -0
- package/app/components/dashboard/DashboardStats.vue +63 -0
- package/app/components/form/inputs/CategoryTreePicker.vue +265 -109
- package/app/components/form/inputs/EntityConfigurationsPanel.vue +235 -0
- package/app/composables/useClientFormExtensions.ts +89 -0
- package/app/composables/useClientLanguages.ts +81 -0
- package/app/composables/useDashboardData.ts +169 -0
- package/app/composables/useOnboardingState.ts +151 -0
- package/app/data/footerTemplate.ts +283 -0
- package/app/lang/de/motor-admin/ai_system_prompts.json +1 -0
- package/app/lang/de/motor-admin/categories.json +1 -0
- package/app/lang/de/motor-admin/category_trees.json +2 -1
- package/app/lang/de/motor-admin/clients.json +17 -1
- package/app/lang/de/motor-admin/config_variables.json +1 -0
- package/app/lang/de/motor-admin/dashboard.json +83 -0
- package/app/lang/de/motor-admin/domains.json +6 -1
- package/app/lang/de/motor-admin/email_templates.json +1 -0
- package/app/lang/de/motor-admin/entity_configurations.json +12 -0
- package/app/lang/de/motor-admin/languages.json +1 -0
- package/app/lang/de/motor-admin/onboarding.json +60 -0
- package/app/lang/de/motor-admin/permissions.json +1 -0
- package/app/lang/de/motor-admin/roles.json +1 -0
- package/app/lang/de/motor-admin/users.json +1 -0
- package/app/lang/en/motor-admin/ai_system_prompts.json +1 -0
- package/app/lang/en/motor-admin/categories.json +1 -0
- package/app/lang/en/motor-admin/category_trees.json +2 -1
- package/app/lang/en/motor-admin/clients.json +17 -1
- package/app/lang/en/motor-admin/config_variables.json +1 -0
- package/app/lang/en/motor-admin/dashboard.json +83 -0
- package/app/lang/en/motor-admin/domains.json +6 -1
- package/app/lang/en/motor-admin/email_templates.json +1 -0
- package/app/lang/en/motor-admin/entity_configurations.json +12 -0
- package/app/lang/en/motor-admin/languages.json +1 -0
- package/app/lang/en/motor-admin/onboarding.json +60 -0
- package/app/lang/en/motor-admin/permissions.json +1 -0
- package/app/lang/en/motor-admin/roles.json +1 -0
- package/app/lang/en/motor-admin/users.json +1 -0
- package/app/pages/index.vue +119 -22
- package/app/pages/login.vue +6 -0
- package/app/pages/motor-admin/ai-system-prompts/[id]/edit.vue +4 -4
- package/app/pages/motor-admin/category-trees/[id]/categories/[categoryId]/edit.vue +4 -3
- package/app/pages/motor-admin/category-trees/[id]/edit.vue +4 -4
- package/app/pages/motor-admin/clients/[id]/edit.vue +146 -6
- package/app/pages/motor-admin/clients/create.vue +34 -2
- package/app/pages/motor-admin/config-variables/[id]/edit.vue +4 -4
- package/app/pages/motor-admin/domains/[id]/edit.vue +18 -5
- package/app/pages/motor-admin/email-templates/[id]/edit.vue +4 -4
- package/app/pages/motor-admin/email-templates/index.vue +36 -25
- package/app/pages/motor-admin/languages/[id]/edit.vue +17 -4
- package/app/pages/motor-admin/languages/create.vue +13 -0
- package/app/pages/motor-admin/permission-groups/[id]/edit.vue +4 -4
- package/app/pages/motor-admin/roles/[id]/edit.vue +4 -4
- package/app/pages/motor-admin/roles/create.vue +4 -1
- package/app/pages/motor-admin/users/[id]/edit.vue +4 -3
- package/app/pages/motor-admin/users/index.vue +1 -0
- package/app/pages/profile.vue +47 -1
- package/app/pages/search.vue +13 -3
- package/app/types/generated/form-meta.ts +24 -20
- package/app/types/generated/grid-meta.ts +5 -3
- package/nuxt.config.ts +15 -1
- package/package.json +6 -2
- 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>
|