i18n-dashboard 0.6.1 → 0.6.2
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 +5 -2
- package/composables/useProfile.ts +7 -6
- package/package.json +1 -1
- package/pages/users/[id]/profile.vue +35 -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
|
@@ -314,8 +314,11 @@
|
|
|
314
314
|
"profile.name_label": "Full name",
|
|
315
315
|
"profile.name_placeholder": "John Doe",
|
|
316
316
|
"profile.total_translations": "Total translations",
|
|
317
|
-
"profile.
|
|
318
|
-
"profile.
|
|
317
|
+
"profile.period_1d": "Last 24 hours",
|
|
318
|
+
"profile.period_7d": "Last 7 days",
|
|
319
|
+
"profile.period_30d": "Last 30 days",
|
|
320
|
+
"profile.period_365d": "Last year",
|
|
321
|
+
"profile.period_all": "Since account creation",
|
|
319
322
|
"profile.general": "General",
|
|
320
323
|
"profile.name_email_required": "Name and email are required",
|
|
321
324
|
"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,8 @@
|
|
|
4
4
|
<!-- Loading -->
|
|
5
5
|
<div v-if="pending" class="space-y-4">
|
|
6
6
|
<USkeleton class="h-24" />
|
|
7
|
-
<div class="grid grid-cols-
|
|
8
|
-
<USkeleton v-for="i in
|
|
7
|
+
<div class="grid grid-cols-2 gap-4">
|
|
8
|
+
<USkeleton v-for="i in 2" :key="i" class="h-20" />
|
|
9
9
|
</div>
|
|
10
10
|
</div>
|
|
11
11
|
|
|
@@ -51,18 +51,27 @@
|
|
|
51
51
|
</div>
|
|
52
52
|
|
|
53
53
|
<!-- Stats -->
|
|
54
|
-
<div class="
|
|
55
|
-
<
|
|
56
|
-
<
|
|
57
|
-
<div class="
|
|
58
|
-
<
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
<
|
|
62
|
-
|
|
54
|
+
<div class="flex items-center justify-between gap-4">
|
|
55
|
+
<div class="grid grid-cols-2 gap-4 flex-1">
|
|
56
|
+
<UCard v-for="stat in statCards" :key="stat.label">
|
|
57
|
+
<div class="flex items-center gap-3">
|
|
58
|
+
<div class="p-2 rounded-lg" :class="stat.bg">
|
|
59
|
+
<UIcon :name="stat.icon" class="text-xl" :class="stat.color" />
|
|
60
|
+
</div>
|
|
61
|
+
<div>
|
|
62
|
+
<p class="text-xs text-gray-500 dark:text-gray-400">{{ stat.label }}</p>
|
|
63
|
+
<p class="text-2xl font-bold text-gray-900 dark:text-white">{{ stat.value }}</p>
|
|
64
|
+
</div>
|
|
63
65
|
</div>
|
|
64
|
-
</
|
|
65
|
-
</
|
|
66
|
+
</UCard>
|
|
67
|
+
</div>
|
|
68
|
+
<USelect
|
|
69
|
+
v-model="period"
|
|
70
|
+
:items="periodOptions"
|
|
71
|
+
class="w-52 shrink-0"
|
|
72
|
+
value-key="value"
|
|
73
|
+
label-key="label"
|
|
74
|
+
/>
|
|
66
75
|
</div>
|
|
67
76
|
|
|
68
77
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 items-start">
|
|
@@ -258,7 +267,7 @@ const { projects } = useProject()
|
|
|
258
267
|
const { t } = useT()
|
|
259
268
|
|
|
260
269
|
const userId = computed(() => Number(route.params.id))
|
|
261
|
-
const { profile, pending, refresh, editSaving: selfEditSaving, editError, updateProfile, rolesSaving, saveRoles } = useProfile(userId)
|
|
270
|
+
const { profile, period, pending, refresh, editSaving: selfEditSaving, editError, updateProfile, rolesSaving, saveRoles } = useProfile(userId)
|
|
262
271
|
|
|
263
272
|
const isSelf = computed(() => currentUser.value?.id === userId.value)
|
|
264
273
|
|
|
@@ -355,6 +364,16 @@ async function doSaveRoles() {
|
|
|
355
364
|
}
|
|
356
365
|
|
|
357
366
|
// ── Stats ──────────────────────────────────────────────────────────────────────
|
|
367
|
+
const periodOptions = computed(() => [
|
|
368
|
+
{ label: t('profile.period_1d', 'Last 24 hours'), value: '1d' },
|
|
369
|
+
{ label: t('profile.period_7d', 'Last 7 days'), value: '7d' },
|
|
370
|
+
{ label: t('profile.period_30d', 'Last 30 days'), value: '30d' },
|
|
371
|
+
{ label: t('profile.period_365d', 'Last year'), value: '365d' },
|
|
372
|
+
{ label: t('profile.period_all', 'Since account creation'), value: 'all' },
|
|
373
|
+
])
|
|
374
|
+
|
|
375
|
+
const periodLabel = computed(() => periodOptions.value.find(o => o.value === period.value)?.label ?? '')
|
|
376
|
+
|
|
358
377
|
const statCards = computed(() => [
|
|
359
378
|
{
|
|
360
379
|
label: t('profile.total_translations', 'Total translations'),
|
|
@@ -364,19 +383,12 @@ const statCards = computed(() => [
|
|
|
364
383
|
bg: 'bg-primary-50 dark:bg-primary-900/20',
|
|
365
384
|
},
|
|
366
385
|
{
|
|
367
|
-
label:
|
|
368
|
-
value: profile.value?.stats.
|
|
386
|
+
label: periodLabel.value,
|
|
387
|
+
value: profile.value?.stats.periodCount ?? 0,
|
|
369
388
|
icon: 'i-heroicons-calendar-days',
|
|
370
389
|
color: 'text-blue-600',
|
|
371
390
|
bg: 'bg-blue-50 dark:bg-blue-900/20',
|
|
372
391
|
},
|
|
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
392
|
])
|
|
381
393
|
|
|
382
394
|
// ── 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
|
|