Use TailwindCSS

This commit is contained in:
2025-01-14 13:31:33 +01:00
parent e102d01bff
commit 07ceec4db2
5 changed files with 145 additions and 212 deletions

View File

@ -4,70 +4,85 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Create New Election</title> <title>Create New Election</title>
<link rel="stylesheet" href="/static/css/styles.css"> <script src="/static/js/tailwind.min.js"></script>
<script src="https://unpkg.com/alpinejs@3" defer></script> <script src="/static/js/alpine.min.js" defer></script>
</head> </head>
<body> <body class="bg-gray-100 font-sans leading-normal tracking-normal">
<div x-data="electionForm" class="container"> <div x-data="electionForm" class="container mx-auto p-6 bg-white rounded-lg shadow-lg">
<main> <main>
<h1>Create New Election</h1> <h1 class="text-3xl font-bold mb-6">Create New Election</h1>
<form @submit.prevent="createElection" class="form"> <form @submit.prevent="createElection" class="space-y-6">
<div class="form-group">
<label for="name">Election Name</label> <div>
<input type="text" id="name" x-model="election.name" required> <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>
<div class="form-row"> <div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="form-group"> <div>
<label for="tokens">Tokens per Voter</label> <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> <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>
<div class="form-group"> <div>
<label for="areVotersKnown">Voter Access</label> <label for="areVotersKnown" class="block text-sm font-medium text-gray-700">Voter Access</label>
<select id="areVotersKnown" x-model="election.areVotersKnown" required> <select id="areVotersKnown" x-model="election.areVotersKnown" required
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="true">Known voters only</option>
<option :value="false">Open to anyone</option> <option :value="false">Open to anyone</option>
</select> </select>
<small>Known voters only = codes will be generated and you must give those to your voters</small> <p class="text-sm text-gray-500 mt-1">Known voters only = codes will be generated for distribution.</p>
</div> </div>
</div> </div>
<div class="form-group"> <div>
<label for="maxVoters">Maximum Number of Voters</label> <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" min="1"> <input type="number" id="maxVoters" x-model.number="election.maxVoters" min="1"
<small>0 = unlimited</small> 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"> <template x-if="election.areVotersKnown && election.maxVoters <= 0">
<span class="error-text">Maximum number of voters must be greater than 0 if voters are known</span> <span class="text-red-500 text-sm">Maximum number of voters must be greater than 0 if voters are known.</span>
</template> </template>
</div> </div>
<div class="form-group"> <div>
<label for="expiresAt">Expiration Date</label> <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> <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>
<div class="form-group"> <div>
<label>Choices</label> <label class="block text-sm font-medium text-gray-700">Choices</label>
<div id="choices-container"> <div class="space-y-2 mt-2">
<template x-for="(choice, index) in election.choices" :key="index"> <template x-for="(choice, index) in election.choices" :key="index">
<div class="choice-input"> <div class="flex space-x-2 items-center">
<input type="text" x-model="election.choices[index]" required> <input type="text" x-model="election.choices[index]" required
<button type="button" class="remove-choice" @click="removeChoice(index)" x-show="election.choices.length > 2">×</button> 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> </div>
</template> </template>
</div> </div>
<button type="button" id="add-choice" @click="addChoice">Add Another Choice</button> <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>
<div class="form-actions"> <div>
<button type="submit">Create Election</button> <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> </div>
</form> </form>
<div x-show="errorMessage || Object.keys(errorDetails).length > 0" class="error-container"> <div x-show="errorMessage || Object.keys(errorDetails).length > 0" class="mt-6 bg-red-100 p-4 rounded-md">
<h2 x-text="errorMessage"></h2> <h2 class="text-red-700 font-bold" x-text="errorMessage"></h2>
<ul> <ul class="text-red-600 mt-2 space-y-1">
<template x-for="(message, field) in errorDetails" :key="field"> <template x-for="(message, field) in errorDetails" :key="field">
<li><strong x-text="field"></strong>: <span x-text="message"></span></li> <li><strong x-text="field"></strong>: <span x-text="message"></span></li>
</template> </template>
@ -75,37 +90,9 @@
</div> </div>
<template x-if="createdElectionId > 0"> <template x-if="createdElectionId > 0">
<div class="election-info"> <div class="mt-6 bg-green-100 p-4 rounded-md">
<h2>Election Created Successfully</h2> <h2 class="text-green-700 font-bold">Election Created Successfully</h2>
<div class="info-container"> <p class="mt-2">Election ID: <span class="font-mono" x-text="createdElectionId"></span></p>
<span class="info-label">Election ID:</span>
<span class="info-value" x-text="createdElectionId"></span>
<button @click="copyElectionId" class="copy-btn">
Copy
</button>
</div>
</div>
</template>
<template x-if="voterIdentities.length > 0">
<div class="voter-codes">
<h2>Voter Access Codes</h2>
<div class="codes-container">
<button @click="copyAllCodes" class="copy-all-btn">
Copy All Codes
</button>
<div class="codes-list">
<template x-for="(code, index) in voterIdentities" :key="code">
<div class="code-item">
<span class="code-number" x-text="index + 1"></span>.
<span class="code-text" x-text="code"></span>
<button @click="copyCode(code)" class="copy-btn">
Copy
</button>
</div>
</template>
</div>
</div>
</div> </div>
</template> </template>
</main> </main>
@ -123,7 +110,6 @@
choices: ["", ""] choices: ["", ""]
}, },
createdElectionId: 0, createdElectionId: 0,
voterIdentities: [],
errorMessage: "", errorMessage: "",
errorDetails: {}, errorDetails: {},
@ -135,27 +121,9 @@
this.election.choices.splice(index, 1); this.election.choices.splice(index, 1);
}, },
async copyCode(code) {
try {
await navigator.clipboard.writeText(code);
} catch (err) {
console.error("Failed to copy code:", err);
}
},
async copyAllCodes() {
try {
const allCodes = this.voterIdentities.join("\n");
await navigator.clipboard.writeText(allCodes);
} catch (err) {
console.error("Failed to copy codes:", err);
}
},
async createElection() { async createElection() {
this.errorMessage = ""; this.errorMessage = "";
this.errorDetails = {}; this.errorDetails = {};
this.voterIdentities = [];
const payload = { const payload = {
...this.election, ...this.election,
@ -181,12 +149,8 @@
const locationHeader = response.headers.get("Location"); const locationHeader = response.headers.get("Location");
this.createdElectionId = locationHeader.replace("/election/", ""); this.createdElectionId = locationHeader.replace("/election/", "");
const data = await response.json();
this.voterIdentities = data.voterIdentities;
} catch (error) { } catch (error) {
this.errorMessage = "Failed to create election."; this.errorMessage = "Failed to create election.";
console.error(error);
} }
} }
})); }));

