1
1
<script lang="ts" setup>
2
2
import { ref , computed } from ' vue'
3
3
import { useHead } from ' @vueuse/head'
4
+ import { Line , Bar , Doughnut } from ' vue-chartjs'
5
+ import { Chart as ChartJS , Title , Tooltip , Legend , BarElement , CategoryScale , LinearScale , PointElement , LineElement , ArcElement } from ' chart.js'
6
+
7
+ // Register ChartJS components
8
+ ChartJS .register (Title , Tooltip , Legend , BarElement , CategoryScale , LinearScale , PointElement , LineElement , ArcElement )
4
9
5
10
useHead ({
6
11
title: ' Dashboard - Blog Tags' ,
7
12
})
8
13
14
+ // Chart options
15
+ const lineChartOptions = {
16
+ responsive: true ,
17
+ maintainAspectRatio: false ,
18
+ scales: {
19
+ y: {
20
+ beginAtZero: true ,
21
+ ticks: {
22
+ precision: 0
23
+ }
24
+ }
25
+ },
26
+ plugins: {
27
+ legend: {
28
+ position: ' top' as const ,
29
+ }
30
+ }
31
+ }
32
+
33
+ const barChartOptions = {
34
+ responsive: true ,
35
+ maintainAspectRatio: false ,
36
+ scales: {
37
+ y: {
38
+ beginAtZero: true ,
39
+ ticks: {
40
+ precision: 0
41
+ }
42
+ }
43
+ },
44
+ plugins: {
45
+ legend: {
46
+ position: ' top' as const ,
47
+ }
48
+ }
49
+ }
50
+
51
+ const doughnutChartOptions = {
52
+ responsive: true ,
53
+ maintainAspectRatio: false ,
54
+ plugins: {
55
+ legend: {
56
+ position: ' top' as const ,
57
+ }
58
+ }
59
+ }
60
+
61
+ // Time range selector
62
+ const timeRanges = [
63
+ { label: ' Today' , value: ' today' },
64
+ { label: ' Last 7 days' , value: ' 7days' },
65
+ { label: ' Last 30 days' , value: ' 30days' },
66
+ { label: ' Last 90 days' , value: ' 90days' },
67
+ { label: ' Last year' , value: ' year' },
68
+ { label: ' All time' , value: ' all' }
69
+ ]
70
+ const selectedTimeRange = ref (' 30days' )
71
+
9
72
// Sample tags data
10
73
const tags = ref ([
11
74
{
@@ -268,16 +331,94 @@ const filteredTags = computed(() => {
268
331
})
269
332
})
270
333
334
+ // Monthly chart data
335
+ const monthlyChartData = computed (() => {
336
+ // Generate sample data for tag growth over time
337
+ const tagGrowthData = {
338
+ labels: [' Jan' , ' Feb' , ' Mar' , ' Apr' , ' May' , ' Jun' , ' Jul' , ' Aug' , ' Sep' , ' Oct' , ' Nov' , ' Dec' ],
339
+ datasets: [
340
+ {
341
+ label: ' New Tags' ,
342
+ backgroundColor: ' rgba(75, 192, 192, 0.2)' ,
343
+ borderColor: ' rgba(75, 192, 192, 1)' ,
344
+ borderWidth: 2 ,
345
+ pointBackgroundColor: ' rgba(75, 192, 192, 1)' ,
346
+ data: [2 , 3 , 1 , 4 , 2 , 5 , 3 , 2 , 4 , 6 , 3 , 2 ]
347
+ }
348
+ ]
349
+ }
350
+
351
+ // Generate sample data for posts per tag
352
+ const postsPerTagData = {
353
+ labels: tags .value .slice (0 , 8 ).map (tag => tag .name ),
354
+ datasets: [
355
+ {
356
+ label: ' Posts' ,
357
+ backgroundColor: ' rgba(54, 162, 235, 0.6)' ,
358
+ borderColor: ' rgba(54, 162, 235, 1)' ,
359
+ borderWidth: 1 ,
360
+ data: tags .value .slice (0 , 8 ).map (tag => tag .postCount )
361
+ }
362
+ ]
363
+ }
364
+
365
+ // Generate sample data for tag distribution
366
+ const tagColors = [
367
+ ' rgba(255, 99, 132, 0.6)' ,
368
+ ' rgba(54, 162, 235, 0.6)' ,
369
+ ' rgba(255, 206, 86, 0.6)' ,
370
+ ' rgba(75, 192, 192, 0.6)' ,
371
+ ' rgba(153, 102, 255, 0.6)' ,
372
+ ' rgba(255, 159, 64, 0.6)' ,
373
+ ' rgba(199, 199, 199, 0.6)' ,
374
+ ' rgba(83, 102, 255, 0.6)'
375
+ ]
376
+
377
+ const tagDistributionData = {
378
+ labels: tags .value .slice (0 , 8 ).map (tag => tag .name ),
379
+ datasets: [
380
+ {
381
+ backgroundColor: tagColors ,
382
+ borderColor: tagColors .map (color => color .replace (' 0.6' , ' 1' )),
383
+ borderWidth: 1 ,
384
+ data: tags .value .slice (0 , 8 ).map (tag => tag .postCount )
385
+ }
386
+ ]
387
+ }
388
+
389
+ return {
390
+ tagGrowthChartData: tagGrowthData ,
391
+ postsPerTagChartData: postsPerTagData ,
392
+ tagDistributionChartData: tagDistributionData
393
+ }
394
+ })
395
+
271
396
// Tag statistics
272
397
const tagStats = computed (() => {
273
398
const totalTags = tags .value .length
274
399
const totalPosts = tags .value .reduce ((sum , tag ) => sum + tag .postCount , 0 )
275
400
const mostUsedTag = [... tags .value ].sort ((a , b ) => b .postCount - a .postCount )[0 ]
401
+ const leastUsedTag = [... tags .value ].sort ((a , b ) => a .postCount - b .postCount )[0 ]
402
+ const avgPostsPerTag = totalPosts / totalTags
403
+ const topTagPercentage = mostUsedTag ? Math .round ((mostUsedTag .postCount / totalPosts ) * 100 ) : 0
404
+
405
+ // Find newest tag
406
+ let newestTag = tags .value [0 ] || { name: ' None' , createdAt: ' ' }
407
+
408
+ for (const tag of tags .value ) {
409
+ if (new Date (tag .createdAt ) > new Date (newestTag .createdAt )) {
410
+ newestTag = tag
411
+ }
412
+ }
276
413
277
414
return {
278
415
totalTags ,
279
416
totalPosts ,
280
- mostUsedTag
417
+ mostUsedTag ,
418
+ leastUsedTag ,
419
+ avgPostsPerTag: avgPostsPerTag .toFixed (1 ),
420
+ topTagPercentage ,
421
+ newestTag
281
422
}
282
423
})
283
424
@@ -457,8 +598,20 @@ const paginationRange = computed(() => {
457
598
</button >
458
599
</div >
459
600
460
- <!-- Tag Statistics -->
461
- <div class =" grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3" >
601
+ <!-- Time Range Selector -->
602
+ <div class =" flex justify-end" >
603
+ <div class =" relative inline-block w-full sm:w-auto" >
604
+ <select
605
+ v-model =" selectedTimeRange"
606
+ class =" block w-full rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6 dark:bg-blue-gray-800 dark:text-white dark:ring-gray-700"
607
+ >
608
+ <option v-for =" range in timeRanges" :key =" range.value" :value =" range.value" >{{ range.label }}</option >
609
+ </select >
610
+ </div >
611
+ </div >
612
+
613
+ <!-- Enhanced Tag Statistics -->
614
+ <div class =" grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-4" >
462
615
<!-- Total Tags -->
463
616
<div class =" bg-white dark:bg-gray-800 overflow-hidden shadow rounded-lg" >
464
617
<div class =" px-4 py-5 sm:p-6" >
@@ -477,6 +630,9 @@ const paginationRange = computed(() => {
477
630
<div class =" text-lg font-medium text-gray-900 dark:text-white" >
478
631
{{ tagStats.totalTags }}
479
632
</div >
633
+ <div class =" mt-1 text-sm text-green-600 dark:text-green-400" >
634
+ <span >{{ tagStats.newestTag.name }} added recently</span >
635
+ </div >
480
636
</dd >
481
637
</dl >
482
638
</div >
@@ -502,6 +658,9 @@ const paginationRange = computed(() => {
502
658
<div class =" text-lg font-medium text-gray-900 dark:text-white" >
503
659
{{ tagStats.totalPosts }}
504
660
</div >
661
+ <div class =" mt-1 text-sm text-gray-500 dark:text-gray-400" >
662
+ <span >{{ tagStats.avgPostsPerTag }} avg per tag</span >
663
+ </div >
505
664
</dd >
506
665
</dl >
507
666
</div >
@@ -526,9 +685,37 @@ const paginationRange = computed(() => {
526
685
<dd >
527
686
<div class =" text-lg font-medium text-gray-900 dark:text-white" >
528
687
{{ tagStats.mostUsedTag ? tagStats.mostUsedTag.name : 'None' }}
529
- <span v-if =" tagStats.mostUsedTag" class =" text-sm text-gray-500 dark:text-gray-400" >
530
- ({{ tagStats.mostUsedTag.postCount }} posts)
531
- </span >
688
+ </div >
689
+ <div class =" mt-1 text-sm text-green-600 dark:text-green-400" >
690
+ <span v-if =" tagStats.mostUsedTag" >{{ tagStats.mostUsedTag.postCount }} posts ({{ tagStats.topTagPercentage }}%)</span >
691
+ </div >
692
+ </dd >
693
+ </dl >
694
+ </div >
695
+ </div >
696
+ </div >
697
+ </div >
698
+
699
+ <!-- Least Used Tag -->
700
+ <div class =" bg-white dark:bg-gray-800 overflow-hidden shadow rounded-lg" >
701
+ <div class =" px-4 py-5 sm:p-6" >
702
+ <div class =" flex items-center" >
703
+ <div class =" flex-shrink-0 bg-blue-500 rounded-md p-3" >
704
+ <svg class =" h-6 w-6 text-white" xmlns =" http://www.w3.org/2000/svg" fill =" none" viewBox =" 0 0 24 24" stroke =" currentColor" >
705
+ <path stroke-linecap =" round" stroke-linejoin =" round" stroke-width =" 2" d =" M13 10V3L4 14h7v7l9-11h-7z" />
706
+ </svg >
707
+ </div >
708
+ <div class =" ml-5 w-0 flex-1" >
709
+ <dl >
710
+ <dt class =" text-sm font-medium text-gray-500 dark:text-gray-400 truncate" >
711
+ Least Used Tag
712
+ </dt >
713
+ <dd >
714
+ <div class =" text-lg font-medium text-gray-900 dark:text-white" >
715
+ {{ tagStats.leastUsedTag ? tagStats.leastUsedTag.name : 'None' }}
716
+ </div >
717
+ <div class =" mt-1 text-sm text-gray-500 dark:text-gray-400" >
718
+ <span v-if =" tagStats.leastUsedTag" >{{ tagStats.leastUsedTag.postCount }} posts</span >
532
719
</div >
533
720
</dd >
534
721
</dl >
@@ -538,6 +725,39 @@ const paginationRange = computed(() => {
538
725
</div >
539
726
</div >
540
727
728
+ <!-- Charts -->
729
+ <div class =" grid grid-cols-1 gap-5 lg:grid-cols-3" >
730
+ <!-- Tag Growth Chart -->
731
+ <div class =" bg-white dark:bg-gray-800 overflow-hidden shadow rounded-lg" >
732
+ <div class =" px-4 py-5 sm:p-6" >
733
+ <h3 class =" text-base font-semibold text-gray-900 dark:text-white" >Tag Growth</h3 >
734
+ <div class =" mt-4" style =" height : 250px ;" >
735
+ <Line :data =" monthlyChartData.tagGrowthChartData" :options =" lineChartOptions" />
736
+ </div >
737
+ </div >
738
+ </div >
739
+
740
+ <!-- Posts Per Tag Chart -->
741
+ <div class =" bg-white dark:bg-gray-800 overflow-hidden shadow rounded-lg" >
742
+ <div class =" px-4 py-5 sm:p-6" >
743
+ <h3 class =" text-base font-semibold text-gray-900 dark:text-white" >Posts Per Tag</h3 >
744
+ <div class =" mt-4" style =" height : 250px ;" >
745
+ <Bar :data =" monthlyChartData.postsPerTagChartData" :options =" barChartOptions" />
746
+ </div >
747
+ </div >
748
+ </div >
749
+
750
+ <!-- Tag Distribution Chart -->
751
+ <div class =" bg-white dark:bg-gray-800 overflow-hidden shadow rounded-lg" >
752
+ <div class =" px-4 py-5 sm:p-6" >
753
+ <h3 class =" text-base font-semibold text-gray-900 dark:text-white" >Tag Distribution</h3 >
754
+ <div class =" mt-4" style =" height : 250px ;" >
755
+ <Doughnut :data =" monthlyChartData.tagDistributionChartData" :options =" doughnutChartOptions" />
756
+ </div >
757
+ </div >
758
+ </div >
759
+ </div >
760
+
541
761
<!-- Filters and Search -->
542
762
<div class =" mt-6 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4" >
543
763
<div class =" relative max-w-sm" >
@@ -584,6 +804,12 @@ const paginationRange = computed(() => {
584
804
585
805
<!-- Tags Table -->
586
806
<div class =" mt-6 flow-root" >
807
+ <div class =" sm:flex sm:items-center sm:justify-between mb-4" >
808
+ <h3 class =" text-base font-semibold leading-6 text-gray-900 dark:text-white" >All Tags</h3 >
809
+ <p class =" mt-1 text-sm text-gray-500 dark:text-gray-400" >
810
+ A list of all blog tags including name, description, and post count.
811
+ </p >
812
+ </div >
587
813
<div class =" -mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8" >
588
814
<div class =" inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8" >
589
815
<div class =" overflow-hidden shadow ring-1 ring-black ring-opacity-5 sm:rounded-lg" >
0 commit comments