Add page for results
This commit is contained in:
@ -39,6 +39,17 @@ func (app *application) createElectionPage(w http.ResponseWriter, r *http.Reques
|
|||||||
w.Write(content)
|
w.Write(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (app *application) getElectionResultsPage(w http.ResponseWriter, r *http.Request) {
|
||||||
|
content, err := ui.Files.ReadFile("election-results.html")
|
||||||
|
if err != nil {
|
||||||
|
app.serverError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "text/html")
|
||||||
|
w.Write(content)
|
||||||
|
}
|
||||||
|
|
||||||
func (app *application) getElectionPage(w http.ResponseWriter, r *http.Request) {
|
func (app *application) getElectionPage(w http.ResponseWriter, r *http.Request) {
|
||||||
content, err := ui.Files.ReadFile("election.html")
|
content, err := ui.Files.ReadFile("election.html")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -24,6 +24,7 @@ func (app *application) routes() http.Handler {
|
|||||||
|
|
||||||
mux.HandleFunc("GET /", app.indexPage)
|
mux.HandleFunc("GET /", app.indexPage)
|
||||||
mux.HandleFunc("GET /election/create", app.createElectionPage)
|
mux.HandleFunc("GET /election/create", app.createElectionPage)
|
||||||
|
mux.HandleFunc("GET /election/{id}/results", app.getElectionResultsPage)
|
||||||
mux.HandleFunc("GET /election/{id}", app.getElectionPage)
|
mux.HandleFunc("GET /election/{id}", app.getElectionPage)
|
||||||
|
|
||||||
api.HandlerWithOptions(app, api.StdHTTPServerOptions{
|
api.HandlerWithOptions(app, api.StdHTTPServerOptions{
|
||||||
|
145
ui/election-results.html
Normal file
145
ui/election-results.html
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
<!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>
|
||||||
|
<script src="/static/js/alpine.min.js" defer></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.7.0/chart.min.js"></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>
|
Reference in New Issue
Block a user