Skip to content

Commit 03135ec

Browse files
committed
chore: wip
1 parent e44575d commit 03135ec

File tree

3 files changed

+633
-32
lines changed

3 files changed

+633
-32
lines changed

storage/framework/defaults/views/dashboard/models/subscribers.vue

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,37 @@
6868
</div>
6969
</div>
7070

71+
<!-- Growth Chart -->
72+
<div class="mb-8 px-4 lg:px-8 sm:px-6">
73+
<div class="bg-white dark:bg-blue-gray-700 rounded-lg shadow">
74+
<div class="p-6">
75+
<div class="flex items-center justify-between mb-6">
76+
<div>
77+
<h3 class="text-base font-medium text-gray-900 dark:text-gray-100">Subscriber Growth</h3>
78+
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Track subscriber count over time</p>
79+
</div>
80+
<div class="flex items-center space-x-4">
81+
<select
82+
v-model="timeRange"
83+
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"
84+
>
85+
<option value="7">Last 7 days</option>
86+
<option value="30">Last 30 days</option>
87+
<option value="90">Last 90 days</option>
88+
<option value="365">Last year</option>
89+
</select>
90+
</div>
91+
</div>
92+
<div class="h-[400px] relative">
93+
<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">
94+
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
95+
</div>
96+
<Line :data="chartData" :options="chartOptions" />
97+
</div>
98+
</div>
99+
</div>
100+
</div>
101+
71102
<div class="px-4 pt-12 lg:px-8 sm:px-6">
72103
<div class="sm:flex sm:items-center">
73104
<div class="sm:flex-auto">
@@ -198,3 +229,173 @@
198229
</div>
199230
</div>
200231
</template>
232+
233+
<script lang="ts" setup>
234+
import { ref, computed, watch, onMounted } from 'vue'
235+
import { useHead } from '@vueuse/head'
236+
import { Line } from 'vue-chartjs'
237+
import {
238+
Chart as ChartJS,
239+
CategoryScale,
240+
LinearScale,
241+
PointElement,
242+
LineElement,
243+
Title,
244+
Tooltip,
245+
Legend,
246+
Filler,
247+
Scale,
248+
CoreScaleOptions,
249+
} from 'chart.js'
250+
251+
ChartJS.register(
252+
CategoryScale,
253+
LinearScale,
254+
PointElement,
255+
LineElement,
256+
Title,
257+
Tooltip,
258+
Legend,
259+
Filler,
260+
)
261+
262+
useHead({
263+
title: 'Dashboard - Subscribers',
264+
})
265+
266+
const timeRange = ref<'7' | '30' | '90' | '365'>('30')
267+
const isLoading = ref(false)
268+
269+
// Helper function to format dates
270+
const formatDate = (daysAgo: number) => {
271+
const date = new Date()
272+
date.setDate(date.getDate() - daysAgo)
273+
return date.toLocaleDateString('en-US', {
274+
month: 'short',
275+
day: 'numeric'
276+
})
277+
}
278+
279+
// Generate date labels for the selected time range
280+
const generateDateLabels = (days: number) => {
281+
return Array.from({ length: days }, (_, i) => formatDate(days - 1 - i)).reverse()
282+
}
283+
284+
// Generate mock growth data
285+
const generateGrowthData = (days: number, baseCount: number, dailyGrowth: number) => {
286+
return Array.from({ length: days }, (_, i) => {
287+
const dayVariance = Math.random() * dailyGrowth * 0.5 // 50% variance
288+
return Math.floor(baseCount + (dailyGrowth * i) + dayVariance)
289+
})
290+
}
291+
292+
// Chart options
293+
const chartOptions = {
294+
responsive: true,
295+
maintainAspectRatio: false,
296+
scales: {
297+
y: {
298+
type: 'linear' as const,
299+
beginAtZero: true,
300+
grid: {
301+
color: 'rgba(200, 200, 200, 0.1)',
302+
},
303+
ticks: {
304+
color: 'rgb(156, 163, 175)',
305+
font: {
306+
family: "'JetBrains Mono', monospace",
307+
},
308+
callback: function(this: Scale<CoreScaleOptions>, tickValue: string | number) {
309+
const value = Number(tickValue)
310+
if (value >= 1000000) return `${(value / 1000000).toFixed(1)}M`
311+
if (value >= 1000) return `${(value / 1000).toFixed(1)}k`
312+
return value.toString()
313+
}
314+
},
315+
},
316+
x: {
317+
type: 'category' as const,
318+
grid: {
319+
display: false,
320+
},
321+
ticks: {
322+
color: 'rgb(156, 163, 175)',
323+
font: {
324+
family: "'JetBrains Mono', monospace",
325+
},
326+
},
327+
},
328+
},
329+
plugins: {
330+
legend: {
331+
display: true,
332+
position: 'top' as const,
333+
align: 'end' as const,
334+
labels: {
335+
color: 'rgb(156, 163, 175)',
336+
font: {
337+
family: "'JetBrains Mono', monospace",
338+
},
339+
boxWidth: 12,
340+
padding: 15,
341+
},
342+
},
343+
tooltip: {
344+
mode: 'index' as const,
345+
intersect: false,
346+
callbacks: {
347+
label: (context: any) => {
348+
let label = context.dataset.label || ''
349+
if (label) label += ': '
350+
if (context.parsed.y !== null) {
351+
const value = context.parsed.y
352+
if (value >= 1000000) return `${label}${(value / 1000000).toFixed(1)}M subscribers`
353+
if (value >= 1000) return `${label}${(value / 1000).toFixed(1)}k subscribers`
354+
return `${label}${value} subscribers`
355+
}
356+
return label
357+
}
358+
}
359+
}
360+
},
361+
interaction: {
362+
mode: 'index' as const,
363+
intersect: false,
364+
},
365+
} as const
366+
367+
// Chart data
368+
const chartData = computed(() => {
369+
const days = parseInt(timeRange.value)
370+
const labels = generateDateLabels(days)
371+
const baseCount = 500 // Starting with 500 subscribers
372+
const dailyGrowth = 5 // Average 5 new subscribers per day
373+
374+
return {
375+
labels,
376+
datasets: [{
377+
label: 'Total Subscribers',
378+
data: generateGrowthData(days, baseCount, dailyGrowth),
379+
borderColor: 'rgb(14, 165, 233)', // Sky blue color for subscribers
380+
backgroundColor: 'rgba(14, 165, 233, 0.1)',
381+
fill: true,
382+
tension: 0.4
383+
}]
384+
}
385+
})
386+
387+
// Watch for time range changes
388+
watch(timeRange, async () => {
389+
isLoading.value = true
390+
// Simulate API delay
391+
await new Promise(resolve => setTimeout(resolve, 500))
392+
isLoading.value = false
393+
})
394+
395+
// Initial load
396+
onMounted(async () => {
397+
isLoading.value = true
398+
await new Promise(resolve => setTimeout(resolve, 500))
399+
isLoading.value = false
400+
})
401+
</script>

0 commit comments

Comments
 (0)