i18n-dashboard 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.
- package/LICENSE +21 -0
- package/README.md +715 -0
- package/app.vue +8 -0
- package/assets/css/main.css +21 -0
- package/assets/locales/en.json +380 -0
- package/bin/cli.mjs +279 -0
- package/components/LinkedKeyPicker.vue +135 -0
- package/components/PathPicker.vue +153 -0
- package/components/PluralEditor.vue +295 -0
- package/components/ScanModal.vue +153 -0
- package/components/TranslationHistoryModal.vue +66 -0
- package/components/TranslationRow.vue +541 -0
- package/components/dashboard/WidgetConfigModal.vue +121 -0
- package/components/dashboard/WidgetGrid.vue +190 -0
- package/components/dashboard/WidgetPicker.vue +75 -0
- package/components/dashboard/widgets/ActivityWidget.vue +109 -0
- package/components/dashboard/widgets/LanguagesCoverageWidget.vue +104 -0
- package/components/dashboard/widgets/ProjectsWidget.vue +77 -0
- package/components/dashboard/widgets/ReviewWidget.vue +150 -0
- package/components/dashboard/widgets/StatWidget.vue +133 -0
- package/composables/useAuth.ts +72 -0
- package/composables/useConfig.ts +14 -0
- package/composables/useDashboard.ts +89 -0
- package/composables/useFormats.ts +100 -0
- package/composables/useKeys.ts +231 -0
- package/composables/useLanguages.ts +221 -0
- package/composables/useProfile.ts +76 -0
- package/composables/useProject.ts +180 -0
- package/composables/useReview.ts +94 -0
- package/composables/useSettings.ts +30 -0
- package/composables/useStats.ts +16 -0
- package/composables/useT.ts +38 -0
- package/composables/useUsers.ts +101 -0
- package/composables/useWidgetData.ts +50 -0
- package/consts/commons.const.ts +6 -0
- package/consts/dashboard.const.ts +94 -0
- package/consts/languages.const.ts +223 -0
- package/enums/commons.enum.ts +7 -0
- package/i18n-dashboard.config.example.js +40 -0
- package/interfaces/commons.interface.ts +23 -0
- package/interfaces/job.interface.ts +10 -0
- package/interfaces/key.interface.ts +39 -0
- package/interfaces/languages.interface.ts +23 -0
- package/interfaces/project.interface.ts +9 -0
- package/interfaces/scan.interface.ts +12 -0
- package/interfaces/settings.interface.ts +4 -0
- package/interfaces/stat.interface.ts +30 -0
- package/interfaces/translation.interface.ts +11 -0
- package/interfaces/user.interface.ts +24 -0
- package/layouts/auth.vue +5 -0
- package/layouts/default.vue +327 -0
- package/middleware/auth.global.ts +26 -0
- package/nuxt.config.ts +66 -0
- package/package.json +89 -0
- package/pages/index.vue +5 -0
- package/pages/login.vue +74 -0
- package/pages/onboarding.vue +563 -0
- package/pages/projects/[id]/formats/datetime.vue +240 -0
- package/pages/projects/[id]/formats/modifiers.vue +194 -0
- package/pages/projects/[id]/formats/number.vue +250 -0
- package/pages/projects/[id]/index.vue +182 -0
- package/pages/projects/[id]/languages.vue +537 -0
- package/pages/projects/[id]/review.vue +109 -0
- package/pages/projects/[id]/settings.vue +515 -0
- package/pages/projects/[id]/translations/[keyId].vue +642 -0
- package/pages/projects/[id]/translations/index.vue +250 -0
- package/pages/projects/[id]/users.vue +276 -0
- package/pages/projects/index.vue +334 -0
- package/pages/users/[id]/profile.vue +421 -0
- package/pages/users/index.vue +345 -0
- package/plugins/loading.client.ts +3 -0
- package/plugins/ui-i18n.ts +6 -0
- package/server/api/auth/login.post.ts +28 -0
- package/server/api/auth/logout.post.ts +7 -0
- package/server/api/auth/me.get.ts +11 -0
- package/server/api/auth/me.put.ts +31 -0
- package/server/api/auth/password.put.ts +27 -0
- package/server/api/auth/status.get.ts +16 -0
- package/server/api/config.get.ts +10 -0
- package/server/api/dashboard/layout.get.ts +18 -0
- package/server/api/dashboard/layout.post.ts +18 -0
- package/server/api/db-config.get.ts +44 -0
- package/server/api/db-config.post.ts +73 -0
- package/server/api/export.get.ts +64 -0
- package/server/api/formats/datetime/[id].delete.ts +8 -0
- package/server/api/formats/datetime/[id].put.ts +15 -0
- package/server/api/formats/datetime.get.ts +11 -0
- package/server/api/formats/datetime.post.ts +16 -0
- package/server/api/formats/modifiers/[id].delete.ts +8 -0
- package/server/api/formats/modifiers/[id].put.ts +10 -0
- package/server/api/formats/modifiers.get.ts +10 -0
- package/server/api/formats/modifiers.post.ts +14 -0
- package/server/api/formats/number/[id].delete.ts +8 -0
- package/server/api/formats/number/[id].put.ts +15 -0
- package/server/api/formats/number.get.ts +11 -0
- package/server/api/formats/number.post.ts +16 -0
- package/server/api/formats/snippet.get.ts +87 -0
- package/server/api/fs/browse.get.ts +50 -0
- package/server/api/history/[translationId].get.ts +13 -0
- package/server/api/keys/[id].delete.ts +14 -0
- package/server/api/keys/[id].get.ts +41 -0
- package/server/api/keys/[id].patch.ts +20 -0
- package/server/api/keys/index.get.ts +98 -0
- package/server/api/keys/index.post.ts +17 -0
- package/server/api/languages/[code].delete.ts +15 -0
- package/server/api/languages/[id].put.ts +24 -0
- package/server/api/languages/index.get.ts +13 -0
- package/server/api/languages/index.post.ts +42 -0
- package/server/api/onboarding.post.ts +56 -0
- package/server/api/profile.get.ts +81 -0
- package/server/api/project-snapshot.get.ts +73 -0
- package/server/api/project-snapshot.post.ts +160 -0
- package/server/api/projects/[id].delete.ts +13 -0
- package/server/api/projects/[id].put.ts +40 -0
- package/server/api/projects/index.get.ts +19 -0
- package/server/api/projects/index.post.ts +34 -0
- package/server/api/scan.post.ts +165 -0
- package/server/api/settings/index.get.ts +9 -0
- package/server/api/settings/index.post.ts +20 -0
- package/server/api/setup.post.ts +39 -0
- package/server/api/stats/global.get.ts +126 -0
- package/server/api/stats.get.ts +70 -0
- package/server/api/sync.post.ts +179 -0
- package/server/api/translate.post.ts +52 -0
- package/server/api/translations/batch-translate.post.ts +121 -0
- package/server/api/translations/bulk-status.post.ts +24 -0
- package/server/api/translations/index.post.ts +62 -0
- package/server/api/translations/job/[id].get.ts +23 -0
- package/server/api/translations/status.post.ts +30 -0
- package/server/api/translations/translate-all.post.ts +18 -0
- package/server/api/ui-locale.get.ts +39 -0
- package/server/api/users/[id]/profile.get.ts +107 -0
- package/server/api/users/[id]/roles.put.ts +67 -0
- package/server/api/users/[id].delete.ts +36 -0
- package/server/api/users/[id].put.ts +43 -0
- package/server/api/users/index.get.ts +49 -0
- package/server/api/users/index.post.ts +89 -0
- package/server/consts/auto-translate.const.ts +2 -0
- package/server/consts/commons.const.ts +10 -0
- package/server/consts/db.const.ts +3 -0
- package/server/consts/scanner.const.ts +4 -0
- package/server/consts/translation-job.const.ts +8 -0
- package/server/db/index.ts +672 -0
- package/server/enums/auth.enum.ts +5 -0
- package/server/enums/translation.enum.ts +6 -0
- package/server/interfaces/profile.interface.ts +48 -0
- package/server/interfaces/project-config.interface.ts +9 -0
- package/server/interfaces/scanner.interface.ts +18 -0
- package/server/interfaces/translation-job.interface.ts +13 -0
- package/server/middleware/auth.ts +32 -0
- package/server/plugins/db.ts +6 -0
- package/server/routes/locale/[lang].get.ts +179 -0
- package/server/types/auth.type.ts +3 -0
- package/server/utils/auth.util.ts +89 -0
- package/server/utils/auto-translate.util.ts +112 -0
- package/server/utils/lang-api.util.ts +24 -0
- package/server/utils/mailer.util.ts +80 -0
- package/server/utils/project-config.util.ts +37 -0
- package/server/utils/scanner.uti.ts +307 -0
- package/server/utils/translation-job.util.ts +142 -0
- package/services/auth.service.ts +31 -0
- package/services/base.service.ts +140 -0
- package/services/job.service.ts +10 -0
- package/services/key.service.ts +26 -0
- package/services/language.service.ts +26 -0
- package/services/profile.service.ts +14 -0
- package/services/project.service.ts +23 -0
- package/services/scan.service.ts +14 -0
- package/services/settings.service.ts +14 -0
- package/services/stats.service.ts +11 -0
- package/services/translation.service.ts +36 -0
- package/services/user.service.ts +28 -0
- package/tsconfig.json +3 -0
- package/types/commons.type.ts +3 -0
- package/types/dashboard.type.ts +26 -0
- package/utils/config.util.ts +60 -0
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-950 p-4">
|
|
3
|
+
<div class="w-full max-w-2xl">
|
|
4
|
+
<!-- Header -->
|
|
5
|
+
<div class="text-center mb-8">
|
|
6
|
+
<div class="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary-500 mb-4">
|
|
7
|
+
<UIcon name="i-heroicons-language" class="text-white text-3xl" />
|
|
8
|
+
</div>
|
|
9
|
+
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">i18n Dashboard</h1>
|
|
10
|
+
<p class="text-gray-500 dark:text-gray-400 mt-1">{{ t('onboarding.subtitle', 'Configurons votre espace de travail en quelques étapes') }}</p>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<!-- Steps indicator -->
|
|
14
|
+
<div class="flex items-center justify-center gap-2 mb-8">
|
|
15
|
+
<template v-for="(step, i) in steps" :key="i">
|
|
16
|
+
<div class="flex items-center gap-2">
|
|
17
|
+
<div
|
|
18
|
+
class="w-8 h-8 rounded-full flex items-center justify-center text-sm font-semibold transition-colors"
|
|
19
|
+
:class="i < currentStep
|
|
20
|
+
? 'bg-primary-500 text-white'
|
|
21
|
+
: i === currentStep
|
|
22
|
+
? 'bg-primary-500 text-white ring-4 ring-primary-100 dark:ring-primary-900/30'
|
|
23
|
+
: 'bg-gray-200 dark:bg-gray-700 text-gray-500 dark:text-gray-400'"
|
|
24
|
+
>
|
|
25
|
+
<UIcon v-if="i < currentStep" name="i-heroicons-check" class="text-sm" />
|
|
26
|
+
<span v-else>{{ i + 1 }}</span>
|
|
27
|
+
</div>
|
|
28
|
+
<span class="text-sm hidden sm:block" :class="i === currentStep ? 'text-gray-900 dark:text-white font-medium' : 'text-gray-400'">{{ step.label }}</span>
|
|
29
|
+
</div>
|
|
30
|
+
<div v-if="i < steps.length - 1" class="flex-1 h-0.5 bg-gray-200 dark:bg-gray-700 max-w-12" />
|
|
31
|
+
</template>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<UCard>
|
|
35
|
+
<!-- ── Step 0 : Database ──────────────────────────────────────────── -->
|
|
36
|
+
<div v-if="currentStep === 0" class="space-y-4">
|
|
37
|
+
<div>
|
|
38
|
+
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">{{ t('onboarding.db_title', 'Database') }}</h2>
|
|
39
|
+
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
|
40
|
+
{{ t('onboarding.db_subtitle', 'Configure your database connection. Values are pre-filled from your config file.') }}
|
|
41
|
+
<code class="text-xs bg-gray-100 dark:bg-gray-800 px-1 rounded">i18n-dashboard.config.js</code>
|
|
42
|
+
</p>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<!-- DB type selector -->
|
|
46
|
+
<UFormField :label="t('onboarding.db_type_label', 'Database type')">
|
|
47
|
+
<USelect v-model="dbForm.type" :items="dbTypeOptions" class="w-full" />
|
|
48
|
+
</UFormField>
|
|
49
|
+
|
|
50
|
+
<!-- SQLite fields -->
|
|
51
|
+
<template v-if="dbForm.type === 'sqlite'">
|
|
52
|
+
<UFormField :label="t('onboarding.db_file_label', 'Database file')">
|
|
53
|
+
<div class="flex gap-2">
|
|
54
|
+
<UInput v-model="dbForm.connection" placeholder="./i18n-dashboard.db" class="flex-1" />
|
|
55
|
+
<UButton
|
|
56
|
+
v-if="!sqliteFileExists"
|
|
57
|
+
color="neutral"
|
|
58
|
+
variant="outline"
|
|
59
|
+
icon="i-heroicons-document-plus"
|
|
60
|
+
:loading="creatingFile"
|
|
61
|
+
@click="createSqliteFile"
|
|
62
|
+
>
|
|
63
|
+
{{ t('onboarding.db_create_file', 'Create') }}
|
|
64
|
+
</UButton>
|
|
65
|
+
<UBadge v-else color="success" variant="soft" class="shrink-0 self-center">
|
|
66
|
+
<UIcon name="i-heroicons-check" class="mr-1" />
|
|
67
|
+
{{ t('onboarding.db_file_found', 'File found') }}
|
|
68
|
+
</UBadge>
|
|
69
|
+
</div>
|
|
70
|
+
<p v-if="!sqliteFileExists" class="text-xs text-amber-500 mt-1">
|
|
71
|
+
<UIcon name="i-heroicons-exclamation-triangle" class="inline mr-1" />
|
|
72
|
+
{{ t('onboarding.db_file_missing', 'The file does not exist yet. Click "Create" to create it.') }}
|
|
73
|
+
</p>
|
|
74
|
+
</UFormField>
|
|
75
|
+
</template>
|
|
76
|
+
|
|
77
|
+
<!-- PostgreSQL / MySQL fields -->
|
|
78
|
+
<template v-else>
|
|
79
|
+
<div class="grid grid-cols-2 gap-3">
|
|
80
|
+
<UFormField :label="t('onboarding.db_host', 'Host')" class="col-span-1">
|
|
81
|
+
<UInput v-model="dbForm.host" placeholder="localhost" class="w-full" />
|
|
82
|
+
</UFormField>
|
|
83
|
+
<UFormField :label="t('onboarding.db_port', 'Port')" class="col-span-1">
|
|
84
|
+
<UInput v-model="dbForm.port" :placeholder="dbForm.type === 'mysql' ? '3306' : '5432'" class="w-full" />
|
|
85
|
+
</UFormField>
|
|
86
|
+
</div>
|
|
87
|
+
<UFormField :label="t('onboarding.db_user', 'User')">
|
|
88
|
+
<UInput v-model="dbForm.user" placeholder="postgres" class="w-full" />
|
|
89
|
+
</UFormField>
|
|
90
|
+
<UFormField :label="t('onboarding.db_password', 'Password')">
|
|
91
|
+
<UInput v-model="dbForm.password" type="password" placeholder="••••••••" class="w-full" />
|
|
92
|
+
</UFormField>
|
|
93
|
+
<UFormField :label="t('onboarding.db_name', 'Database name')">
|
|
94
|
+
<UInput v-model="dbForm.database" placeholder="i18n_dashboard" class="w-full" />
|
|
95
|
+
</UFormField>
|
|
96
|
+
</template>
|
|
97
|
+
|
|
98
|
+
<!-- Actions -->
|
|
99
|
+
<div class="flex items-center gap-3 flex-wrap">
|
|
100
|
+
<UButton
|
|
101
|
+
color="neutral"
|
|
102
|
+
variant="outline"
|
|
103
|
+
icon="i-heroicons-signal"
|
|
104
|
+
:loading="testingDb"
|
|
105
|
+
@click="testDbConnection"
|
|
106
|
+
>
|
|
107
|
+
{{ t('onboarding.db_test', 'Test connection') }}
|
|
108
|
+
</UButton>
|
|
109
|
+
<UButton
|
|
110
|
+
v-if="dbFormChanged"
|
|
111
|
+
icon="i-heroicons-check"
|
|
112
|
+
:loading="applyingDb"
|
|
113
|
+
@click="applyDbConfig"
|
|
114
|
+
>
|
|
115
|
+
{{ t('onboarding.db_test_apply', 'Apply') }}
|
|
116
|
+
</UButton>
|
|
117
|
+
<UBadge v-if="dbConnected" color="success" variant="soft">
|
|
118
|
+
<UIcon name="i-heroicons-check-circle" class="mr-1" />
|
|
119
|
+
{{ t('onboarding.db_connected', 'Connection OK') }}
|
|
120
|
+
</UBadge>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<p v-if="dbError" class="text-sm text-red-500 bg-red-50 dark:bg-red-900/20 rounded-lg px-3 py-2">
|
|
124
|
+
<UIcon name="i-heroicons-exclamation-circle" class="inline mr-1" />
|
|
125
|
+
{{ dbError }}
|
|
126
|
+
</p>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
<!-- ── Step 1 : Admin account ─────────────────────────────────────── -->
|
|
130
|
+
<div v-if="currentStep === 1" class="space-y-4">
|
|
131
|
+
<!-- Fresh install: creation form -->
|
|
132
|
+
<template v-if="!hasUsers">
|
|
133
|
+
<div class="bg-primary-50 dark:bg-primary-900/20 rounded-lg p-4">
|
|
134
|
+
<p class="text-sm text-primary-700 dark:text-primary-300">
|
|
135
|
+
<UIcon name="i-heroicons-information-circle" class="inline mr-1" />
|
|
136
|
+
{{ t('onboarding.create_admin_hint', 'Create the') }} <strong>{{ t('onboarding.super_admin_role', 'Super Administrator') }}</strong> {{ t('onboarding.create_admin_hint2', 'account to get started.') }}
|
|
137
|
+
</p>
|
|
138
|
+
</div>
|
|
139
|
+
<form class="space-y-4" @submit.prevent="createAdmin">
|
|
140
|
+
<UFormField :label="t('users.full_name', 'Full name')" required>
|
|
141
|
+
<UInput v-model="adminForm.name" placeholder="Marie Dupont" class="w-full" autofocus />
|
|
142
|
+
</UFormField>
|
|
143
|
+
<UFormField :label="t('login.email', 'Email')" required>
|
|
144
|
+
<UInput v-model="adminForm.email" type="email" placeholder="admin@example.com" class="w-full" />
|
|
145
|
+
</UFormField>
|
|
146
|
+
<UFormField :label="t('login.password', 'Password')" :hint="t('user.password_hint', 'Minimum 8 characters')" required>
|
|
147
|
+
<UInput v-model="adminForm.password" type="password" placeholder="••••••••" class="w-full" />
|
|
148
|
+
</UFormField>
|
|
149
|
+
<UFormField :label="t('onboarding.confirm_password', 'Confirm password')" required>
|
|
150
|
+
<UInput v-model="adminForm.confirm" type="password" placeholder="••••••••" class="w-full" />
|
|
151
|
+
</UFormField>
|
|
152
|
+
<p v-if="adminError" class="text-sm text-red-500 bg-red-50 dark:bg-red-900/20 rounded-lg px-3 py-2">
|
|
153
|
+
<UIcon name="i-heroicons-exclamation-circle" class="inline mr-1" />
|
|
154
|
+
{{ adminError }}
|
|
155
|
+
</p>
|
|
156
|
+
</form>
|
|
157
|
+
</template>
|
|
158
|
+
<!-- Existing install: confirmation -->
|
|
159
|
+
<template v-else>
|
|
160
|
+
<div class="flex items-center gap-3 p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
|
|
161
|
+
<UIcon name="i-heroicons-check-circle" class="text-green-500 text-2xl shrink-0" />
|
|
162
|
+
<div>
|
|
163
|
+
<p class="font-medium text-green-700 dark:text-green-400">{{ t('onboarding.admin_done', 'Compte administrateur créé avec succès.') }}</p>
|
|
164
|
+
<p class="text-sm text-green-600 dark:text-green-500 mt-0.5">{{ currentUser?.name }} — {{ currentUser?.email }}</p>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
</template>
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
<!-- ── Step 2 : UI Languages ──────────────────────────────────────── -->
|
|
171
|
+
<div v-if="currentStep === 2" class="space-y-4">
|
|
172
|
+
<div>
|
|
173
|
+
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">{{ t('onboarding.languages_title', 'Langues de l\'interface') }}</h2>
|
|
174
|
+
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">{{ t('onboarding.languages_hint', 'Sélectionnez les langues disponibles pour l\'interface du dashboard.') }}</p>
|
|
175
|
+
</div>
|
|
176
|
+
<UInput
|
|
177
|
+
v-model="uiLangSearch"
|
|
178
|
+
:placeholder="t('onboarding.languages_search', 'Rechercher une langue...')"
|
|
179
|
+
icon="i-heroicons-magnifying-glass"
|
|
180
|
+
class="w-full"
|
|
181
|
+
/>
|
|
182
|
+
<div class="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden max-h-56 overflow-y-auto">
|
|
183
|
+
<button
|
|
184
|
+
v-for="lang in filteredUiLangs"
|
|
185
|
+
:key="lang.code"
|
|
186
|
+
class="w-full flex items-center gap-3 px-3 py-2 text-sm text-left transition-colors hover:bg-gray-50 dark:hover:bg-gray-800"
|
|
187
|
+
:class="selectedUiLangs.includes(lang.code) ? 'bg-primary-50 dark:bg-primary-900/20' : ''"
|
|
188
|
+
@click="toggleUiLang(lang)"
|
|
189
|
+
>
|
|
190
|
+
<span class="font-mono text-xs text-gray-400 w-10 shrink-0">{{ lang.code }}</span>
|
|
191
|
+
<span class="flex-1 text-gray-700 dark:text-gray-300">{{ lang.nativeName }}</span>
|
|
192
|
+
<span class="text-xs text-gray-400">{{ lang.name }}</span>
|
|
193
|
+
<UIcon v-if="selectedUiLangs.includes(lang.code)" name="i-heroicons-check" class="text-primary-500 shrink-0" />
|
|
194
|
+
</button>
|
|
195
|
+
</div>
|
|
196
|
+
<p class="text-xs text-gray-400">
|
|
197
|
+
{{ selectedUiLangs.length }} {{ t('onboarding.languages_selected', 'langue(s) sélectionnée(s)') }}
|
|
198
|
+
<span v-if="nonEnLangs.length" class="ml-1 text-primary-500">
|
|
199
|
+
— {{ nonEnLangs.length }} {{ t('onboarding.langs_will_be_translated', 'language(s) will be automatically translated via Google Translate') }}
|
|
200
|
+
</span>
|
|
201
|
+
</p>
|
|
202
|
+
<UFormField :label="t('onboarding.languages_default', 'Langue par défaut')">
|
|
203
|
+
<USelect v-model="defaultUiLang" :items="selectedUiLangsOptions" class="w-full" />
|
|
204
|
+
</UFormField>
|
|
205
|
+
</div>
|
|
206
|
+
|
|
207
|
+
<!-- ── Step 3 : First project ─────────────────────────────────────── -->
|
|
208
|
+
<div v-if="currentStep === 3" class="space-y-4">
|
|
209
|
+
<div>
|
|
210
|
+
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">{{ t('onboarding.project_title', 'Votre premier projet') }}</h2>
|
|
211
|
+
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">{{ t('onboarding.project_hint', 'Configurez le projet Vue.js que vous souhaitez gérer.') }}</p>
|
|
212
|
+
</div>
|
|
213
|
+
<UFormField :label="t('projects.name_label', 'Project name')" required>
|
|
214
|
+
<UInput v-model="projectForm.name" :placeholder="t('projects.name_placeholder', 'My App')" class="w-full" />
|
|
215
|
+
</UFormField>
|
|
216
|
+
<UFormField :label="t('settings.root_path', 'Project path')" required>
|
|
217
|
+
<UInput v-model="projectForm.root_path" placeholder="/path/to/my-project" class="w-full" />
|
|
218
|
+
</UFormField>
|
|
219
|
+
<UFormField :label="t('settings.locales_folder', 'Locales folder')">
|
|
220
|
+
<UInput v-model="projectForm.locales_path" placeholder="src/locales" class="w-full" />
|
|
221
|
+
</UFormField>
|
|
222
|
+
<p v-if="projectError" class="text-sm text-red-500 bg-red-50 dark:bg-red-900/20 rounded-lg px-3 py-2">{{ projectError }}</p>
|
|
223
|
+
</div>
|
|
224
|
+
|
|
225
|
+
<!-- ── Step 4 : Done ──────────────────────────────────────────────── -->
|
|
226
|
+
<div v-if="currentStep === 4" class="text-center space-y-4 py-4">
|
|
227
|
+
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-green-100 dark:bg-green-900/30 mb-2">
|
|
228
|
+
<UIcon name="i-heroicons-check" class="text-green-500 text-3xl" />
|
|
229
|
+
</div>
|
|
230
|
+
<h2 class="text-xl font-bold text-gray-900 dark:text-white">{{ t('onboarding.done_title', 'Tout est prêt !') }}</h2>
|
|
231
|
+
<p class="text-gray-500 dark:text-gray-400">{{ t('onboarding.done_hint', 'Votre dashboard est configuré. Vous pouvez maintenant gérer vos traductions.') }}</p>
|
|
232
|
+
<p v-if="nonEnLangs.length" class="text-sm text-primary-500">
|
|
233
|
+
{{ t('onboarding.translating_in_progress', 'Interface translation is in progress in the background for') }} {{ nonEnLangs.join(', ') }}.
|
|
234
|
+
</p>
|
|
235
|
+
</div>
|
|
236
|
+
|
|
237
|
+
<!-- ── Footer navigation ──────────────────────────────────────────── -->
|
|
238
|
+
<template #footer>
|
|
239
|
+
<div class="flex items-center justify-between">
|
|
240
|
+
<UButton
|
|
241
|
+
v-if="currentStep > 0 && currentStep < 4"
|
|
242
|
+
color="neutral"
|
|
243
|
+
variant="ghost"
|
|
244
|
+
icon="i-heroicons-arrow-left"
|
|
245
|
+
@click="currentStep--"
|
|
246
|
+
>
|
|
247
|
+
{{ t('onboarding.previous', 'Précédent') }}
|
|
248
|
+
</UButton>
|
|
249
|
+
<div v-else />
|
|
250
|
+
|
|
251
|
+
<div class="flex items-center gap-3">
|
|
252
|
+
<UButton
|
|
253
|
+
v-if="currentStep === 3"
|
|
254
|
+
color="neutral"
|
|
255
|
+
variant="ghost"
|
|
256
|
+
@click="skipProject"
|
|
257
|
+
>
|
|
258
|
+
{{ t('onboarding.project_skip', 'Passer cette étape') }}
|
|
259
|
+
</UButton>
|
|
260
|
+
|
|
261
|
+
<!-- Step 0: DB -->
|
|
262
|
+
<UButton v-if="currentStep === 0" icon="i-heroicons-arrow-right" trailing @click="currentStep++">
|
|
263
|
+
{{ t('onboarding.next', 'Suivant') }}
|
|
264
|
+
</UButton>
|
|
265
|
+
|
|
266
|
+
<!-- Step 1: Admin -->
|
|
267
|
+
<UButton
|
|
268
|
+
v-if="currentStep === 1"
|
|
269
|
+
:loading="saving"
|
|
270
|
+
icon="i-heroicons-arrow-right"
|
|
271
|
+
trailing
|
|
272
|
+
@click="hasUsers ? currentStep++ : createAdmin()"
|
|
273
|
+
>
|
|
274
|
+
{{ t('onboarding.next', 'Suivant') }}
|
|
275
|
+
</UButton>
|
|
276
|
+
|
|
277
|
+
<!-- Step 2: Languages -->
|
|
278
|
+
<UButton
|
|
279
|
+
v-if="currentStep === 2"
|
|
280
|
+
:loading="saving"
|
|
281
|
+
:disabled="selectedUiLangs.length === 0"
|
|
282
|
+
icon="i-heroicons-arrow-right"
|
|
283
|
+
trailing
|
|
284
|
+
@click="saveLanguages"
|
|
285
|
+
>
|
|
286
|
+
{{ t('onboarding.next', 'Suivant') }}
|
|
287
|
+
</UButton>
|
|
288
|
+
|
|
289
|
+
<!-- Step 3: Project -->
|
|
290
|
+
<UButton
|
|
291
|
+
v-if="currentStep === 3"
|
|
292
|
+
:loading="saving"
|
|
293
|
+
icon="i-heroicons-arrow-right"
|
|
294
|
+
trailing
|
|
295
|
+
@click="saveProject"
|
|
296
|
+
>
|
|
297
|
+
{{ t('onboarding.finish', 'Terminer') }}
|
|
298
|
+
</UButton>
|
|
299
|
+
|
|
300
|
+
<!-- Step 4: Done -->
|
|
301
|
+
<UButton
|
|
302
|
+
v-if="currentStep === 4"
|
|
303
|
+
icon="i-heroicons-home"
|
|
304
|
+
trailing
|
|
305
|
+
@click="goToDashboard"
|
|
306
|
+
>
|
|
307
|
+
{{ t('onboarding.go_to_dashboard', 'Aller au dashboard') }}
|
|
308
|
+
</UButton>
|
|
309
|
+
</div>
|
|
310
|
+
</div>
|
|
311
|
+
</template>
|
|
312
|
+
</UCard>
|
|
313
|
+
</div>
|
|
314
|
+
</div>
|
|
315
|
+
</template>
|
|
316
|
+
|
|
317
|
+
<script lang="ts" setup>
|
|
318
|
+
definePageMeta({ layout: false })
|
|
319
|
+
|
|
320
|
+
const router = useRouter()
|
|
321
|
+
const { t } = useT()
|
|
322
|
+
const { currentUser, fetchMe } = useAuth()
|
|
323
|
+
await fetchMe()
|
|
324
|
+
|
|
325
|
+
const { data: authStatus } = await useFetch('/api/auth/status', { key: 'auth-status' })
|
|
326
|
+
const hasUsers = computed(() => !!(authStatus.value as any)?.hasUsers)
|
|
327
|
+
|
|
328
|
+
const currentStep = ref(0)
|
|
329
|
+
const saving = ref(false)
|
|
330
|
+
|
|
331
|
+
const steps = [
|
|
332
|
+
{ label: t('onboarding.step_db', 'Database') },
|
|
333
|
+
{ label: t('onboarding.step_admin', 'Admin account') },
|
|
334
|
+
{ label: t('onboarding.step_languages', 'Interface languages') },
|
|
335
|
+
{ label: t('onboarding.step_project', 'First project') },
|
|
336
|
+
{ label: t('onboarding.step_done', 'Done') },
|
|
337
|
+
]
|
|
338
|
+
|
|
339
|
+
// ─── Step 0 : Database config ──────────────────────────────────────────────────
|
|
340
|
+
const { data: dbConfig, refresh: refreshDbConfig } = await useFetch<any>('/api/db-config')
|
|
341
|
+
|
|
342
|
+
const dbTypeOptions = computed(() => [
|
|
343
|
+
{ label: t('onboarding.db_type_sqlite', 'SQLite (local file)'), value: 'sqlite' },
|
|
344
|
+
{ label: t('onboarding.db_type_postgresql', 'PostgreSQL'), value: 'postgresql' },
|
|
345
|
+
{ label: t('onboarding.db_type_mysql', 'MySQL / MariaDB'), value: 'mysql' },
|
|
346
|
+
])
|
|
347
|
+
|
|
348
|
+
const dbForm = ref({
|
|
349
|
+
type: (dbConfig.value?.type as string) || 'sqlite',
|
|
350
|
+
connection: (dbConfig.value?.connection as string) || './i18n-dashboard.db',
|
|
351
|
+
host: (dbConfig.value?.host as string) || 'localhost',
|
|
352
|
+
port: (dbConfig.value?.port as string) || '5432',
|
|
353
|
+
user: (dbConfig.value?.user as string) || '',
|
|
354
|
+
password: '',
|
|
355
|
+
database: (dbConfig.value?.database as string) || '',
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
const sqliteFileExists = ref<boolean>(dbConfig.value?.fileExists ?? true)
|
|
359
|
+
const dbConnected = ref(false)
|
|
360
|
+
const testingDb = ref(false)
|
|
361
|
+
const applyingDb = ref(false)
|
|
362
|
+
const creatingFile = ref(false)
|
|
363
|
+
const dbError = ref('')
|
|
364
|
+
|
|
365
|
+
// Track if form changed from initial config
|
|
366
|
+
const dbFormOriginal = JSON.stringify(dbForm.value)
|
|
367
|
+
const dbFormChanged = computed(() => JSON.stringify(dbForm.value) !== dbFormOriginal)
|
|
368
|
+
|
|
369
|
+
// Update sqliteFileExists when SQLite path changes
|
|
370
|
+
let _checkPathTimer: ReturnType<typeof setTimeout> | null = null
|
|
371
|
+
watch(() => dbForm.value.connection, (path) => {
|
|
372
|
+
if (dbForm.value.type !== 'sqlite') return
|
|
373
|
+
if (_checkPathTimer) clearTimeout(_checkPathTimer)
|
|
374
|
+
_checkPathTimer = setTimeout(async () => {
|
|
375
|
+
try {
|
|
376
|
+
const result = await $fetch<any>(`/api/db-config?checkPath=${encodeURIComponent(path)}`)
|
|
377
|
+
sqliteFileExists.value = result.fileExists ?? false
|
|
378
|
+
} catch { /* ignore */ }
|
|
379
|
+
}, 400)
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
async function createSqliteFile() {
|
|
383
|
+
creatingFile.value = true
|
|
384
|
+
dbError.value = ''
|
|
385
|
+
try {
|
|
386
|
+
await $fetch('/api/db-config', {
|
|
387
|
+
method: 'POST',
|
|
388
|
+
body: { type: 'sqlite', connection: dbForm.value.connection, createFile: true },
|
|
389
|
+
})
|
|
390
|
+
sqliteFileExists.value = true
|
|
391
|
+
} catch (e: any) {
|
|
392
|
+
dbError.value = e.data?.message || t('onboarding.db_create_file_error', 'Error creating the file.')
|
|
393
|
+
} finally {
|
|
394
|
+
creatingFile.value = false
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function dbBody() {
|
|
399
|
+
return {
|
|
400
|
+
type: dbForm.value.type,
|
|
401
|
+
connection: dbForm.value.connection,
|
|
402
|
+
host: dbForm.value.host,
|
|
403
|
+
port: dbForm.value.port,
|
|
404
|
+
user: dbForm.value.user,
|
|
405
|
+
password: dbForm.value.password,
|
|
406
|
+
database: dbForm.value.database,
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async function testDbConnection() {
|
|
411
|
+
testingDb.value = true
|
|
412
|
+
dbConnected.value = false
|
|
413
|
+
dbError.value = ''
|
|
414
|
+
try {
|
|
415
|
+
await $fetch('/api/db-config', { method: 'POST', body: { ...dbBody(), testOnly: true } })
|
|
416
|
+
dbConnected.value = true
|
|
417
|
+
} catch (e: any) {
|
|
418
|
+
dbError.value = e.data?.message || 'Connection failed.'
|
|
419
|
+
} finally {
|
|
420
|
+
testingDb.value = false
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
async function applyDbConfig() {
|
|
425
|
+
applyingDb.value = true
|
|
426
|
+
dbError.value = ''
|
|
427
|
+
try {
|
|
428
|
+
await $fetch('/api/db-config', { method: 'POST', body: dbBody() })
|
|
429
|
+
dbConnected.value = true
|
|
430
|
+
await refreshDbConfig()
|
|
431
|
+
sqliteFileExists.value = dbConfig.value?.fileExists ?? true
|
|
432
|
+
} catch (e: any) {
|
|
433
|
+
dbError.value = e.data?.message || 'Failed to apply configuration.'
|
|
434
|
+
dbConnected.value = false
|
|
435
|
+
} finally {
|
|
436
|
+
applyingDb.value = false
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
// ─── Step 1 : Admin account ────────────────────────────────────────────────────
|
|
442
|
+
const adminForm = ref({ name: '', email: '', password: '', confirm: '' })
|
|
443
|
+
const adminError = ref('')
|
|
444
|
+
|
|
445
|
+
async function createAdmin() {
|
|
446
|
+
adminError.value = ''
|
|
447
|
+
if (!adminForm.value.name || !adminForm.value.email || !adminForm.value.password) {
|
|
448
|
+
adminError.value = t('onboarding.all_fields_required', 'All fields are required.')
|
|
449
|
+
return
|
|
450
|
+
}
|
|
451
|
+
if (adminForm.value.password.length < 8) {
|
|
452
|
+
adminError.value = t('onboarding.password_min_length', 'Password must be at least 8 characters.')
|
|
453
|
+
return
|
|
454
|
+
}
|
|
455
|
+
if (adminForm.value.password !== adminForm.value.confirm) {
|
|
456
|
+
adminError.value = t('onboarding.passwords_mismatch', 'Passwords do not match.')
|
|
457
|
+
return
|
|
458
|
+
}
|
|
459
|
+
saving.value = true
|
|
460
|
+
try {
|
|
461
|
+
await $fetch('/api/setup', {
|
|
462
|
+
method: 'POST',
|
|
463
|
+
body: { name: adminForm.value.name, email: adminForm.value.email, password: adminForm.value.password },
|
|
464
|
+
})
|
|
465
|
+
await fetchMe()
|
|
466
|
+
currentStep.value = 2
|
|
467
|
+
} catch (e: any) {
|
|
468
|
+
adminError.value = e.data?.message || t('onboarding.admin_creation_error', 'Error creating the account.')
|
|
469
|
+
} finally {
|
|
470
|
+
saving.value = false
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// ─── Step 2 : UI Languages ─────────────────────────────────────────────────────
|
|
475
|
+
const { languages: allWorldLangs, filteredLanguages, searchQuery: uiLangSearch } = useLanguages()
|
|
476
|
+
|
|
477
|
+
const { data: configData } = await useFetch<{ uiLanguages?: string[]; defaultUiLanguage?: string }>('/api/config')
|
|
478
|
+
const selectedUiLangs = ref<string[]>(configData.value?.uiLanguages || ['en'])
|
|
479
|
+
const defaultUiLang = ref(configData.value?.defaultUiLanguage || 'en')
|
|
480
|
+
|
|
481
|
+
const filteredUiLangs = computed(() => filteredLanguages.value)
|
|
482
|
+
const nonEnLangs = computed(() => selectedUiLangs.value.filter(c => c !== 'en'))
|
|
483
|
+
|
|
484
|
+
const selectedUiLangsOptions = computed(() =>
|
|
485
|
+
selectedUiLangs.value.map((code) => {
|
|
486
|
+
const lang = allWorldLangs.find((l) => l.code === code)
|
|
487
|
+
return { label: lang ? `${lang.nativeName} (${code})` : code, value: code }
|
|
488
|
+
}),
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
function toggleUiLang(lang: { code: string; name: string; nativeName: string }) {
|
|
492
|
+
const idx = selectedUiLangs.value.indexOf(lang.code)
|
|
493
|
+
if (idx >= 0) {
|
|
494
|
+
if (selectedUiLangs.value.length === 1) return
|
|
495
|
+
selectedUiLangs.value.splice(idx, 1)
|
|
496
|
+
if (defaultUiLang.value === lang.code) defaultUiLang.value = selectedUiLangs.value[0]
|
|
497
|
+
} else {
|
|
498
|
+
selectedUiLangs.value.push(lang.code)
|
|
499
|
+
if (selectedUiLangs.value.length === 1) defaultUiLang.value = lang.code
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
async function saveLanguages() {
|
|
504
|
+
saving.value = true
|
|
505
|
+
try {
|
|
506
|
+
const langs = selectedUiLangs.value.map((code) => {
|
|
507
|
+
const lang = allWorldLangs.find((l) => l.code === code)
|
|
508
|
+
return { code, name: lang?.name || code }
|
|
509
|
+
})
|
|
510
|
+
await $fetch('/api/onboarding', {
|
|
511
|
+
method: 'POST',
|
|
512
|
+
body: { languages: langs, defaultLanguage: defaultUiLang.value },
|
|
513
|
+
})
|
|
514
|
+
currentStep.value = 3
|
|
515
|
+
} catch {
|
|
516
|
+
// silent — onboarding marked complete regardless
|
|
517
|
+
} finally {
|
|
518
|
+
saving.value = false
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// ─── Step 3 : First project ────────────────────────────────────────────────────
|
|
523
|
+
const projectForm = ref({
|
|
524
|
+
name: configData.value?.project?.name || '',
|
|
525
|
+
root_path: '',
|
|
526
|
+
locales_path: configData.value?.project?.localesPath || 'src/locales',
|
|
527
|
+
})
|
|
528
|
+
const projectError = ref('')
|
|
529
|
+
|
|
530
|
+
async function saveProject() {
|
|
531
|
+
projectError.value = ''
|
|
532
|
+
if (!projectForm.value.name.trim() || !projectForm.value.root_path.trim()) {
|
|
533
|
+
projectError.value = t('onboarding.project_name_path_required', 'Project name and path are required.')
|
|
534
|
+
return
|
|
535
|
+
}
|
|
536
|
+
saving.value = true
|
|
537
|
+
try {
|
|
538
|
+
await $fetch('/api/projects', {
|
|
539
|
+
method: 'POST',
|
|
540
|
+
body: {
|
|
541
|
+
name: projectForm.value.name.trim(),
|
|
542
|
+
root_path: projectForm.value.root_path.trim(),
|
|
543
|
+
locales_path: projectForm.value.locales_path || 'src/locales',
|
|
544
|
+
},
|
|
545
|
+
})
|
|
546
|
+
currentStep.value = 4
|
|
547
|
+
} catch (e: any) {
|
|
548
|
+
projectError.value = e.data?.message || t('onboarding.project_creation_error', 'Error creating the project.')
|
|
549
|
+
} finally {
|
|
550
|
+
saving.value = false
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function skipProject() {
|
|
555
|
+
currentStep.value = 4
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// ─── Step 4 : Done ─────────────────────────────────────────────────────────────
|
|
559
|
+
async function goToDashboard() {
|
|
560
|
+
await clearNuxtData('auth-status')
|
|
561
|
+
await router.push('/')
|
|
562
|
+
}
|
|
563
|
+
</script>
|