Update stats page design
This commit is contained in:
+276
-36
@@ -1,73 +1,313 @@
|
||||
import Chart from 'chart.js/auto';
|
||||
|
||||
// Theming
|
||||
if (localStorage.theme !== 'light') {
|
||||
Chart.defaults.color = "#ADBABD";
|
||||
Chart.defaults.borderColor = "rgba(255,255,255,0.1)";
|
||||
Chart.defaults.backgroundColor = "rgba(255,255,0,0.1)";
|
||||
Chart.defaults.elements.line.borderColor = "rgba(255,255,0,0.4)";
|
||||
/**
|
||||
* Theme-aware chart defaults
|
||||
*/
|
||||
function getChartColors() {
|
||||
const isDark = localStorage.theme !== 'light' &&
|
||||
(!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||
|
||||
if (isDark) {
|
||||
return {
|
||||
textColor: '#ADBABD',
|
||||
gridColor: 'rgba(255, 255, 255, 0.06)',
|
||||
fillStart: 'rgba(190, 18, 60, 0.2)',
|
||||
fillEnd: 'rgba(190, 18, 60, 0.0)',
|
||||
borderColor: 'rgba(190, 18, 60, 0.9)',
|
||||
pointColor: 'rgba(190, 18, 60, 1)',
|
||||
pointHoverColor: '#ffffff',
|
||||
skeletonBase: '#262626',
|
||||
skeletonShimmer: '#333333',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
textColor: '#6B7280',
|
||||
gridColor: 'rgba(0, 0, 0, 0.06)',
|
||||
fillStart: 'rgba(190, 18, 60, 0.15)',
|
||||
fillEnd: 'rgba(190, 18, 60, 0.0)',
|
||||
borderColor: 'rgba(190, 18, 60, 1.0)',
|
||||
pointColor: 'rgba(190, 18, 60, 1)',
|
||||
pointHoverColor: '#ffffff',
|
||||
skeletonBase: '#E5E7EB',
|
||||
skeletonShimmer: '#F3F4F6',
|
||||
};
|
||||
}
|
||||
|
||||
// Get Tags from API
|
||||
window.axios.get('/v1/monthly-views').then(function (response) {
|
||||
if (response.status != 200) {
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* Show the skeleton loader
|
||||
*/
|
||||
function showSkeleton() {
|
||||
const skeleton = document.getElementById('chart-skeleton');
|
||||
const canvas = document.getElementById('monthlyChart');
|
||||
const error = document.getElementById('chart-error');
|
||||
|
||||
if (skeleton) skeleton.style.display = '';
|
||||
if (canvas) canvas.style.opacity = '0';
|
||||
if (error) error.classList.add('hidden');
|
||||
}
|
||||
|
||||
const data = {
|
||||
labels: response.data.map((entry) => { return entry.date }),
|
||||
/**
|
||||
* Hide the skeleton and show the chart canvas
|
||||
*/
|
||||
function hideSkeleton() {
|
||||
const skeleton = document.getElementById('chart-skeleton');
|
||||
const canvas = document.getElementById('monthlyChart');
|
||||
|
||||
if (skeleton) {
|
||||
// Fade out skeleton
|
||||
skeleton.style.transition = 'opacity 0.4s ease-out';
|
||||
skeleton.style.opacity = '0';
|
||||
setTimeout(() => {
|
||||
if (skeleton) skeleton.style.display = 'none';
|
||||
}, 400);
|
||||
}
|
||||
|
||||
if (canvas) {
|
||||
setTimeout(() => {
|
||||
canvas.style.opacity = '1';
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show error state
|
||||
*/
|
||||
function showError() {
|
||||
const skeleton = document.getElementById('chart-skeleton');
|
||||
const error = document.getElementById('chart-error');
|
||||
|
||||
if (skeleton) skeleton.style.display = 'none';
|
||||
if (error) {
|
||||
error.classList.remove('hidden');
|
||||
error.style.display = 'flex';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide error state
|
||||
*/
|
||||
function hideError() {
|
||||
const error = document.getElementById('chart-error');
|
||||
if (error) {
|
||||
error.classList.add('hidden');
|
||||
error.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create gradient fill for the chart
|
||||
*/
|
||||
function createGradient(ctx, colors) {
|
||||
const gradient = ctx.createLinearGradient(0, 0, 0, ctx.canvas.clientHeight);
|
||||
gradient.addColorStop(0, colors.fillStart);
|
||||
gradient.addColorStop(1, colors.fillEnd);
|
||||
return gradient;
|
||||
}
|
||||
|
||||
let monthlyViewChart = null;
|
||||
|
||||
/**
|
||||
* Render the chart with data
|
||||
*/
|
||||
function renderChart(data) {
|
||||
const colors = getChartColors();
|
||||
const canvas = document.getElementById('monthlyChart');
|
||||
if (!canvas) return;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// Destroy previous chart instance if it exists
|
||||
if (monthlyViewChart) {
|
||||
monthlyViewChart.destroy();
|
||||
monthlyViewChart = null;
|
||||
}
|
||||
|
||||
const gradient = createGradient(ctx, colors);
|
||||
|
||||
const chartData = {
|
||||
labels: data.map((entry) => entry.date),
|
||||
datasets: [{
|
||||
label: 'Views',
|
||||
fill: false,
|
||||
backgroundColor: 'rgba(190, 18, 60, 0.3)',
|
||||
borderColor: 'rgba(190, 18, 60, 1.0)',
|
||||
fill: true,
|
||||
backgroundColor: gradient,
|
||||
borderColor: colors.borderColor,
|
||||
borderWidth: 2.5,
|
||||
pointBackgroundColor: colors.pointColor,
|
||||
pointBorderColor: colors.pointColor,
|
||||
pointHoverBackgroundColor: colors.pointHoverColor,
|
||||
pointHoverBorderColor: colors.borderColor,
|
||||
pointHoverBorderWidth: 2,
|
||||
pointHoverRadius: 6,
|
||||
pointRadius: 2.5,
|
||||
pointHitRadius: 20,
|
||||
cubicInterpolationMode: 'monotone',
|
||||
data: response.data.map((entry) => { return entry.count }),
|
||||
tension: 0.4,
|
||||
data: data.map((entry) => entry.count),
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
const config = {
|
||||
type: 'line',
|
||||
data: data,
|
||||
data: chartData,
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
animation: {
|
||||
duration: 1200,
|
||||
easing: 'easeOutQuart',
|
||||
},
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Views the last 28 days',
|
||||
font: {
|
||||
size: 18
|
||||
display: false,
|
||||
},
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(17, 17, 17, 0.95)',
|
||||
titleColor: '#ffffff',
|
||||
bodyColor: '#D1D5DB',
|
||||
borderColor: 'rgba(255, 255, 255, 0.1)',
|
||||
borderWidth: 1,
|
||||
padding: 12,
|
||||
cornerRadius: 10,
|
||||
displayColors: false,
|
||||
bodyFont: {
|
||||
size: 13,
|
||||
},
|
||||
titleFont: {
|
||||
size: 12,
|
||||
weight: '600',
|
||||
},
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
return 'Views: ' + new Intl.NumberFormat().format(context.parsed.y);
|
||||
},
|
||||
title: function(context) {
|
||||
return context[0].label;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
interaction: {
|
||||
intersect: false,
|
||||
mode: 'index',
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
display: true,
|
||||
grid: {
|
||||
color: colors.gridColor,
|
||||
drawBorder: false,
|
||||
},
|
||||
ticks: {
|
||||
color: colors.textColor,
|
||||
font: {
|
||||
size: 11,
|
||||
},
|
||||
maxTicksLimit: 14,
|
||||
maxRotation: 0,
|
||||
},
|
||||
title: {
|
||||
display: true
|
||||
display: false,
|
||||
}
|
||||
},
|
||||
y: {
|
||||
display: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Views'
|
||||
beginAtZero: true,
|
||||
grid: {
|
||||
color: colors.gridColor,
|
||||
drawBorder: false,
|
||||
},
|
||||
ticks: {
|
||||
color: colors.textColor,
|
||||
font: {
|
||||
size: 11,
|
||||
},
|
||||
callback: function(value) {
|
||||
if (value >= 1000000) return (value / 1000000).toFixed(1) + 'M';
|
||||
if (value >= 1000) return (value / 1000).toFixed(1) + 'K';
|
||||
return value;
|
||||
},
|
||||
},
|
||||
title: {
|
||||
display: false,
|
||||
},
|
||||
suggestedMin: 0,
|
||||
suggestedMax: 40000
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const monthlyViewChart = new Chart(
|
||||
document.getElementById('monthlyChart'),
|
||||
config
|
||||
);
|
||||
}).catch(function (error) {
|
||||
console.log(error);
|
||||
hideError();
|
||||
monthlyViewChart = new Chart(canvas, config);
|
||||
hideSkeleton();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch chart data from the API
|
||||
*/
|
||||
async function fetchChartData() {
|
||||
showSkeleton();
|
||||
hideError();
|
||||
|
||||
try {
|
||||
const response = await window.axios.get('/v1/monthly-views');
|
||||
|
||||
if (response.status !== 200 || !response.data || response.data.length === 0) {
|
||||
throw new Error('Invalid or empty response');
|
||||
}
|
||||
|
||||
renderChart(response.data);
|
||||
} catch (error) {
|
||||
console.error('Failed to load chart data:', error);
|
||||
showError();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retry loading the chart (called from error state button)
|
||||
*/
|
||||
window.retryChart = function() {
|
||||
if (monthlyViewChart) {
|
||||
monthlyViewChart.destroy();
|
||||
monthlyViewChart = null;
|
||||
}
|
||||
fetchChartData();
|
||||
};
|
||||
|
||||
// Listen for theme changes to re-render chart
|
||||
const themeObserver = new MutationObserver(() => {
|
||||
if (monthlyViewChart) {
|
||||
const data = monthlyViewChart.data.datasets[0].data.map((value, index) => ({
|
||||
date: monthlyViewChart.data.labels[index],
|
||||
count: value,
|
||||
}));
|
||||
renderChart(data);
|
||||
}
|
||||
});
|
||||
|
||||
// Observe theme class changes on html element
|
||||
const htmlElement = document.documentElement;
|
||||
if (htmlElement) {
|
||||
themeObserver.observe(htmlElement, { attributes: true, attributeFilter: ['class'] });
|
||||
}
|
||||
|
||||
// Start the fetch when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Small delay to ensure the page has rendered and skeleton is visible
|
||||
setTimeout(() => {
|
||||
fetchChartData();
|
||||
}, 300);
|
||||
});
|
||||
|
||||
// Handle window resize for theme changes (system preference)
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
|
||||
if (!('theme' in localStorage) && monthlyViewChart) {
|
||||
const data = monthlyViewChart.data.datasets[0].data.map((value, index) => ({
|
||||
date: monthlyViewChart.data.labels[index],
|
||||
count: value,
|
||||
}));
|
||||
renderChart(data);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user