Files
qv/ui/create-election.html

162 lines
7.6 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Create New 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="electionForm" class="container mx-auto p-6 bg-white rounded-lg shadow-lg">
<main>
<h1 class="text-3xl font-bold mb-6">Create New Election</h1>
<form @submit.prevent="createElection" class="space-y-6">
<div>
<label for="name" class="block text-sm font-medium text-gray-700">Election Name</label>
<input type="text" id="name" x-model="election.name" required
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500">
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label for="tokens" class="block text-sm font-medium text-gray-700">Tokens per Voter</label>
<input type="number" id="tokens" x-model.number="election.tokens" min="1" required
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500">
</div>
<div>
<label for="areVotersKnown" class="block text-sm font-medium text-gray-700">Voter Access</label>
<select id="areVotersKnown" x-model="election.areVotersKnown" required
@change="election.areVotersKnown = JSON.parse(election.areVotersKnown)"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500">
<option :value=true>Known voters only</option>
<option :value=false>Open to anyone</option>
</select>
<p class="text-sm text-gray-500 mt-1">Known voters only = codes will be generated for distribution.</p>
</div>
</div>
<div>
<label for="maxVoters" class="block text-sm font-medium text-gray-700">Maximum Number of Voters</label>
<input type="number" id="maxVoters" x-model.number="election.maxVoters"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500">
<p class="text-sm text-gray-500">0 = unlimited</p>
<template x-if="election.areVotersKnown && election.maxVoters <= 0">
<span class="text-red-500 text-sm">Maximum number of voters must be greater than 0 if voters are known.</span>
</template>
</div>
<div>
<label for="expiresAt" class="block text-sm font-medium text-gray-700">Expiration Date</label>
<input type="datetime-local" id="expiresAt" x-model="election.expiresAt" required
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Choices</label>
<div class="space-y-2 mt-2">
<template x-for="(choice, index) in election.choices" :key="index">
<div class="flex space-x-2 items-center">
<input type="text" x-model="election.choices[index]" required
class="block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500">
<button type="button" class="text-red-500 hover:text-red-700"
@click="removeChoice(index)" x-show="election.choices.length > 2">×</button>
</div>
</template>
</div>
<button type="button" @click="addChoice"
class="mt-3 inline-flex items-center px-4 py-2 bg-indigo-600 border border-transparent rounded-md font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Add Another Choice
</button>
</div>
<div>
<button type="submit"
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">
Create Election
</button>
</div>
</form>
<div x-show="errorMessage || Object.keys(errorDetails).length > 0" class="mt-6 bg-red-100 p-4 rounded-md">
<h2 class="text-red-700 font-bold" x-text="errorMessage"></h2>
<ul class="text-red-600 mt-2 space-y-1">
<template x-for="(message, field) in errorDetails" :key="field">
<li><strong x-text="field"></strong>: <span x-text="message"></span></li>
</template>
</ul>
</div>
<template x-if="createdElectionId > 0">
<div class="mt-6 bg-green-100 p-4 rounded-md">
<h2 class="text-green-700 font-bold">Election Created Successfully</h2>
<p class="mt-2">Election ID: <span class="font-mono" x-text="createdElectionId"></span></p>
</div>
</template>
</main>
</div>
<script>
document.addEventListener("alpine:init", () => {
Alpine.data("electionForm", () => ({
election: {
name: "",
tokens: 100,
areVotersKnown: true,
maxVoters: 0,
expiresAt: "",
choices: ["", ""]
},
createdElectionId: 0,
errorMessage: "",
errorDetails: {},
addChoice() {
this.election.choices.push("");
},
removeChoice(index) {
this.election.choices.splice(index, 1);
},
async createElection() {
this.errorMessage = "";
this.errorDetails = {};
const payload = {
...this.election,
expiresAt: this.election.expiresAt + ":00Z",
choices: this.election.choices.filter(choice => choice.trim() !== "")
};
try {
const response = await fetch("/api/election", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(payload)
});
if (!response.ok) {
const errorData = await response.json();
this.errorMessage = errorData.message || "An error occurred.";
this.errorDetails = errorData.details?.fields || {};
return;
}
const locationHeader = response.headers.get("Location");
this.createdElectionId = locationHeader.replace("/election/", "");
} catch (error) {
this.errorMessage = "Failed to create election.";
}
}
}));
});
</script>
</body>
</html>