i18n-dashboard 0.6.1 → 0.6.3
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/assets/locales/en.json +6 -2
- package/composables/useProfile.ts +7 -6
- package/package.json +1 -1
- package/pages/users/[id]/profile.vue +44 -23
- package/server/api/profile.get.ts +20 -8
- package/server/api/translations/index.post.ts +1 -1
- package/server/api/users/[id]/profile.get.ts +21 -8
- package/server/interfaces/profile.interface.ts +4 -2
- package/services/profile.service.ts +5 -5
package/assets/locales/en.json
CHANGED
|
@@ -313,9 +313,13 @@
|
|
|
313
313
|
"profile.edit_modal_title": "Edit account",
|
|
314
314
|
"profile.name_label": "Full name",
|
|
315
315
|
"profile.name_placeholder": "John Doe",
|
|
316
|
+
"profile.stats_title": "Activity",
|
|
316
317
|
"profile.total_translations": "Total translations",
|
|
317
|
-
"profile.
|
|
318
|
-
"profile.
|
|
318
|
+
"profile.period_1d": "Last 24 hours",
|
|
319
|
+
"profile.period_7d": "Last 7 days",
|
|
320
|
+
"profile.period_30d": "Last 30 days",
|
|
321
|
+
"profile.period_365d": "Last year",
|
|
322
|
+
"profile.period_all": "Since account creation",
|
|
319
323
|
"profile.general": "General",
|
|
320
324
|
"profile.name_email_required": "Name and email are required",
|
|
321
325
|
"dashboard.title": "Dashboard",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { profileService } from '../services/profile.service'
|
|
2
2
|
import { authService } from '../services/auth.service'
|
|
3
3
|
import { userService } from '../services/user.service'
|
|
4
|
+
import type { ProfilePeriod } from '../server/interfaces/profile.interface'
|
|
4
5
|
|
|
5
6
|
export function useProfile(userId?: MaybeRefOrGetter<number | string>) {
|
|
6
7
|
const toast = useToast()
|
|
@@ -8,16 +9,15 @@ export function useProfile(userId?: MaybeRefOrGetter<number | string>) {
|
|
|
8
9
|
const { fetchMe } = useAuth()
|
|
9
10
|
|
|
10
11
|
// ── Profile data ─────────────────────────────────────────────────────────
|
|
11
|
-
// Load the target user's profile (or own profile if no userId provided)
|
|
12
|
-
|
|
13
12
|
const targetId = computed(() => userId ? toValue(userId) : null)
|
|
13
|
+
const period = ref<ProfilePeriod>('all')
|
|
14
14
|
|
|
15
15
|
const { data: profile, pending, refresh } = useAsyncData(
|
|
16
|
-
() => targetId.value ? `user-profile-${targetId.value}` :
|
|
16
|
+
() => targetId.value ? `user-profile-${targetId.value}-${period.value}` : `user-profile-${period.value}`,
|
|
17
17
|
() => targetId.value
|
|
18
|
-
? profileService.getUserProfile(targetId.value)
|
|
19
|
-
: profileService.getProfile(),
|
|
20
|
-
{ watch: [targetId] },
|
|
18
|
+
? profileService.getUserProfile(targetId.value, period.value)
|
|
19
|
+
: profileService.getProfile(period.value),
|
|
20
|
+
{ watch: [targetId, period] },
|
|
21
21
|
)
|
|
22
22
|
|
|
23
23
|
// ── Own account editing (current logged-in user) ─────────────────────────
|
|
@@ -66,6 +66,7 @@ export function useProfile(userId?: MaybeRefOrGetter<number | string>) {
|
|
|
66
66
|
|
|
67
67
|
return {
|
|
68
68
|
profile,
|
|
69
|
+
period,
|
|
69
70
|
pending,
|
|
70
71
|
refresh,
|
|
71
72
|
editSaving,
|
package/package.json
CHANGED
|
@@ -4,8 +4,14 @@
|
|
|
4
4
|
<!-- Loading -->
|
|
5
5
|
<div v-if="pending" class="space-y-4">
|
|
6
6
|
<USkeleton class="h-24" />
|
|
7
|
-
<div class="
|
|
8
|
-
<
|
|
7
|
+
<div class="space-y-3">
|
|
8
|
+
<div class="flex items-center justify-between">
|
|
9
|
+
<USkeleton class="h-4 w-16" />
|
|
10
|
+
<USkeleton class="h-8 w-52" />
|
|
11
|
+
</div>
|
|
12
|
+
<div class="grid grid-cols-2 gap-4">
|
|
13
|
+
<USkeleton v-for="i in 2" :key="i" class="h-20" />
|
|
14
|
+
</div>
|
|
9
15
|
</div>
|
|
10
16
|
</div>
|
|
11
17
|
|
|
@@ -51,18 +57,30 @@
|
|
|
51
57
|
</div>
|
|
52
58
|
|
|
53
59
|
<!-- Stats -->
|
|
54
|
-
<div class="
|
|
55
|
-
<
|
|
56
|
-
<
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
<div class="space-y-3">
|
|
61
|
+
<div class="flex items-center justify-between gap-4">
|
|
62
|
+
<p class="text-xs font-semibold text-gray-400 uppercase tracking-wide">{{ t('profile.stats_title', 'Activity') }}</p>
|
|
63
|
+
<USelect
|
|
64
|
+
v-model="period"
|
|
65
|
+
:items="periodOptions"
|
|
66
|
+
class="w-52"
|
|
67
|
+
value-key="value"
|
|
68
|
+
label-key="label"
|
|
69
|
+
/>
|
|
70
|
+
</div>
|
|
71
|
+
<div class="grid grid-cols-2 gap-4">
|
|
72
|
+
<UCard v-for="stat in statCards" :key="stat.label">
|
|
73
|
+
<div class="flex items-center gap-3">
|
|
74
|
+
<div class="p-2 rounded-lg" :class="stat.bg">
|
|
75
|
+
<UIcon :name="stat.icon" class="text-xl" :class="stat.color" />
|
|
76
|
+
</div>
|
|
77
|
+
<div>
|
|
78
|
+
<p class="text-xs text-gray-500 dark:text-gray-400">{{ stat.label }}</p>
|
|
79
|
+
<p class="text-2xl font-bold text-gray-900 dark:text-white">{{ stat.value }}</p>
|
|
80
|
+
</div>
|
|
63
81
|
</div>
|
|
64
|
-
</
|
|
65
|
-
</
|
|
82
|
+
</UCard>
|
|
83
|
+
</div>
|
|
66
84
|
</div>
|
|
67
85
|
|
|
68
86
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 items-start">
|
|
@@ -258,7 +276,7 @@ const { projects } = useProject()
|
|
|
258
276
|
const { t } = useT()
|
|
259
277
|
|
|
260
278
|
const userId = computed(() => Number(route.params.id))
|
|
261
|
-
const { profile, pending, refresh, editSaving: selfEditSaving, editError, updateProfile, rolesSaving, saveRoles } = useProfile(userId)
|
|
279
|
+
const { profile, period, pending, refresh, editSaving: selfEditSaving, editError, updateProfile, rolesSaving, saveRoles } = useProfile(userId)
|
|
262
280
|
|
|
263
281
|
const isSelf = computed(() => currentUser.value?.id === userId.value)
|
|
264
282
|
|
|
@@ -355,6 +373,16 @@ async function doSaveRoles() {
|
|
|
355
373
|
}
|
|
356
374
|
|
|
357
375
|
// ── Stats ──────────────────────────────────────────────────────────────────────
|
|
376
|
+
const periodOptions = computed(() => [
|
|
377
|
+
{ label: t('profile.period_1d', 'Last 24 hours'), value: '1d' },
|
|
378
|
+
{ label: t('profile.period_7d', 'Last 7 days'), value: '7d' },
|
|
379
|
+
{ label: t('profile.period_30d', 'Last 30 days'), value: '30d' },
|
|
380
|
+
{ label: t('profile.period_365d', 'Last year'), value: '365d' },
|
|
381
|
+
{ label: t('profile.period_all', 'Since account creation'), value: 'all' },
|
|
382
|
+
])
|
|
383
|
+
|
|
384
|
+
const periodLabel = computed(() => periodOptions.value.find(o => o.value === period.value)?.label ?? '')
|
|
385
|
+
|
|
358
386
|
const statCards = computed(() => [
|
|
359
387
|
{
|
|
360
388
|
label: t('profile.total_translations', 'Total translations'),
|
|
@@ -364,19 +392,12 @@ const statCards = computed(() => [
|
|
|
364
392
|
bg: 'bg-primary-50 dark:bg-primary-900/20',
|
|
365
393
|
},
|
|
366
394
|
{
|
|
367
|
-
label:
|
|
368
|
-
value: profile.value?.stats.
|
|
395
|
+
label: periodLabel.value,
|
|
396
|
+
value: profile.value?.stats.periodCount ?? 0,
|
|
369
397
|
icon: 'i-heroicons-calendar-days',
|
|
370
398
|
color: 'text-blue-600',
|
|
371
399
|
bg: 'bg-blue-50 dark:bg-blue-900/20',
|
|
372
400
|
},
|
|
373
|
-
{
|
|
374
|
-
label: t('profile.this_week', 'This week'),
|
|
375
|
-
value: profile.value?.stats.thisWeek ?? 0,
|
|
376
|
-
icon: 'i-heroicons-bolt',
|
|
377
|
-
color: 'text-green-600',
|
|
378
|
-
bg: 'bg-green-50 dark:bg-green-900/20',
|
|
379
|
-
},
|
|
380
401
|
])
|
|
381
402
|
|
|
382
403
|
// ── Languages grouped by project ───────────────────────────────────────────────
|
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
import { getDb } from '../db/index'
|
|
2
|
-
import type { UserProfile } from '../interfaces/profile.interface'
|
|
2
|
+
import type { UserProfile, ProfilePeriod } from '../interfaces/profile.interface'
|
|
3
|
+
|
|
4
|
+
const PERIOD_MS: Record<ProfilePeriod, number | null> = {
|
|
5
|
+
'1d': 24 * 60 * 60 * 1000,
|
|
6
|
+
'7d': 7 * 24 * 60 * 60 * 1000,
|
|
7
|
+
'30d': 30 * 24 * 60 * 60 * 1000,
|
|
8
|
+
'365d': 365 * 24 * 60 * 60 * 1000,
|
|
9
|
+
'all': null,
|
|
10
|
+
}
|
|
3
11
|
|
|
4
12
|
export default defineEventHandler(async (event): Promise<UserProfile> => {
|
|
5
13
|
const user = event.context.user
|
|
14
|
+
const query = getQuery(event)
|
|
15
|
+
const period = (query.period as ProfilePeriod) || 'all'
|
|
6
16
|
const db = getDb()
|
|
7
17
|
|
|
8
18
|
// ── Roles with project info ───────────────────────────────────────────────
|
|
@@ -31,13 +41,15 @@ export default defineEventHandler(async (event): Promise<UserProfile> => {
|
|
|
31
41
|
}
|
|
32
42
|
|
|
33
43
|
// ── Stats ─────────────────────────────────────────────────────────────────
|
|
34
|
-
const
|
|
35
|
-
const
|
|
44
|
+
const ms = PERIOD_MS[period] ?? null
|
|
45
|
+
const since = ms ? new Date(Date.now() - ms).toISOString() : null
|
|
46
|
+
|
|
47
|
+
const baseQ = db('translation_history').where('changed_by', user.name)
|
|
48
|
+
const periodQ = since ? db('translation_history').where('changed_by', user.name).where('changed_at', '>=', since) : baseQ.clone()
|
|
36
49
|
|
|
37
|
-
const [countTotal,
|
|
50
|
+
const [countTotal, countPeriod] = await Promise.all([
|
|
38
51
|
db('translation_history').where('changed_by', user.name).count('id as n').first(),
|
|
39
|
-
|
|
40
|
-
db('translation_history').where('changed_by', user.name).where('changed_at', '>=', monthAgo).count('id as n').first(),
|
|
52
|
+
periodQ.count('id as n').first(),
|
|
41
53
|
])
|
|
42
54
|
|
|
43
55
|
// ── Recent translations ───────────────────────────────────────────────────
|
|
@@ -72,8 +84,8 @@ export default defineEventHandler(async (event): Promise<UserProfile> => {
|
|
|
72
84
|
roles,
|
|
73
85
|
stats: {
|
|
74
86
|
total: Number(countTotal?.n ?? 0),
|
|
75
|
-
|
|
76
|
-
|
|
87
|
+
periodCount: Number(countPeriod?.n ?? 0),
|
|
88
|
+
period,
|
|
77
89
|
},
|
|
78
90
|
languages,
|
|
79
91
|
recentTranslations,
|
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
import { getDb } from '../../../db/index'
|
|
2
2
|
import { getUserRole } from '../../../utils/auth.util'
|
|
3
|
-
import type { UserProfile } from '../../../interfaces/profile.interface'
|
|
3
|
+
import type { UserProfile, ProfilePeriod } from '../../../interfaces/profile.interface'
|
|
4
|
+
|
|
5
|
+
const PERIOD_MS: Record<ProfilePeriod, number | null> = {
|
|
6
|
+
'1d': 24 * 60 * 60 * 1000,
|
|
7
|
+
'7d': 7 * 24 * 60 * 60 * 1000,
|
|
8
|
+
'30d': 30 * 24 * 60 * 60 * 1000,
|
|
9
|
+
'365d': 365 * 24 * 60 * 60 * 1000,
|
|
10
|
+
'all': null,
|
|
11
|
+
}
|
|
4
12
|
|
|
5
13
|
export default defineEventHandler(async (event): Promise<UserProfile> => {
|
|
6
14
|
const currentUser = event.context.user
|
|
7
15
|
const targetId = Number(getRouterParam(event, 'id'))
|
|
16
|
+
const query = getQuery(event)
|
|
17
|
+
const period = (query.period as ProfilePeriod) || 'all'
|
|
8
18
|
const db = getDb()
|
|
9
19
|
|
|
10
20
|
const target = await db('users')
|
|
@@ -56,13 +66,16 @@ export default defineEventHandler(async (event): Promise<UserProfile> => {
|
|
|
56
66
|
}
|
|
57
67
|
|
|
58
68
|
// ── Stats ──────────────────────────────────────────────────────────────────
|
|
59
|
-
const
|
|
60
|
-
const
|
|
69
|
+
const ms = PERIOD_MS[period] ?? null
|
|
70
|
+
const since = ms ? new Date(Date.now() - ms).toISOString() : null
|
|
71
|
+
|
|
72
|
+
const periodQ = since
|
|
73
|
+
? db('translation_history').where('changed_by', target.name).where('changed_at', '>=', since)
|
|
74
|
+
: db('translation_history').where('changed_by', target.name)
|
|
61
75
|
|
|
62
|
-
const [countTotal,
|
|
76
|
+
const [countTotal, countPeriod] = await Promise.all([
|
|
63
77
|
db('translation_history').where('changed_by', target.name).count('id as n').first(),
|
|
64
|
-
|
|
65
|
-
db('translation_history').where('changed_by', target.name).where('changed_at', '>=', monthAgo).count('id as n').first(),
|
|
78
|
+
periodQ.count('id as n').first(),
|
|
66
79
|
])
|
|
67
80
|
|
|
68
81
|
// ── Recent translations ────────────────────────────────────────────────────
|
|
@@ -98,8 +111,8 @@ export default defineEventHandler(async (event): Promise<UserProfile> => {
|
|
|
98
111
|
roles,
|
|
99
112
|
stats: {
|
|
100
113
|
total: Number(countTotal?.n ?? 0),
|
|
101
|
-
|
|
102
|
-
|
|
114
|
+
periodCount: Number(countPeriod?.n ?? 0),
|
|
115
|
+
period,
|
|
103
116
|
},
|
|
104
117
|
languages,
|
|
105
118
|
recentTranslations,
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { BaseService } from './base.service'
|
|
2
|
-
import type { UserProfile } from '../server/interfaces/profile.interface'
|
|
2
|
+
import type { UserProfile, ProfilePeriod } from '../server/interfaces/profile.interface'
|
|
3
3
|
|
|
4
4
|
class ProfileService extends BaseService {
|
|
5
|
-
async getProfile(): Promise<UserProfile> {
|
|
6
|
-
return this.get<UserProfile>('/api/profile')
|
|
5
|
+
async getProfile(period: ProfilePeriod = 'all'): Promise<UserProfile> {
|
|
6
|
+
return this.get<UserProfile>('/api/profile', { query: { period } })
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
async getUserProfile(id: number | string): Promise<UserProfile> {
|
|
10
|
-
return this.get<UserProfile>(`/api/users/${id}/profile
|
|
9
|
+
async getUserProfile(id: number | string, period: ProfilePeriod = 'all'): Promise<UserProfile> {
|
|
10
|
+
return this.get<UserProfile>(`/api/users/${id}/profile`, { query: { period } })
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
13
|
|