6
ui/static/js/alpine.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,120 +0,0 @@
htmx.defineExtension('json-enc-custom', {
onEvent: function (name, evt) {
if (name === "htmx:configRequest") {
evt.detail.headers['Content-Type'] = "application/json";
}
},
encodeParameters: function (xhr, parameters, elt) {
xhr.overrideMimeType('text/json');
let encoded_parameters = encodingAlgorithm(parameters);
return encoded_parameters;
}
});
function encodingAlgorithm(parameters) {
let resultingObject = Object.create(null);
const PARAM_NAMES = Object.keys(parameters);
const PARAM_VALUES = Object.values(parameters);
const PARAM_LENGHT = PARAM_NAMES.length;
for (let param_index = 0; param_index < PARAM_LENGHT; param_index++) {
let name = PARAM_NAMES[param_index];
let value = PARAM_VALUES[param_index];
let steps = JSONEncodingPath(name);
let context = resultingObject;
for (let step_index = 0; step_index < steps.length; step_index++) {
let step = steps[step_index];
context = setValueFromPath(context, step, value);
}
}
let result = JSON.stringify(resultingObject);
return result
}
function JSONEncodingPath(name) {
let path = name;
let original = path;
const FAILURE = [{ "type": "object", "key": original, "last": true, "next_type": null }];
let steps = Array();
let first_key = String();
for (let i = 0; i < path.length; i++) {
if (path[i] !== "[") first_key += path[i];
else break;
}
if (first_key === "") return FAILURE;
path = path.slice(first_key.length);
steps.push({ "type": "object", "key": first_key, "last": false, "next_type": null });
while (path.length) {
// [123...]
if (/^\[\d+\]/.test(path)) {
path = path.slice(1);
let collected_digits = path.match(/\d+/)[0]
path = path.slice(collected_digits.length);
let numeric_key = parseInt(collected_digits, 10);
path = path.slice(1);
steps.push({ "type": "array", "key": numeric_key, "last": false, "next_type": null });
continue
}
// [abc...]
if (/^\[[^\]]+\]/.test(path)) {
path = path.slice(1);
let collected_characters = path.match(/[^\]]+/)[0];
path = path.slice(collected_characters.length);
let object_key = collected_characters;
path = path.slice(1);
steps.push({ "type": "object", "key": object_key, "last": false, "next_type": null });
continue;
}
return FAILURE;
}
for (let step_index = 0; step_index < steps.length; step_index++) {
if (step_index === steps.length - 1) {
let tmp_step = steps[step_index];
tmp_step["last"] = true;
steps[step_index] = tmp_step;
}
else {
let tmp_step = steps[step_index];
tmp_step["next_type"] = steps[step_index + 1]["type"];
steps[step_index] = tmp_step;
}
}
return steps;
}
function setValueFromPath(context, step, value) {
if (step.last) {
context[step.key] = value;
}
//TODO: make merge functionality and file suport.
//check if the context value already exists
if (context[step.key] === undefined) {
if (step.type === "object") {
if (step.next_type === "object") {
context[step.key] = {};
return context[step.key];
}
if (step.next_type === "array") {
context[step.key] = [];
return context[step.key];
}
}
if (step.type === "array") {
if (step.next_type === "object") {
context[step.key] = {};
return context[step.key];
}
if (step.next_type === "array") {
context[step.key] = [];
return context[step.key];
}
}
}
else {
return context[step.key];
}
}

84
ui/static/js/tailwind.min.js vendored Normal file

File diff suppressed because one or more lines are too long