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.
Files changed (176) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +715 -0
  3. package/app.vue +8 -0
  4. package/assets/css/main.css +21 -0
  5. package/assets/locales/en.json +380 -0
  6. package/bin/cli.mjs +279 -0
  7. package/components/LinkedKeyPicker.vue +135 -0
  8. package/components/PathPicker.vue +153 -0
  9. package/components/PluralEditor.vue +295 -0
  10. package/components/ScanModal.vue +153 -0
  11. package/components/TranslationHistoryModal.vue +66 -0
  12. package/components/TranslationRow.vue +541 -0
  13. package/components/dashboard/WidgetConfigModal.vue +121 -0
  14. package/components/dashboard/WidgetGrid.vue +190 -0
  15. package/components/dashboard/WidgetPicker.vue +75 -0
  16. package/components/dashboard/widgets/ActivityWidget.vue +109 -0
  17. package/components/dashboard/widgets/LanguagesCoverageWidget.vue +104 -0
  18. package/components/dashboard/widgets/ProjectsWidget.vue +77 -0
  19. package/components/dashboard/widgets/ReviewWidget.vue +150 -0
  20. package/components/dashboard/widgets/StatWidget.vue +133 -0
  21. package/composables/useAuth.ts +72 -0
  22. package/composables/useConfig.ts +14 -0
  23. package/composables/useDashboard.ts +89 -0
  24. package/composables/useFormats.ts +100 -0
  25. package/composables/useKeys.ts +231 -0
  26. package/composables/useLanguages.ts +221 -0
  27. package/composables/useProfile.ts +76 -0
  28. package/composables/useProject.ts +180 -0
  29. package/composables/useReview.ts +94 -0
  30. package/composables/useSettings.ts +30 -0
  31. package/composables/useStats.ts +16 -0
  32. package/composables/useT.ts +38 -0
  33. package/composables/useUsers.ts +101 -0
  34. package/composables/useWidgetData.ts +50 -0
  35. package/consts/commons.const.ts +6 -0
  36. package/consts/dashboard.const.ts +94 -0
  37. package/consts/languages.const.ts +223 -0
  38. package/enums/commons.enum.ts +7 -0
  39. package/i18n-dashboard.config.example.js +40 -0
  40. package/interfaces/commons.interface.ts +23 -0
  41. package/interfaces/job.interface.ts +10 -0
  42. package/interfaces/key.interface.ts +39 -0
  43. package/interfaces/languages.interface.ts +23 -0
  44. package/interfaces/project.interface.ts +9 -0
  45. package/interfaces/scan.interface.ts +12 -0
  46. package/interfaces/settings.interface.ts +4 -0
  47. package/interfaces/stat.interface.ts +30 -0
  48. package/interfaces/translation.interface.ts +11 -0
  49. package/interfaces/user.interface.ts +24 -0
  50. package/layouts/auth.vue +5 -0
  51. package/layouts/default.vue +327 -0
  52. package/middleware/auth.global.ts +26 -0
  53. package/nuxt.config.ts +66 -0
  54. package/package.json +89 -0
  55. package/pages/index.vue +5 -0
  56. package/pages/login.vue +74 -0
  57. package/pages/onboarding.vue +563 -0
  58. package/pages/projects/[id]/formats/datetime.vue +240 -0
  59. package/pages/projects/[id]/formats/modifiers.vue +194 -0
  60. package/pages/projects/[id]/formats/number.vue +250 -0
  61. package/pages/projects/[id]/index.vue +182 -0
  62. package/pages/projects/[id]/languages.vue +537 -0
  63. package/pages/projects/[id]/review.vue +109 -0
  64. package/pages/projects/[id]/settings.vue +515 -0
  65. package/pages/projects/[id]/translations/[keyId].vue +642 -0
  66. package/pages/projects/[id]/translations/index.vue +250 -0
  67. package/pages/projects/[id]/users.vue +276 -0
  68. package/pages/projects/index.vue +334 -0
  69. package/pages/users/[id]/profile.vue +421 -0
  70. package/pages/users/index.vue +345 -0
  71. package/plugins/loading.client.ts +3 -0
  72. package/plugins/ui-i18n.ts +6 -0
  73. package/server/api/auth/login.post.ts +28 -0
  74. package/server/api/auth/logout.post.ts +7 -0
  75. package/server/api/auth/me.get.ts +11 -0
  76. package/server/api/auth/me.put.ts +31 -0
  77. package/server/api/auth/password.put.ts +27 -0
  78. package/server/api/auth/status.get.ts +16 -0
  79. package/server/api/config.get.ts +10 -0
  80. package/server/api/dashboard/layout.get.ts +18 -0
  81. package/server/api/dashboard/layout.post.ts +18 -0
  82. package/server/api/db-config.get.ts +44 -0
  83. package/server/api/db-config.post.ts +73 -0
  84. package/server/api/export.get.ts +64 -0
  85. package/server/api/formats/datetime/[id].delete.ts +8 -0
  86. package/server/api/formats/datetime/[id].put.ts +15 -0
  87. package/server/api/formats/datetime.get.ts +11 -0
  88. package/server/api/formats/datetime.post.ts +16 -0
  89. package/server/api/formats/modifiers/[id].delete.ts +8 -0
  90. package/server/api/formats/modifiers/[id].put.ts +10 -0
  91. package/server/api/formats/modifiers.get.ts +10 -0
  92. package/server/api/formats/modifiers.post.ts +14 -0
  93. package/server/api/formats/number/[id].delete.ts +8 -0
  94. package/server/api/formats/number/[id].put.ts +15 -0
  95. package/server/api/formats/number.get.ts +11 -0
  96. package/server/api/formats/number.post.ts +16 -0
  97. package/server/api/formats/snippet.get.ts +87 -0
  98. package/server/api/fs/browse.get.ts +50 -0
  99. package/server/api/history/[translationId].get.ts +13 -0
  100. package/server/api/keys/[id].delete.ts +14 -0
  101. package/server/api/keys/[id].get.ts +41 -0
  102. package/server/api/keys/[id].patch.ts +20 -0
  103. package/server/api/keys/index.get.ts +98 -0
  104. package/server/api/keys/index.post.ts +17 -0
  105. package/server/api/languages/[code].delete.ts +15 -0
  106. package/server/api/languages/[id].put.ts +24 -0
  107. package/server/api/languages/index.get.ts +13 -0
  108. package/server/api/languages/index.post.ts +42 -0
  109. package/server/api/onboarding.post.ts +56 -0
  110. package/server/api/profile.get.ts +81 -0
  111. package/server/api/project-snapshot.get.ts +73 -0
  112. package/server/api/project-snapshot.post.ts +160 -0
  113. package/server/api/projects/[id].delete.ts +13 -0
  114. package/server/api/projects/[id].put.ts +40 -0
  115. package/server/api/projects/index.get.ts +19 -0
  116. package/server/api/projects/index.post.ts +34 -0
  117. package/server/api/scan.post.ts +165 -0
  118. package/server/api/settings/index.get.ts +9 -0
  119. package/server/api/settings/index.post.ts +20 -0
  120. package/server/api/setup.post.ts +39 -0
  121. package/server/api/stats/global.get.ts +126 -0
  122. package/server/api/stats.get.ts +70 -0
  123. package/server/api/sync.post.ts +179 -0
  124. package/server/api/translate.post.ts +52 -0
  125. package/server/api/translations/batch-translate.post.ts +121 -0
  126. package/server/api/translations/bulk-status.post.ts +24 -0
  127. package/server/api/translations/index.post.ts +62 -0
  128. package/server/api/translations/job/[id].get.ts +23 -0
  129. package/server/api/translations/status.post.ts +30 -0
  130. package/server/api/translations/translate-all.post.ts +18 -0
  131. package/server/api/ui-locale.get.ts +39 -0
  132. package/server/api/users/[id]/profile.get.ts +107 -0
  133. package/server/api/users/[id]/roles.put.ts +67 -0
  134. package/server/api/users/[id].delete.ts +36 -0
  135. package/server/api/users/[id].put.ts +43 -0
  136. package/server/api/users/index.get.ts +49 -0
  137. package/server/api/users/index.post.ts +89 -0
  138. package/server/consts/auto-translate.const.ts +2 -0
  139. package/server/consts/commons.const.ts +10 -0
  140. package/server/consts/db.const.ts +3 -0
  141. package/server/consts/scanner.const.ts +4 -0
  142. package/server/consts/translation-job.const.ts +8 -0
  143. package/server/db/index.ts +672 -0
  144. package/server/enums/auth.enum.ts +5 -0
  145. package/server/enums/translation.enum.ts +6 -0
  146. package/server/interfaces/profile.interface.ts +48 -0
  147. package/server/interfaces/project-config.interface.ts +9 -0
  148. package/server/interfaces/scanner.interface.ts +18 -0
  149. package/server/interfaces/translation-job.interface.ts +13 -0
  150. package/server/middleware/auth.ts +32 -0
  151. package/server/plugins/db.ts +6 -0
  152. package/server/routes/locale/[lang].get.ts +179 -0
  153. package/server/types/auth.type.ts +3 -0
  154. package/server/utils/auth.util.ts +89 -0
  155. package/server/utils/auto-translate.util.ts +112 -0
  156. package/server/utils/lang-api.util.ts +24 -0
  157. package/server/utils/mailer.util.ts +80 -0
  158. package/server/utils/project-config.util.ts +37 -0
  159. package/server/utils/scanner.uti.ts +307 -0
  160. package/server/utils/translation-job.util.ts +142 -0
  161. package/services/auth.service.ts +31 -0
  162. package/services/base.service.ts +140 -0
  163. package/services/job.service.ts +10 -0
  164. package/services/key.service.ts +26 -0
  165. package/services/language.service.ts +26 -0
  166. package/services/profile.service.ts +14 -0
  167. package/services/project.service.ts +23 -0
  168. package/services/scan.service.ts +14 -0
  169. package/services/settings.service.ts +14 -0
  170. package/services/stats.service.ts +11 -0
  171. package/services/translation.service.ts +36 -0
  172. package/services/user.service.ts +28 -0
  173. package/tsconfig.json +3 -0
  174. package/types/commons.type.ts +3 -0
  175. package/types/dashboard.type.ts +26 -0
  176. package/utils/config.util.ts +60 -0
