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.
@@ -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.this_month": "This month",
318
- "profile.this_week": "This week",
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}` : 'user-profile',
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i18n-dashboard",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "description": "A web dashboard to manage vue-i18n translation keys with database persistence",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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="grid grid-cols-3 gap-4">
8
- <USkeleton v-for="i in 3" :key="i" class="h-20" />
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="grid grid-cols-3 gap-4">
55
- <UCard v-for="stat in statCards" :key="stat.label">
56
- <div class="flex items-center gap-3">
57
- <div class="p-2 rounded-lg" :class="stat.bg">
58
- <UIcon :name="stat.icon" class="text-xl" :class="stat.color" />
59
- </div>
60
- <div>
61
- <p class="text-xs text-gray-500 dark:text-gray-400">{{ stat.label }}</p>
62
- <p class="text-2xl font-bold text-gray-900 dark:text-white">{{ stat.value }}</p>
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
- </div>
65
- </UCard>
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: t('profile.this_month', 'This month'),
368
- value: profile.value?.stats.thisMonth ?? 0,
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 weekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString()
35
- const monthAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString()
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, countWeek, countMonth] = await Promise.all([
50
+ const [countTotal, countPeriod] = await Promise.all([
38
51
  db('translation_history').where('changed_by', user.name).count('id as n').first(),
39
- db('translation_history').where('changed_by', user.name).where('changed_at', '>=', weekAgo).count('id as n').first(),
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
- thisWeek: Number(countWeek?.n ?? 0),
76
- thisMonth: Number(countMonth?.n ?? 0),
87
+ periodCount: Number(countPeriod?.n ?? 0),
88
+ period,
77
89
  },
78
90
  languages,
79
91
  recentTranslations,
@@ -35,7 +35,7 @@ export default defineEventHandler(async (event) => {
35
35
  translation_id: existing.id,
36
36
  old_value: oldValue,
37
37
  new_value: value,
38
- changed_by: 'user',
38
+ changed_by: event.context.user?.name ?? 'user',
39
39
  })
40
40
  }
41
41
 
@@ -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 weekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString()
60
- const monthAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString()
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, countWeek, countMonth] = await Promise.all([
76
+ const [countTotal, countPeriod] = await Promise.all([
63
77
  db('translation_history').where('changed_by', target.name).count('id as n').first(),
64
- db('translation_history').where('changed_by', target.name).where('changed_at', '>=', weekAgo).count('id as n').first(),
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
- thisWeek: Number(countWeek?.n ?? 0),
102
- thisMonth: Number(countMonth?.n ?? 0),
114
+ periodCount: Number(countPeriod?.n ?? 0),
115
+ period,
103
116
  },
104
117
  languages,
105
118
  recentTranslations,
@@ -1,7 +1,9 @@
1
+ export type ProfilePeriod = '1d' | '7d' | '30d' | '365d' | 'all'
2
+
1
3
  export interface ProfileStats {
2
4
  total: number
3
- thisWeek: number
4
- thisMonth: number
5
+ periodCount: number
6
+ period: ProfilePeriod
5
7
  }
6
8
 
7
9
  export interface ProfileRole {
@@ -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