Skip to content

Commit e44575d

Browse files
committed
chore: wip
1 parent 50dd9d8 commit e44575d

File tree

2 files changed

+652
-4
lines changed

2 files changed

+652
-4
lines changed

storage/framework/defaults/views/dashboard/packages/index.vue

Lines changed: 370 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,38 @@
11
<script lang="ts" setup>
2+
import { ref, computed } from 'vue'
3+
import { useHead } from '@vueuse/head'
4+
import { Line, Bar } from 'vue-chartjs'
5+
import {
6+
Chart as ChartJS,
7+
CategoryScale,
8+
LinearScale,
9+
PointElement,
10+
LineElement,
11+
BarElement,
12+
Title,
13+
Tooltip,
14+
Legend,
15+
Filler,
16+
Scale,
17+
ChartOptions,
18+
ScaleOptionsByType,
19+
CartesianScaleTypeRegistry,
20+
CoreScaleOptions,
21+
Tick,
22+
} from 'chart.js'
23+
24+
ChartJS.register(
25+
CategoryScale,
26+
LinearScale,
27+
PointElement,
28+
LineElement,
29+
BarElement,
30+
Title,
31+
Tooltip,
32+
Legend,
33+
Filler,
34+
)
35+
236
useHead({
337
title: 'Dashboard - Packages',
438
})
@@ -163,6 +197,237 @@ const packages = [
163197
]
164198
165199
const getPackageUrl = (pkgName: string) => `https://github.com/stacksjs/${pkgName}`
200+
201+
const colors = [
202+
{ border: 'rgb(59, 130, 246)', background: 'rgba(59, 130, 246, 0.8)' },
203+
{ border: 'rgb(234, 179, 8)', background: 'rgba(234, 179, 8, 0.8)' },
204+
{ border: 'rgb(239, 68, 68)', background: 'rgba(239, 68, 68, 0.8)' },
205+
{ border: 'rgb(16, 185, 129)', background: 'rgba(16, 185, 129, 0.8)' },
206+
{ border: 'rgb(168, 85, 247)', background: 'rgba(168, 85, 247, 0.8)' },
207+
]
208+
209+
// Update the chart options to use proper typing
210+
const baseChartOptions = {
211+
responsive: true,
212+
maintainAspectRatio: false,
213+
scales: {
214+
y: {
215+
type: 'linear' as const,
216+
beginAtZero: true,
217+
min: 0,
218+
grid: {
219+
color: 'rgba(200, 200, 200, 0.1)',
220+
},
221+
ticks: {
222+
color: 'rgb(156, 163, 175)',
223+
font: {
224+
family: "'JetBrains Mono', monospace",
225+
},
226+
callback: function(this: Scale<CoreScaleOptions>, tickValue: number | string, index: number, ticks: Tick[]) {
227+
const value = Number(tickValue)
228+
if (value >= 1000000) return `${(value / 1000000).toFixed(1)}M`
229+
if (value >= 1000) return `${(value / 1000).toFixed(1)}k`
230+
return value.toString()
231+
}
232+
},
233+
},
234+
x: {
235+
type: 'category' as const,
236+
grid: {
237+
display: false,
238+
},
239+
ticks: {
240+
color: 'rgb(156, 163, 175)',
241+
font: {
242+
family: "'JetBrains Mono', monospace",
243+
},
244+
},
245+
},
246+
},
247+
plugins: {
248+
legend: {
249+
display: true,
250+
position: 'top' as const,
251+
align: 'end' as const,
252+
labels: {
253+
color: 'rgb(156, 163, 175)',
254+
font: {
255+
family: "'JetBrains Mono', monospace",
256+
},
257+
boxWidth: 12,
258+
padding: 15,
259+
},
260+
},
261+
},
262+
}
263+
264+
const timeChartOptions = {
265+
...baseChartOptions,
266+
interaction: {
267+
mode: 'index' as const,
268+
intersect: false,
269+
},
270+
plugins: {
271+
...baseChartOptions.plugins,
272+
tooltip: {
273+
mode: 'index' as const,
274+
intersect: false,
275+
callbacks: {
276+
label: (context: any) => {
277+
let label = context.dataset.label || ''
278+
if (label) label += ': '
279+
if (context.parsed.y !== null) {
280+
const value = context.parsed.y
281+
if (value >= 1000000) return `${label}${(value / 1000000).toFixed(1)}M downloads`
282+
if (value >= 1000) return `${label}${(value / 1000).toFixed(1)}k downloads`
283+
return `${label}${value} downloads`
284+
}
285+
return label
286+
}
287+
}
288+
}
289+
},
290+
}
291+
292+
// Sort packages by downloads for better visualization
293+
const sortedPackages = computed(() => {
294+
return [...packages].sort((a, b) => b.downloads - a.downloads)
295+
})
296+
297+
// Chart data for downloads
298+
const downloadsData = computed(() => ({
299+
labels: sortedPackages.value.map(pkg => pkg.name),
300+
datasets: [{
301+
label: 'Downloads',
302+
data: sortedPackages.value.map(pkg => pkg.downloads),
303+
backgroundColor: 'rgba(59, 130, 246, 0.8)',
304+
borderColor: 'rgb(59, 130, 246)',
305+
borderWidth: 1
306+
}]
307+
}))
308+
309+
// Chart data for contributors
310+
const contributorsData = computed(() => ({
311+
labels: sortedPackages.value.map(pkg => pkg.name),
312+
datasets: [{
313+
label: 'Contributors',
314+
data: sortedPackages.value.map(pkg => pkg.contributors),
315+
backgroundColor: 'rgba(234, 179, 8, 0.8)',
316+
borderColor: 'rgb(234, 179, 8)',
317+
borderWidth: 1
318+
}]
319+
}))
320+
321+
// Chart data for issues
322+
const issuesData = computed(() => ({
323+
labels: sortedPackages.value.map(pkg => pkg.name),
324+
datasets: [{
325+
label: 'Open Issues',
326+
data: sortedPackages.value.map(pkg => pkg.issues),
327+
backgroundColor: 'rgba(239, 68, 68, 0.8)',
328+
borderColor: 'rgb(239, 68, 68)',
329+
borderWidth: 1
330+
}]
331+
}))
332+
333+
// Customize options for different metrics
334+
const contributorsOptions = {
335+
...baseChartOptions,
336+
plugins: {
337+
...baseChartOptions.plugins,
338+
tooltip: {
339+
callbacks: {
340+
label: (context: any) => {
341+
return `Contributors: ${context.parsed.y}`
342+
}
343+
}
344+
}
345+
}
346+
} as const
347+
348+
const issuesOptions = {
349+
...baseChartOptions,
350+
plugins: {
351+
...baseChartOptions.plugins,
352+
tooltip: {
353+
callbacks: {
354+
label: (context: any) => {
355+
return `Open Issues: ${context.parsed.y}`
356+
}
357+
}
358+
}
359+
}
360+
} as const
361+
362+
const timeRange = ref<'7' | '30' | '90' | '365'>('30')
363+
const displayMode = ref<'line' | 'bar'>('line')
364+
const selectedPackages = ref<Set<string>>(new Set())
365+
const isLoading = ref(false)
366+
367+
// Helper function to format dates
368+
const formatDate = (daysAgo: number) => {
369+
const date = new Date()
370+
date.setDate(date.getDate() - daysAgo)
371+
return date.toLocaleDateString('en-US', {
372+
month: 'short',
373+
day: 'numeric'
374+
})
375+
}
376+
377+
// Generate date labels for the selected time range
378+
const generateDateLabels = (days: number) => {
379+
return Array.from({ length: days }, (_, i) => formatDate(days - 1 - i)).reverse()
380+
}
381+
382+
// Generate mock daily download data for a package
383+
const generateDailyData = (baseDownloads: number, days: number) => {
384+
return Array.from({ length: days }, () => {
385+
const variance = baseDownloads * 0.2 // 20% variance
386+
return Math.floor(baseDownloads / days + (Math.random() - 0.5) * variance)
387+
})
388+
}
389+
390+
// Computed property to determine if we should use compact mode
391+
const useCompactMode = computed(() => packages.length > 10)
392+
393+
// Computed property for filtered and sorted packages
394+
const filteredPackages = computed(() => {
395+
let filtered = [...packages]
396+
if (useCompactMode.value && selectedPackages.value.size === 0) {
397+
// In compact mode, default to showing top 5 packages by downloads
398+
filtered = filtered.sort((a, b) => b.downloads - a.downloads).slice(0, 5)
399+
} else if (selectedPackages.value.size > 0) {
400+
filtered = filtered.filter(pkg => selectedPackages.value.has(pkg.name))
401+
}
402+
return filtered.sort((a, b) => b.downloads - a.downloads)
403+
})
404+
405+
// Chart data for downloads over time
406+
const downloadsTimeData = computed(() => {
407+
const days = parseInt(timeRange.value)
408+
const labels = generateDateLabels(days)
409+
410+
return {
411+
labels,
412+
datasets: filteredPackages.value.map((pkg, index) => {
413+
const colorIndex = index % colors.length
414+
const color = colors[colorIndex]
415+
if (!color) return null
416+
417+
return {
418+
label: pkg.name,
419+
data: generateDailyData(pkg.downloads, days),
420+
borderColor: color.border,
421+
backgroundColor: displayMode.value === 'line'
422+
? color.background.replace('0.8', '0.1')
423+
: color.background,
424+
fill: true,
425+
tension: 0.4
426+
}
427+
}).filter((dataset): dataset is NonNullable<typeof dataset> => dataset !== null)
428+
}
429+
})
430+
166431
</script>
167432

