Files
qv/ui/election.html

127 lines
5.4 KiB
HTML
Raw Normal View History

2025-01-17 16:48:38 +01:00
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vote in Election - qv</title>
2025-01-17 16:48:38 +01:00
<script src="/static/js/tailwind.min.js"></script>
<script src="/static/js/alpine.min.js" defer></script>
</head>
<body class="bg-gray-100 font-sans leading-normal tracking-normal">
<div x-data="votePage" class="container mx-auto p-6 bg-white rounded-lg shadow-lg">
<main>
<h1 class="text-3xl font-bold mb-6" x-text="election.name || 'Loading...'"></h1>
<form @submit.prevent="submitVote" class="space-y-6">
<!-- Code Field -->
<template x-if="election.areVotersKnown">
<div>
<label for="code" class="block text-sm font-medium text-gray-700">Access Code</label>
<input type="text" id="code" x-model="code" required
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500">
</div>
</template>
<!-- Voting Choices -->
<template x-if="election.choices.length > 0">
<div>
<label class="block text-sm font-medium text-gray-700">Distribute Your Tokens</label>
<div class="space-y-2 mt-2">
<template x-for="(choice, index) in election.choices" :key="index">
<div>
<label class="block text-gray-700" x-text="choice"></label>
<input type="number" min="0"
x-model.number="votes[index]"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500">
</div>
</template>
<p class="text-sm text-gray-500 mt-2">Tokens remaining: <span x-text="remainingTokens"></span></p>
</div>
</div>
</template>
<!-- No Choices Message -->
<template x-if="!election.choices.length">
<p class="text-gray-700">No choices available for this election.</p>
</template>
<button type="submit"
:disabled="remainingTokens < 0"
class="w-full inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50">
Submit Vote
</button>
</form>
<div x-show="message" class="mt-6 p-4 rounded-md" :class="{'bg-green-100 text-green-700': success, 'bg-red-100 text-red-700': !success}">
<p x-text="message"></p>
</div>
</main>
</div>
<script>
document.addEventListener("alpine:init", () => {
Alpine.data("votePage", () => ({
election: { name: "", choices: [], tokens: 0, areVotersKnown: false },
code: "",
votes: [],
message: "",
success: false,
get remainingTokens() {
return this.election.tokens - this.votes.reduce((sum, v) => sum + v, 0);
},
async init() {
const electionId = window.location.pathname.split("/").pop();
try {
const response = await fetch(`/api/election/${electionId}`);
if (!response.ok) throw new Error("Failed to load election details.");
const data = await response.json();
this.election = data;
this.votes = Array(data.choices.length).fill(0);
} catch (error) {
console.error(error);
this.message = "Failed to load election details.";
this.success = false;
}
},
async submitVote() {
if (this.remainingTokens < 0) {
this.message = "You have exceeded the number of available tokens.";
this.success = false;
return;
}
const electionId = window.location.pathname.split("/").pop();
const payload = {
voterIdentity: this.election.areVotersKnown ? this.code : null,
choices: this.election.choices.map((choice, index) => ({
choiceText: choice,
tokens: this.votes[index] || 0,
})),
};
try {
const response = await fetch(`/api/election/${electionId}/votes`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!response.ok) {
res = await response.json();
throw new Error(res.message);
}
2025-01-17 16:48:38 +01:00
this.message = "Vote submitted successfully!";
this.success = true;
} catch (error) {
this.message = error.message;
2025-01-17 16:48:38 +01:00
this.success = false;
}
}
}));
});
</script>
</body>
</html>