@@ -0,0 +1,334 @@
1
+ <template>
2
+ <div class="p-6">
3
+ <div class="flex items-center justify-between mb-6">
4
+ <div>
5
+ <h1 class="text-2xl font-bold text-gray-900 dark:text-white">{{ t('nav.projects', 'Projects') }}</h1>
6
+ <p class="text-gray-500 dark:text-gray-400 mt-0.5 text-sm">{{ t('projects.subtitle', 'Manage your Vue.js projects') }}</p>
7
+ </div>
8
+ <UButton icon="i-heroicons-plus" @click="openAdd">{{ t('projects.add', 'Add a project') }}</UButton>
9
+ </div>
10
+
11
+ <!-- Skeleton -->
12
+ <div v-if="pending" class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
13
+ <div v-for="i in 3" :key="i" class="bg-white dark:bg-gray-900 rounded-xl border border-gray-200 dark:border-gray-800 p-5 space-y-4">
14
+ <div class="flex items-center gap-3">
15
+ <USkeleton class="w-10 h-10 rounded-lg shrink-0" />
16
+ <div class="flex-1 space-y-1.5">
17
+ <USkeleton class="h-4 w-2/3" />
18
+ <USkeleton class="h-3 w-full" />
19
+ </div>
20
+ </div>
21
+ <div class="grid grid-cols-2 gap-3">
22
+ <USkeleton class="h-16 rounded-lg" />
23
+ <USkeleton class="h-16 rounded-lg" />
24
+ </div>
25
+ <USkeleton class="h-8 rounded-lg" />
26
+ </div>
27
+ </div>
28
+
29
+ <!-- Empty state -->
30
+ <div v-else-if="!userProjects.length"
31
+ class="bg-white dark:bg-gray-900 rounded-xl border border-dashed border-gray-300 dark:border-gray-700 p-16 text-center">
32
+ <UIcon class="text-6xl text-gray-300 dark:text-gray-600 mb-4" name="i-heroicons-folder-open"/>
33
+ <h2 class="text-lg font-semibold text-gray-600 dark:text-gray-400 mb-2">{{ t('projects.none', 'No project') }}</h2>
34
+ <p class="text-gray-400 text-sm max-w-md mx-auto mb-6">
35
+ {{ t('projects.none_hint', 'Add a Vue.js project to start managing its translation keys. Point to the root folder of your project.') }}
36
+ </p>
37
+ <UButton icon="i-heroicons-plus" size="lg" @click="openAdd">{{ t('projects.add_first', 'Add your first project') }}</UButton>
38
+ </div>
39
+
40
+ <!-- Project cards -->
41
+ <div v-else-if="userProjects.length" class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
42
+ <div
43
+ v-for="project in userProjects"
44
+ :key="project.id"
45
+ class="bg-white dark:bg-gray-900 rounded-xl border border-gray-200 dark:border-gray-800 p-5 flex flex-col gap-4 hover:border-primary-300 dark:hover:border-primary-700 transition-colors cursor-pointer"
46
+ @click="router.push(`/projects/${project.id}`)"
47
+ >
48
+ <!-- Header -->
49
+ <div class="flex items-start justify-between">
50
+ <div class="flex items-center gap-3">
51
+ <div :class="`bg-${project.color || 'primary'}-100 dark:bg-${project.color || 'primary'}-900/30`"
52
+ class="w-10 h-10 rounded-lg flex items-center justify-center shrink-0">
53
+ <UIcon :class="`text-${project.color || 'primary'}-600`" class="text-xl" name="i-heroicons-folder"/>
54
+ </div>
55
+ <div class="min-w-0">
56
+ <h3 class="font-semibold text-gray-900 dark:text-white truncate">{{ project.name }}</h3>
57
+ <p class="text-xs text-gray-400 truncate font-mono mt-0.5">{{ project.root_path }}</p>
58
+ </div>
59
+ </div>
60
+ <UDropdownMenu :items="projectActions(project)" @click.stop>
61
+ <UButton color="neutral" icon="i-heroicons-ellipsis-vertical" size="xs" variant="ghost" @click.stop/>
62
+ </UDropdownMenu>
63
+ </div>
64
+
65
+ <!-- Description -->
66
+ <p v-if="project.description" class="text-sm text-gray-500 dark:text-gray-400">
67
+ {{ project.description }}
68
+ </p>
69
+
70
+ <!-- Stats -->
71
+ <div class="grid grid-cols-2 gap-3">
72
+ <div class="bg-gray-50 dark:bg-gray-800 rounded-lg p-3">
73
+ <p class="text-2xl font-bold text-gray-900 dark:text-white">{{ project.key_count }}</p>
74
+ <p class="text-xs text-gray-400 mt-0.5">{{ t('projects.keys_stat', 'Translation keys') }}</p>
75
+ </div>
76
+ <div class="bg-gray-50 dark:bg-gray-800 rounded-lg p-3">
77
+ <p class="text-2xl font-bold text-gray-900 dark:text-white">{{ project.language_count }}</p>
78
+ <p class="text-xs text-gray-400 mt-0.5">{{ t('nav.languages', 'Languages') }}</p>
79
+ </div>
80
+ </div>
81
+
82
+ <!-- Config info -->
83
+ <div class="flex flex-wrap gap-3 text-xs text-gray-400">
84
+ <span v-if="project.root_path" class="flex items-center gap-1">
85
+ <UIcon class="text-xs" name="i-heroicons-folder"/>
86
+ {{ project.locales_path }}
87
+ </span>
88
+ <span v-if="project.source_url" class="flex items-center gap-1 text-blue-400">
89
+ <UIcon class="text-xs" name="i-heroicons-globe-alt"/>
90
+ <span class="truncate max-w-[160px]">{{ project.source_url }}</span>
91
+ </span>
92
+ <span class="flex items-center gap-1">
93
+ <UIcon class="text-xs" name="i-heroicons-key"/>
94
+ {{ t('projects.separator', 'separator:') }} <code class="font-mono">{{ project.key_separator }}</code>
95
+ </span>
96
+ </div>
97
+
98
+ <!-- Actions -->
99
+ <div class="flex gap-2 pt-1 border-t border-gray-100 dark:border-gray-800">
100
+ <UTooltip :text="!canScanProject(project) ? t('projects.scan_requires_local', 'Requires a local path') : t('projects.scan_tooltip', 'Scan source files')" :disabled="canScanProject(project)">
101
+ <UButton
102
+ :disabled="!canScanProject(project)"
103
+ :loading="scanning === project.id"
104
+ color="neutral"
105
+ icon="i-heroicons-magnifying-glass"
106
+ size="xs"
107
+ variant="outline"
108
+ @click.stop="canScanProject(project) && scanProject(project)"
109
+ >
110
+ {{ t('sidebar.scan', 'Scan') }}
111
+ </UButton>
112
+ </UTooltip>
113
+ <UTooltip :text="!canSyncProject(project) ? t('projects.sync_requires_source', 'Requires a local path or remote URL') : t('projects.sync_tooltip', 'Sync JSON files')" :disabled="canSyncProject(project)">
114
+ <UButton
115
+ :disabled="!canSyncProject(project)"
116
+ :loading="syncing === project.id"
117
+ color="neutral"
118
+ icon="i-heroicons-arrow-path"
119
+ size="xs"
120
+ variant="outline"
121
+ @click.stop="canSyncProject(project) && syncProject(project)"
122
+ >
123
+ {{ t('sidebar.sync', 'Sync JSON') }}
124
+ </UButton>
125
+ </UTooltip>
126
+ <UButton class="ml-auto" color="primary" size="xs" @click.stop="router.push(`/projects/${project.id}`)">
127
+ {{ t('projects.open', 'Open') }}
128
+ </UButton>
129
+ </div>
130
+ </div>
131
+ </div>
132
+
133
+ <!-- Add / Edit modal -->
134
+ <UModal v-model:open="showModal" :title="editingProject ? t('projects.edit_modal_title', 'Edit project') : t('projects.add_modal_title', 'Add a project')">
135
+ <template #body>
136
+ <div class="space-y-4">
137
+ <UFormField :label="t('projects.name_label', 'Project name')" required>
138
+ <UInput v-model="form.name" class="w-full" :placeholder="t('projects.name_placeholder', 'My Vue App')"/>
139
+ </UFormField>
140
+
141
+ <!-- Source mode toggle -->
142
+ <div class="bg-gray-50 dark:bg-gray-800 rounded-lg p-3 space-y-3">
143
+ <p class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">{{ t('projects.source_title', 'File source') }}</p>
144
+
145
+ <UFormField :hint="t('projects.local_path_hint', 'Absolute path on the server where the dashboard runs')" :label="t('projects.local_path_label', 'Local path (scan + sync)')">
146
+ <PathPicker v-model="form.root_path" class="w-full" />
147
+ </UFormField>
148
+
149
+ <div class="flex items-center gap-2 text-xs text-gray-400">
150
+ <div class="flex-1 h-px bg-gray-200 dark:bg-gray-700"/>
151
+ <span>{{ t('common.or', 'or') }}</span>
152
+ <div class="flex-1 h-px bg-gray-200 dark:bg-gray-700"/>
153
+ </div>
154
+
155
+ <UFormField
156
+ :hint="t('projects.remote_url_hint', 'URL of a remote project exposing /locale/[lang].json — used for sync only')"
157
+ :label="t('projects.remote_url_label', 'Remote URL (sync only)')"
158
+ >
159
+ <UInput v-model="form.source_url" class="w-full" placeholder="https://my-app.com"/>
160
+ </UFormField>
161
+
162
+ <!-- Source mode indicator -->
163
+ <div v-if="!form.root_path && !form.source_url" class="flex items-center gap-1.5 text-xs text-amber-600 dark:text-amber-400">
164
+ <UIcon name="i-heroicons-exclamation-triangle" class="shrink-0"/>
165
+ {{ t('projects.no_source_warning', 'Without a source configured, scan and sync will be disabled.') }}
166
+ </div>
167
+ <div v-else-if="form.root_path && !form.source_url" class="flex items-center gap-1.5 text-xs text-green-600 dark:text-green-400">
168
+ <UIcon name="i-heroicons-check-circle" class="shrink-0"/>
169
+ {{ t('projects.local_active', 'Scan + Sync enabled via local path.') }}
170
+ </div>
171
+ <div v-else-if="!form.root_path && form.source_url" class="flex items-center gap-1.5 text-xs text-blue-600 dark:text-blue-400">
172
+ <UIcon name="i-heroicons-information-circle" class="shrink-0"/>
173
+ {{ t('projects.remote_active', 'Sync enabled via remote URL. Scan disabled (requires local path).') }}
174
+ </div>
175
+ <div v-else class="flex items-center gap-1.5 text-xs text-green-600 dark:text-green-400">
176
+ <UIcon name="i-heroicons-check-circle" class="shrink-0"/>
177
+ {{ t('projects.both_active', 'Scan + Sync enabled (local path takes priority).') }}
178
+ </div>
179
+ </div>
180
+
181
+ <UFormField :hint="t('projects.locales_path_hint', 'Relative to the project root')" :label="t('projects.locales_path_label', 'Locales folder')">
182
+ <UInput v-model="form.locales_path" class="w-full" placeholder="src/locales"/>
183
+ </UFormField>
184
+
185
+ <div class="grid grid-cols-2 gap-4">
186
+ <UFormField :label="t('projects.key_separator_label', 'Key separator')">
187
+ <UInput v-model="form.key_separator" :maxlength="5" class="w-full" placeholder="."/>
188
+ </UFormField>
189
+ <UFormField :label="t('projects.color_label', 'Color')">
190
+ <USelect v-model="form.color" :items="colorOptions" class="w-full"/>
191
+ </UFormField>
192
+ </div>
193
+
194
+ <UFormField :label="t('translations.description_label', 'Description')">
195
+ <UInput v-model="form.description" class="w-full" :placeholder="t('projects.description_placeholder', 'Optional description')"/>
196
+ </UFormField>
197
+
198
+ <div v-if="form.root_path" class="bg-gray-50 dark:bg-gray-800 rounded-lg p-3">
199
+ <p class="text-xs text-gray-500 font-medium mb-1">{{ t('projects.locales_path_preview', 'Locales path:') }}</p>
200
+ <code class="text-xs text-gray-700 dark:text-gray-300 font-mono break-all">
201
+ {{ form.root_path }}/{{ form.locales_path || 'src/locales' }}
202
+ </code>
203
+ </div>
204
+ </div>
205
+ </template>
206
+ <template #footer>
207
+ <div class="flex justify-end gap-3">
208
+ <UButton color="neutral" variant="ghost" @click="showModal = false">{{ t('common.cancel', 'Cancel') }}</UButton>
209
+ <UButton :disabled="!form.name" :loading="saving" @click="saveProject">
210
+ {{ editingProject ? t('common.edit', 'Edit') : t('common.add', 'Add') }}
211
+ </UButton>
212
+ </div>
213
+ </template>
214
+ </UModal>
215
+
216
+ <!-- Delete confirm -->
217
+ <UModal v-model:open="showDeleteConfirm" :title="t('projects.delete_title', 'Delete project')">
218
+ <template #body>
219
+ <p class="text-gray-600 dark:text-gray-400">
220
+ {{ t('projects.delete_confirm', 'Delete') }} <strong>{{ deletingProject?.name }}</strong>? {{ t('projects.delete_warning', 'All keys, translations and history will be deleted.') }}
221
+ </p>
222
+ <p class="text-red-500 text-sm mt-2 font-medium">{{ t('common.irreversible', 'This action is irreversible.') }}</p>
223
+ </template>
224
+ <template #footer>
225
+ <div class="flex justify-end gap-3">
226
+ <UButton color="neutral" variant="ghost" @click="showDeleteConfirm = false">{{ t('common.cancel', 'Cancel') }}</UButton>
227
+ <UButton :loading="deleting" color="error" @click="deleteProject">{{ t('common.delete', 'Delete') }}</UButton>
228
+ </div>
229
+ </template>
230
+ </UModal>
231
+ </div>
232
+ </template>
233
+
234
+ <script lang="ts" setup>
235
+ import { canScanProject, canSyncProject } from '~/composables/useProject'
236
+
237
+ const router = useRouter()
238
+ const { t } = useT()
239
+
240
+ const showModal = ref(false)
241
+ const showDeleteConfirm = ref(false)
242
+ const editingProject = ref<any>(null)
243
+ const deletingProject = ref<any>(null)
244
+
245
+ const form = ref({
246
+ name: '',
247
+ root_path: '',
248
+ source_url: '',
249
+ locales_path: 'src/locales',
250
+ key_separator: '.',
251
+ color: 'primary',
252
+ description: '',
253
+ })
254
+
255
+ const colorOptions = computed(() => [
256
+ { label: t('projects.color_blue', 'Blue (primary)'), value: 'primary' },
257
+ { label: t('projects.color_green', 'Green'), value: 'green' },
258
+ { label: t('projects.color_orange', 'Orange'), value: 'orange' },
259
+ { label: t('projects.color_red', 'Red'), value: 'red' },
260
+ { label: t('projects.color_purple', 'Purple'), value: 'purple' },
261
+ { label: t('projects.color_pink', 'Pink'), value: 'pink' },
262
+ { label: t('projects.color_yellow', 'Yellow'), value: 'yellow' },
263
+ { label: t('projects.color_gray', 'Gray'), value: 'gray' },
264
+ ])
265
+
266
+ const {
267
+ pending,
268
+ saving,
269
+ createProject,
270
+ updateProject,
271
+ deleting,
272
+ deleteProject: doDeleteProject,
273
+ scanning,
274
+ scanProject,
275
+ syncing,
276
+ syncProject,
277
+ visibleProjects: userProjects,
278
+ } = useProject()
279
+
280
+
281
+ function openAdd() {
282
+ editingProject.value = null
283
+ form.value = { name: '', root_path: '', source_url: '', locales_path: 'src/locales', key_separator: '.', color: 'primary', description: '' }
284
+ showModal.value = true
285
+ }
286
+
287
+ function openEdit(project: any) {
288
+ editingProject.value = project
289
+ form.value = {
290
+ name: project.name,
291
+ root_path: project.root_path || '',
292
+ source_url: project.source_url || '',
293
+ locales_path: project.locales_path,
294
+ key_separator: project.key_separator,
295
+ color: project.color || 'primary',
296
+ description: project.description || '',
297
+ }
298
+ showModal.value = true
299
+ }
300
+
301
+ function projectActions(project: any) {
302
+ return [
303
+ [
304
+ { label: t('common.edit', 'Edit'), icon: 'i-heroicons-pencil', onSelect: () => openEdit(project) },
305
+ {
306
+ label: t('common.delete', 'Delete'),
307
+ icon: 'i-heroicons-trash',
308
+ color: 'error' as const,
309
+ onSelect: () => { deletingProject.value = project; showDeleteConfirm.value = true },
310
+ },
311
+ ],
312
+ ]
313
+ }
314
+
315
+ async function saveProject() {
316
+ if (!form.value.name) return
317
+ if (editingProject.value) {
318
+ const ok = await updateProject(editingProject.value.id, form.value)
319
+ if (ok) showModal.value = false
320
+ } else {
321
+ const newProject = await createProject(form.value)
322
+ if (newProject) {
323
+ showModal.value = false
324
+ await router.push(`/projects/${newProject.id}`)
325
+ }
326
+ }
327
+ }
328
+
329
+ async function deleteProject() {
330
+ if (!deletingProject.value) return
331
+ const ok = await doDeleteProject(deletingProject.value.id)
332
+ if (ok) showDeleteConfirm.value = false
333
+ }
334
+ </script>