Implement page to vote and fix trigger

This commit is contained in:
2025-01-17 16:48:38 +01:00
parent 62562272f1
commit 27d166dad6
4 changed files with 145 additions and 9 deletions

View File

@ -20,7 +20,18 @@ import (
func (app *application) createElectionPage(w http.ResponseWriter, r *http.Request) {
content, err := ui.Files.ReadFile("create-election.html")
if err != nil {
app.serverError(w, r, fmt.Errorf("couldn't read create-election.html"))
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) {
content, err := ui.Files.ReadFile("election.html")
if err != nil {
app.serverError(w, r, err)
return
}
@ -253,23 +264,23 @@ func (app *application) createVotesHandleUnknownVotersElection(w http.ResponseWr
return "", fmt.Errorf(message)
}
_, err = app.voters.InsertMultiple([]string{voterIdentity}, election.ID)
if err != nil {
app.serverError(w, r, err)
return "", err
}
voterCount, err := app.voters.CountByElection(election.ID)
if err != nil && !errors.Is(sql.ErrNoRows, err) {
app.serverError(w, r, err)
return "", err
}
if voterCount == election.MaxVoters {
if voterCount != 0 && voterCount == election.MaxVoters {
message := "maximum voters reached"
app.clientError(w, http.StatusUnprocessableEntity, message)
return "", fmt.Errorf(message)
}
_, err = app.voters.InsertMultiple([]string{voterIdentity}, election.ID)
if err != nil {
app.serverError(w, r, err)
return "", err
}
return voterIdentity, nil
}

View File

@ -23,6 +23,7 @@ func (app *application) routes() http.Handler {
mux.Handle("GET /static/", cached.Then(http.FileServerFS(ui.Files)))
mux.HandleFunc("GET /election/create", app.createElectionPage)
mux.HandleFunc("GET /election/{id}", app.getElectionPage)
api.HandlerWithOptions(app, api.StdHTTPServerOptions{
BaseRouter: mux,

View File

@ -35,7 +35,7 @@ CREATE TRIGGER enforce_max_voters
SELECT 1
FROM elections e
WHERE e.id = NEW.election_id
AND e.max_voters IS NOT NULL
AND e.max_voters != 0
)
BEGIN
SELECT CASE

124
ui/election.html Normal file
View File

@ -0,0 +1,124 @@
<!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</title>
<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) throw new Error("Failed to submit vote.");
this.message = "Vote submitted successfully!";
this.success = true;
} catch (error) {
console.error(error);
this.message = "Failed to submit vote.";
this.success = false;
}
}
}));
});
</script>
</body>
</html>