Files
qv/ui/election-results.html

145 lines
5.6 KiB
HTML
Raw Permalink Normal View History

2025-01-20 18:08:19 +01:00
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Election Results - qv</title>
<script src="/static/js/tailwind.min.js"></script>
2025-01-21 11:38:21 +01:00
<script src="/static/js/chart.umd.js"></script>
2025-01-20 18:08:19 +01:00
<script src="/static/js/alpine.min.js" defer></script>
</head>
<body class="bg-gray-100 font-sans leading-normal tracking-normal">
<div x-data="resultsPage" class="container mx-auto p-6 bg-white rounded-lg shadow-lg">
<main>
<h1 class="text-3xl font-bold mb-6">Election Results</h1>
<div x-show="error" class="bg-red-100 p-4 rounded-md text-red-700">
<p x-text="error"></p>
</div>
<template x-if="!error">
<div class="space-y-6">
<!-- Chart Container -->
<div class="bg-white p-6 rounded-lg shadow">
<h2 class="text-xl font-semibold mb-4">Vote Distribution</h2>
<div class="h-96 w-full">
<canvas id="resultsChart"></canvas>
</div>
</div>
<!-- Detailed Results -->
<div class="bg-white p-6 rounded-lg shadow">
<h3 class="text-lg font-semibold mb-4">Detailed Results</h3>
<div class="space-y-2">
<template x-for="(result, index) in results" :key="index">
<div class="flex justify-between border-b pb-2">
<div class="flex items-center">
<div class="w-4 h-4 rounded mr-2" :style="{ backgroundColor: getColor(index) }"></div>
<span class="font-medium" x-text="result.choice"></span>
</div>
<span class="text-gray-600">
<span x-text="result.votes"></span> votes
(<span x-text="getPercentage(result.votes)"></span>%)
</span>
</div>
</template>
</div>
</div>
</div>
</template>
</main>
</div>
<script>
document.addEventListener('alpine:init', () => {
Alpine.data('resultsPage', () => ({
results: [],
error: '',
chart: null,
colors: [
'#4f46e5', // Indigo (primary)
'#2563eb', // Blue
'#7c3aed', // Violet
'#db2777', // Pink
'#dc2626', // Red
],
getColor(index) {
return this.colors[index % this.colors.length];
},
getPercentage(votes) {
const total = this.results.reduce((sum, result) => sum + result.votes, 0);
if (total === 0) return '0.0';
return ((votes / total) * 100).toFixed(1);
},
async init() {
await this.fetchResults();
this.initChart();
},
async fetchResults() {
const electionId = window.location.pathname.split("/")[2];
try {
const response = await fetch(`/api/election/${electionId}/results`);
if (!response.ok) throw new Error("Failed to load election results.");
const data = await response.json();
this.results = data.results;
} catch (error) {
console.error(error);
this.error = error.message;
}
},
initChart() {
if (this.error) return;
const ctx = document.getElementById('resultsChart').getContext('2d');
// Destroy existing chart if it exists
if (this.chart) {
this.chart.destroy();
}
this.chart = new Chart(ctx, {
type: 'doughnut',
data: {
labels: this.results.map(r => r.choice),
datasets: [{
data: this.results.map(r => r.votes),
backgroundColor: this.results.map((_, index) => this.getColor(index)),
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'right',
labels: {
padding: 20,
boxWidth: 12,
boxHeight: 12
}
},
tooltip: {
callbacks: {
label: (context) => {
const total = context.dataset.data.reduce((sum, count) => sum + count, 0);
const percentage = ((context.raw / total) * 100).toFixed(1);
return `${context.raw} votes (${percentage}%)`;
}
}
}
},
cutout: '60%'
}
});
}
}));
});
</script>
</body>
</html>