168433
<template>
@@ -229,6 +494,111 @@ const getPackageUrl = (pkgName: string) => `https://github.com/stacksjs/${pkgNam
229494
</div>
230495
</div>
231496

497+
<!-- Charts Section -->
498+
<div class="mb-8 px-4 lg:px-8 sm:px-6">
499+
<div class="grid grid-cols-1 gap-8">
500+
<!-- Downloads Chart -->
501+
<div class="bg-white dark:bg-blue-gray-700 rounded-lg shadow">
502+
<div class="p-6">
503+
<div class="flex items-center justify-between mb-6">
504+
<div>
505+
<h3 class="text-base font-medium text-gray-900 dark:text-gray-100">Package Downloads</h3>
506+
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Downloads over time per package</p>
507+
</div>
508+
<div class="flex items-center space-x-4">
509+
<select
510+
v-model="timeRange"
511+
class="h-9 text-sm border-0 rounded-md bg-gray-50 dark:bg-blue-gray-600 py-1.5 pl-3 pr-8 text-gray-900 dark:text-gray-100 ring-1 ring-inset ring-gray-300 dark:ring-gray-600 focus:ring-2 focus:ring-blue-600"
512+
>
513+
<option value="7">Last 7 days</option>
514+
<option value="30">Last 30 days</option>
515+
<option value="90">Last 90 days</option>
516+
<option value="365">Last year</option>
517+
</select>
518+
<div v-if="useCompactMode" class="flex items-center space-x-2">
519+
<select
520+
v-model="selectedPackages"
521+
multiple
522+
class="h-9 text-sm border-0 rounded-md bg-gray-50 dark:bg-blue-gray-600 py-1.5 pl-3 pr-8 text-gray-900 dark:text-gray-100 ring-1 ring-inset ring-gray-300 dark:ring-gray-600 focus:ring-2 focus:ring-blue-600 max-h-32"
523+
>
524+
<option v-for="pkg in packages" :key="pkg.name" :value="pkg.name">
525+
{{ pkg.name }}
526+
</option>
527+
</select>
528+
<button
529+
@click="selectedPackages.clear()"
530+
class="h-9 px-3 text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 ring-1 ring-inset ring-gray-300 dark:ring-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-blue-gray-500"
531+
>
532+
Clear
533+
</button>
534+
</div>
535+
<div class="flex rounded-md shadow-sm h-9">
536+
<button
537+
@click="displayMode = 'line'"
538+
:class="[
539+
'px-3 py-2 text-sm font-semibold rounded-l-md ring-1 ring-inset transition-colors',
540+
displayMode === 'line'
541+
? 'bg-blue-600 text-white ring-blue-600'
542+
: 'bg-white text-gray-600 hover:bg-gray-50 ring-gray-300 dark:bg-blue-gray-600 dark:text-gray-200 dark:ring-gray-600 dark:hover:bg-blue-gray-500'
543+
]"
544+
>
545+
Line
546+
</button>
547+
<button
548+
@click="displayMode = 'bar'"
549+
:class="[
550+
'px-3 py-2 text-sm font-semibold rounded-r-md ring-1 ring-inset transition-colors -ml-px',
551+
displayMode === 'bar'
552+
? 'bg-blue-600 text-white ring-blue-600'
553+
: 'bg-white text-gray-600 hover:bg-gray-50 ring-gray-300 dark:bg-blue-gray-600 dark:text-gray-200 dark:ring-gray-600 dark:hover:bg-blue-gray-500'
554+
]"
555+
>
556+
Bar
557+
</button>
558+
</div>
559+
</div>
560+
</div>
561+
<div class="h-[400px] relative">
562+
<div v-if="isLoading" class="absolute inset-0 flex items-center justify-center bg-white bg-opacity-75 dark:bg-blue-gray-700 dark:bg-opacity-75 z-10">
563+
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
564+
</div>
565+
<component
566+
:is="displayMode === 'line' ? Line : Bar"
567+
:data="downloadsTimeData"
568+
:options="timeChartOptions"
569+
/>
570+
</div>
571+
</div>
572+
</div>
573+
574+
<!-- Contributors Chart -->
575+
<div class="bg-white dark:bg-blue-gray-700 rounded-lg shadow">
576+
<div class="p-6">
577+
<div class="mb-6">
578+
<h3 class="text-base font-medium text-gray-900 dark:text-gray-100">Package Contributors</h3>
579+
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Number of contributors per package</p>
580+
</div>
581+
<div class="h-[400px]">
582+
<Bar :data="contributorsData" :options="contributorsOptions" />
583+
</div>
584+
</div>
585+
</div>
586+
587+
<!-- Issues Chart -->
588+
<div class="bg-white dark:bg-blue-gray-700 rounded-lg shadow">
589+
<div class="p-6">
590+
<div class="mb-6">
591+
<h3 class="text-base font-medium text-gray-900 dark:text-gray-100">Open Issues</h3>
592+
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Number of open issues per package</p>
593+
</div>
594+
<div class="h-[400px]">
595+
<Bar :data="issuesData" :options="issuesOptions" />
596+
</div>
597+
</div>
598+
</div>
599+
</div>
600+
</div>
601+
232602
<!-- Packages Table section -->
233603
<div class="px-4 pt-12 lg:px-8 sm:px-6">
234604
<div class="sm:flex sm:items-center">

0 commit comments

Comments
 (0)