Add target numbers, results and X-10 with sortable admin table
This commit is contained in:
parent
150e0a00b5
commit
619ab9133a
51
Program.cs
51
Program.cs
@ -35,16 +35,17 @@ app.MapPost("/api/visitors", async (VisitorInput input, Database database) =>
|
|||||||
command.CommandText = """
|
command.CommandText = """
|
||||||
insert into visitors (name, street_address, postal_code, city, birthday, email)
|
insert into visitors (name, street_address, postal_code, city, birthday, email)
|
||||||
values ($name, $street, $postal, $city, $birthday, $email)
|
values ($name, $street, $postal, $city, $birthday, $email)
|
||||||
|
returning id
|
||||||
""";
|
""";
|
||||||
command.Parameters.AddWithValue("$name", input.Name.Trim());
|
command.Parameters.AddWithValue("$name", input.Name.Trim());
|
||||||
command.Parameters.AddWithValue("$street", input.StreetAddress.Trim());
|
command.Parameters.AddWithValue("$street", input.StreetAddress.Trim());
|
||||||
command.Parameters.AddWithValue("$postal", input.PostalCode.Trim());
|
command.Parameters.AddWithValue("$postal", input.PostalCode.Trim());
|
||||||
command.Parameters.AddWithValue("$city", input.City.Trim());
|
command.Parameters.AddWithValue("$city", input.City.Trim());
|
||||||
command.Parameters.AddWithValue("$birthday", DateOnly.Parse(input.Birthday).ToString("yyyy-MM-dd"));
|
command.Parameters.AddWithValue("$birthday", DateOnly.Parse(input.Birthday).ToString("yyyy-MM-dd"));
|
||||||
command.Parameters.AddWithValue("$email", input.Email.Trim().ToLowerInvariant());
|
command.Parameters.AddWithValue("$email", input.Email?.Trim().ToLowerInvariant() ?? "");
|
||||||
await command.ExecuteNonQueryAsync();
|
var number = Convert.ToInt64(await command.ExecuteScalarAsync());
|
||||||
|
|
||||||
return Results.Created("/api/visitors", null);
|
return Results.Created("/api/visitors", new { number });
|
||||||
});
|
});
|
||||||
|
|
||||||
app.MapGet("/api/visitors", async (Database database) =>
|
app.MapGet("/api/visitors", async (Database database) =>
|
||||||
@ -53,7 +54,7 @@ app.MapGet("/api/visitors", async (Database database) =>
|
|||||||
await using var connection = await database.OpenConnectionAsync();
|
await using var connection = await database.OpenConnectionAsync();
|
||||||
await using var command = connection.CreateCommand();
|
await using var command = connection.CreateCommand();
|
||||||
command.CommandText = """
|
command.CommandText = """
|
||||||
select id, name, street_address, postal_code, city, birthday, email, created_at
|
select id, name, street_address, postal_code, city, birthday, email, created_at, result, x10
|
||||||
from visitors
|
from visitors
|
||||||
order by created_at desc, id desc
|
order by created_at desc, id desc
|
||||||
""";
|
""";
|
||||||
@ -69,13 +70,31 @@ app.MapGet("/api/visitors", async (Database database) =>
|
|||||||
reader.GetString(4),
|
reader.GetString(4),
|
||||||
DateOnly.Parse(reader.GetString(5)),
|
DateOnly.Parse(reader.GetString(5)),
|
||||||
reader.GetString(6),
|
reader.GetString(6),
|
||||||
DateTime.SpecifyKind(reader.GetDateTime(7), DateTimeKind.Utc)
|
DateTime.SpecifyKind(reader.GetDateTime(7), DateTimeKind.Utc),
|
||||||
|
reader.GetString(8),
|
||||||
|
reader.IsDBNull(9) ? null : reader.GetInt32(9)
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
return Results.Ok(visitors);
|
return Results.Ok(visitors);
|
||||||
}).RequireAuthorization();
|
}).RequireAuthorization();
|
||||||
|
|
||||||
|
app.MapPut("/api/visitors/{id:long}/result", async (long id, VisitorResultInput input, Database database) =>
|
||||||
|
{
|
||||||
|
var result = input.Result?.Trim() ?? "";
|
||||||
|
if (input.X10 < 0)
|
||||||
|
return Results.ValidationProblem(new Dictionary<string, string[]> { ["x10"] = ["X-10 skal være et heltal på mindst 0."] });
|
||||||
|
if (result.Length > 100)
|
||||||
|
return Results.ValidationProblem(new Dictionary<string, string[]> { ["result"] = ["Resultatet må højst være 100 tegn."] });
|
||||||
|
await using var connection = await database.OpenConnectionAsync();
|
||||||
|
await using var command = connection.CreateCommand();
|
||||||
|
command.CommandText = "update visitors set result = $result, x10 = $x10 where id = $id";
|
||||||
|
command.Parameters.AddWithValue("$x10", (object?)input.X10 ?? DBNull.Value);
|
||||||
|
command.Parameters.AddWithValue("$result", result);
|
||||||
|
command.Parameters.AddWithValue("$id", id);
|
||||||
|
return await command.ExecuteNonQueryAsync() == 0 ? Results.NotFound() : Results.Ok(new { result, x10 = input.X10 });
|
||||||
|
}).RequireAuthorization();
|
||||||
|
|
||||||
app.MapDelete("/api/visitors/{id:long}", async (long id, Database database) =>
|
app.MapDelete("/api/visitors/{id:long}", async (long id, Database database) =>
|
||||||
{
|
{
|
||||||
await using var connection = await database.OpenConnectionAsync();
|
await using var connection = await database.OpenConnectionAsync();
|
||||||
@ -122,17 +141,18 @@ static Dictionary<string, string[]> Validate(VisitorInput input)
|
|||||||
errors["birthday"] = ["Fødselsdag mangler eller er ugyldig."];
|
errors["birthday"] = ["Fødselsdag mangler eller er ugyldig."];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(input.Email) || !input.Email.Contains('@') || !input.Email.Contains('.'))
|
if (!string.IsNullOrWhiteSpace(input.Email) && (!input.Email.Contains('@') || !input.Email.Contains('.')))
|
||||||
{
|
{
|
||||||
errors["email"] = ["Email mangler eller er ugyldig."];
|
errors["email"] = ["Email er ugyldig."];
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed record VisitorInput(string Name, string StreetAddress, string PostalCode, string City, string Birthday, string Email);
|
public sealed record VisitorInput(string Name, string StreetAddress, string PostalCode, string City, string Birthday, string? Email);
|
||||||
|
|
||||||
public sealed record VisitorListItem(long Id, string Name, string StreetAddress, string PostalCode, string City, DateOnly Birthday, string Email, DateTime CreatedAt);
|
public sealed record VisitorResultInput(string? Result, int? X10);
|
||||||
|
public sealed record VisitorListItem(long Id, string Name, string StreetAddress, string PostalCode, string City, DateOnly Birthday, string Email, DateTime CreatedAt, string Result, int? X10);
|
||||||
|
|
||||||
public sealed class Database
|
public sealed class Database
|
||||||
{
|
{
|
||||||
@ -181,6 +201,19 @@ public sealed class Database
|
|||||||
""";
|
""";
|
||||||
|
|
||||||
await command.ExecuteNonQueryAsync();
|
await command.ExecuteNonQueryAsync();
|
||||||
|
await using var schemaCommand = connection.CreateCommand();
|
||||||
|
schemaCommand.CommandText = "select count(*) from pragma_table_info('visitors') where name = 'result'";
|
||||||
|
if (Convert.ToInt64(await schemaCommand.ExecuteScalarAsync()) == 0)
|
||||||
|
{
|
||||||
|
schemaCommand.CommandText = "alter table visitors add column result text not null default ''";
|
||||||
|
await schemaCommand.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
|
schemaCommand.CommandText = "select count(*) from pragma_table_info('visitors') where name = 'x10'";
|
||||||
|
if (Convert.ToInt64(await schemaCommand.ExecuteScalarAsync()) == 0)
|
||||||
|
{
|
||||||
|
schemaCommand.CommandText = "alter table visitors add column x10 integer";
|
||||||
|
await schemaCommand.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -16,11 +16,13 @@
|
|||||||
<p class="eyebrow">Vestfalster Skytteforening</p>
|
<p class="eyebrow">Vestfalster Skytteforening</p>
|
||||||
<h1>Besøgende</h1>
|
<h1>Besøgende</h1>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="toolbar-right">
|
||||||
|
<div class="admin-actions no-print">
|
||||||
|
<button id="export-csv" type="button" disabled>CSV / Excel</button>
|
||||||
|
<button id="print-list" type="button" disabled>Udskriv / PDF</button>
|
||||||
|
</div>
|
||||||
<span id="count" class="count">Henter...</span>
|
<span id="count" class="count">Henter...</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="admin-actions no-print">
|
|
||||||
<button id="export-csv" type="button" disabled>Download CSV</button>
|
|
||||||
<button id="print-list" type="button" disabled>Udskriv / gem som PDF</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div id="message" class="notice" hidden></div>
|
<div id="message" class="notice" hidden></div>
|
||||||
<section class="participant-map no-print" aria-labelledby="map-title">
|
<section class="participant-map no-print" aria-labelledby="map-title">
|
||||||
@ -33,18 +35,21 @@
|
|||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Navn</th>
|
<th aria-sort="none"><button type="button" class="sort-button" data-sort="id">Skivenr.<span class="sort-arrow" aria-hidden="true"> ↕</span></button></th>
|
||||||
<th>Vejnavn og husnummer</th>
|
<th aria-sort="none"><button type="button" class="sort-button" data-sort="name">Navn<span class="sort-arrow" aria-hidden="true"> ↕</span></button></th>
|
||||||
<th>Postnummer</th>
|
<th aria-sort="none"><button type="button" class="sort-button" data-sort="streetAddress">Vejnavn og husnummer<span class="sort-arrow" aria-hidden="true"> ↕</span></button></th>
|
||||||
<th>Postby</th>
|
<th aria-sort="none"><button type="button" class="sort-button" data-sort="postalCode">Postnummer<span class="sort-arrow" aria-hidden="true"> ↕</span></button></th>
|
||||||
<th>Fødselsdag</th>
|
<th aria-sort="none"><button type="button" class="sort-button" data-sort="city">Postby<span class="sort-arrow" aria-hidden="true"> ↕</span></button></th>
|
||||||
<th>Email</th>
|
<th aria-sort="none"><button type="button" class="sort-button" data-sort="birthday">Fødselsdag<span class="sort-arrow" aria-hidden="true"> ↕</span></button></th>
|
||||||
<th>Registreret</th>
|
<th aria-sort="none"><button type="button" class="sort-button" data-sort="email">Email<span class="sort-arrow" aria-hidden="true"> ↕</span></button></th>
|
||||||
|
<th aria-sort="none"><button type="button" class="sort-button" data-sort="createdAt">Registreret<span class="sort-arrow" aria-hidden="true"> ↕</span></button></th>
|
||||||
|
<th aria-sort="none"><button type="button" class="sort-button" data-sort="result">Resultat<span class="sort-arrow" aria-hidden="true"> ↕</span></button></th>
|
||||||
|
<th aria-sort="none"><button type="button" class="sort-button" data-sort="x10">X-10<span class="sort-arrow" aria-hidden="true"> ↕</span></button></th>
|
||||||
<th class="no-print">Handling</th>
|
<th class="no-print">Handling</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="visitors">
|
<tbody id="visitors">
|
||||||
<tr><td colspan="8" class="empty">Henter besøgende...</td></tr>
|
<tr><td colspan="11" class="empty">Henter besøgende...</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -4,6 +4,44 @@ const message = document.querySelector("#message");
|
|||||||
const csvButton = document.querySelector("#export-csv");
|
const csvButton = document.querySelector("#export-csv");
|
||||||
const printButton = document.querySelector("#print-list");
|
const printButton = document.querySelector("#print-list");
|
||||||
let visitors = [];
|
let visitors = [];
|
||||||
|
let sortKey = "createdAt";
|
||||||
|
let sortDirection = -1;
|
||||||
|
const sortCollator = new Intl.Collator("da", { numeric: true, sensitivity: "base" });
|
||||||
|
|
||||||
|
function sortVisitors() {
|
||||||
|
// Move existing rows so unsaved result inputs are retained.
|
||||||
|
const rows = new Map([...tableBody.querySelectorAll("tr")].map((row) =>
|
||||||
|
[row.querySelector(".result-form")?.dataset.id, row]));
|
||||||
|
const value = (visitor) => {
|
||||||
|
const row = rows.get(String(visitor.id));
|
||||||
|
if (sortKey === "result") return row?.querySelector('[name="result"]')?.value ?? visitor.result;
|
||||||
|
if (sortKey === "x10") return row?.querySelector('[name="x10"]')?.value ?? visitor.x10;
|
||||||
|
return visitor[sortKey];
|
||||||
|
};
|
||||||
|
visitors.sort((a, b) => {
|
||||||
|
const av = value(a), bv = value(b);
|
||||||
|
if (av === null || av === undefined || av === "") return bv === null || bv === undefined || bv === "" ? a.id - b.id : 1;
|
||||||
|
if (bv === null || bv === undefined || bv === "") return -1;
|
||||||
|
return sortDirection * sortCollator.compare(String(av), String(bv)) || a.id - b.id;
|
||||||
|
});
|
||||||
|
for (const visitor of visitors) {
|
||||||
|
const row = rows.get(String(visitor.id));
|
||||||
|
if (row) tableBody.append(row);
|
||||||
|
}
|
||||||
|
document.querySelectorAll("button[data-sort]").forEach((button) => {
|
||||||
|
const active = button.dataset.sort === sortKey;
|
||||||
|
button.closest("th").setAttribute("aria-sort", active ? (sortDirection === 1 ? "ascending" : "descending") : "none");
|
||||||
|
button.querySelector(".sort-arrow").textContent = active ? (sortDirection === 1 ? " ↑" : " ↓") : " ↕";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll("button[data-sort]").forEach((button) => {
|
||||||
|
button.addEventListener("click", () => {
|
||||||
|
sortDirection = sortKey === button.dataset.sort ? -sortDirection : 1;
|
||||||
|
sortKey = button.dataset.sort;
|
||||||
|
sortVisitors();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
function formatDateTime(value) {
|
function formatDateTime(value) {
|
||||||
return new Intl.DateTimeFormat("da-DK", {
|
return new Intl.DateTimeFormat("da-DK", {
|
||||||
@ -46,28 +84,71 @@ async function loadVisitors() {
|
|||||||
count.textContent = `${visitors.length} registreret`;
|
count.textContent = `${visitors.length} registreret`;
|
||||||
|
|
||||||
if (visitors.length === 0) {
|
if (visitors.length === 0) {
|
||||||
tableBody.innerHTML = '<tr><td colspan="8" class="empty">Ingen besøgende registreret endnu.</td></tr>';
|
tableBody.innerHTML = '<tr><td colspan="11" class="empty">Ingen besøgende registreret endnu.</td></tr>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
tableBody.innerHTML = visitors.map((visitor) => `
|
tableBody.innerHTML = visitors.map((visitor) => `
|
||||||
<tr>
|
<tr>
|
||||||
|
<td><strong>${visitor.id}</strong></td>
|
||||||
<td>${escapeHtml(visitor.name)}</td>
|
<td>${escapeHtml(visitor.name)}</td>
|
||||||
<td>${escapeHtml(visitor.streetAddress)}</td>
|
<td>${escapeHtml(visitor.streetAddress)}</td>
|
||||||
<td>${escapeHtml(visitor.postalCode)}</td>
|
<td>${escapeHtml(visitor.postalCode)}</td>
|
||||||
<td>${escapeHtml(visitor.city)}</td>
|
<td>${escapeHtml(visitor.city)}</td>
|
||||||
<td>${escapeHtml(visitor.birthday)}</td>
|
<td>${escapeHtml(visitor.birthday)}</td>
|
||||||
<td><a href="mailto:${escapeHtml(visitor.email)}">${escapeHtml(visitor.email)}</a></td>
|
<td>${visitor.email ? `<a href="mailto:${escapeHtml(visitor.email)}">${escapeHtml(visitor.email)}</a>` : ""}</td>
|
||||||
<td>${escapeHtml(formatDateTime(visitor.createdAt))}</td>
|
<td>${escapeHtml(formatDateTime(visitor.createdAt))}</td>
|
||||||
|
<td>
|
||||||
|
<form id="result-${visitor.id}" class="result-form no-print" data-id="${visitor.id}">
|
||||||
|
<input name="result" value="${escapeHtml(visitor.result)}" maxlength="100" aria-label="Resultat for ${escapeHtml(visitor.name)}" placeholder="Resultat">
|
||||||
|
<span class="result-status" role="status"></span>
|
||||||
|
</form>
|
||||||
|
<span class="print-result">${escapeHtml(visitor.result)}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input class="x10-input no-print" form="result-${visitor.id}" name="x10" type="number" min="0" step="1" value="${escapeHtml(visitor.x10)}" aria-label="X-10 for ${escapeHtml(visitor.name)}">
|
||||||
|
<button class="save-result-button no-print" type="submit" form="result-${visitor.id}">Gem</button>
|
||||||
|
<span class="print-result print-x10">${escapeHtml(visitor.x10)}</span>
|
||||||
|
</td>
|
||||||
<td class="no-print"><button type="button" class="delete-button" data-id="${visitor.id}" aria-label="Slet ${escapeHtml(visitor.name)}">Slet</button></td>
|
<td class="no-print"><button type="button" class="delete-button" data-id="${visitor.id}" aria-label="Slet ${escapeHtml(visitor.name)}">Slet</button></td>
|
||||||
</tr>
|
</tr>
|
||||||
`).join("");
|
`).join("");
|
||||||
|
sortVisitors();
|
||||||
} catch {
|
} catch {
|
||||||
showError("Der er ikke forbindelse til besøgslisten lige nu.");
|
showError("Der er ikke forbindelse til besøgslisten lige nu.");
|
||||||
count.textContent = "Fejl";
|
count.textContent = "Fejl";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tableBody.addEventListener("submit", async (event) => {
|
||||||
|
const form = event.target.closest(".result-form");
|
||||||
|
if (!form) return;
|
||||||
|
event.preventDefault();
|
||||||
|
const button = form.closest("tr").querySelector(".save-result-button");
|
||||||
|
if (button.disabled) return;
|
||||||
|
const status = form.querySelector(".result-status");
|
||||||
|
const result = form.elements.result.value.trim();
|
||||||
|
const x10 = form.elements.x10.value === "" ? null : Number(form.elements.x10.value);
|
||||||
|
button.disabled = true;
|
||||||
|
status.textContent = "Gemmer...";
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/visitors/${form.dataset.id}/result`, {
|
||||||
|
method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ result, x10 })
|
||||||
|
});
|
||||||
|
if (!response.ok) throw new Error("Resultat kunne ikke gemmes.");
|
||||||
|
const saved = await response.json();
|
||||||
|
const visitor = visitors.find((item) => String(item.id) === form.dataset.id);
|
||||||
|
if (visitor) { visitor.result = saved.result; visitor.x10 = saved.x10; }
|
||||||
|
form.elements.result.value = saved.result;
|
||||||
|
form.parentElement.querySelector(".print-result").textContent = saved.result;
|
||||||
|
form.elements.x10.value = saved.x10 ?? "";
|
||||||
|
form.closest("tr").querySelector(".print-x10").textContent = saved.x10 ?? "";
|
||||||
|
status.textContent = "Gemt";
|
||||||
|
} catch {
|
||||||
|
status.textContent = "Ikke gemt. Prøv igen.";
|
||||||
|
} finally { button.disabled = false; }
|
||||||
|
});
|
||||||
|
|
||||||
tableBody.addEventListener("click", async (event) => {
|
tableBody.addEventListener("click", async (event) => {
|
||||||
const button = event.target.closest("button[data-id]");
|
const button = event.target.closest("button[data-id]");
|
||||||
if (!button || button.disabled) return;
|
if (!button || button.disabled) return;
|
||||||
@ -97,9 +178,9 @@ function csvCell(value) {
|
|||||||
|
|
||||||
csvButton.addEventListener("click", () => {
|
csvButton.addEventListener("click", () => {
|
||||||
const rows = [
|
const rows = [
|
||||||
["Navn", "Vejnavn og husnummer", "Postnummer", "Postby", "Fødselsdag", "Email", "Registreret"],
|
["Skivenr.", "Navn", "Vejnavn og husnummer", "Postnummer", "Postby", "Fødselsdag", "Email", "Registreret", "Resultat", "X-10"],
|
||||||
...visitors.map((visitor) => [visitor.name, visitor.streetAddress, visitor.postalCode,
|
...visitors.map((visitor) => [visitor.id, visitor.name, visitor.streetAddress, visitor.postalCode,
|
||||||
visitor.city, visitor.birthday, visitor.email, formatDateTime(visitor.createdAt)])
|
visitor.city, visitor.birthday, visitor.email, formatDateTime(visitor.createdAt), visitor.result, visitor.x10])
|
||||||
];
|
];
|
||||||
const csv = "\uFEFF" + rows.map((row) => row.map(csvCell).join(";")).join("\r\n") + "\r\n";
|
const csv = "\uFEFF" + rows.map((row) => row.map(csvCell).join(";")).join("\r\n") + "\r\n";
|
||||||
const url = URL.createObjectURL(new Blob([csv], { type: "text/csv;charset=utf-8" }));
|
const url = URL.createObjectURL(new Blob([csv], { type: "text/csv;charset=utf-8" }));
|
||||||
|
|||||||
@ -35,9 +35,13 @@ form.addEventListener("submit", async (event) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const registration = await response.json();
|
||||||
form.reset();
|
form.reset();
|
||||||
citySuggestions.replaceChildren();
|
citySuggestions.replaceChildren();
|
||||||
showMessage("Tak, du er registreret.", "success");
|
showMessage(`Tak, du er registreret. Dit skivenummer er ${registration.number}. Skriv nummeret på dine skiver.`, "success");
|
||||||
|
const numberText = document.createElement("strong");
|
||||||
|
numberText.textContent = `Dit skivenummer er ${registration.number}`;
|
||||||
|
message.replaceChildren("Tak, du er registreret. ", numberText, ". Skriv nummeret på dine skiver.");
|
||||||
} catch {
|
} catch {
|
||||||
showMessage("Der er ikke forbindelse til registreringen lige nu.", "error");
|
showMessage("Der er ikke forbindelse til registreringen lige nu.", "error");
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@ -13,10 +13,10 @@
|
|||||||
<p class="eyebrow">Vestfalster Skytteforening</p>
|
<p class="eyebrow">Vestfalster Skytteforening</p>
|
||||||
<h1>Skydesportens Dag</h1>
|
<h1>Skydesportens Dag</h1>
|
||||||
<p class="intro">Registrer dig som besøgende. Oplysningerne bruges kun i forbindelse med arrangementet.</p>
|
<p class="intro">Registrer dig som besøgende. Oplysningerne bruges kun i forbindelse med arrangementet.</p>
|
||||||
<div id="message" class="notice" hidden></div>
|
<div id="message" class="notice" role="status" hidden></div>
|
||||||
<form id="registration-form" class="form">
|
<form id="registration-form" class="form">
|
||||||
<label>Navn
|
<label>Navn
|
||||||
<input name="name" autocomplete="name" required maxlength="160">
|
<input name="name" autocomplete="name" required maxlength="160" autofocus>
|
||||||
</label>
|
</label>
|
||||||
<label>Vejnavn og husnummer
|
<label>Vejnavn og husnummer
|
||||||
<input name="streetAddress" autocomplete="street-address" required maxlength="240">
|
<input name="streetAddress" autocomplete="street-address" required maxlength="240">
|
||||||
@ -33,8 +33,8 @@
|
|||||||
<label>Fødselsdag
|
<label>Fødselsdag
|
||||||
<input name="birthday" type="date" required>
|
<input name="birthday" type="date" required>
|
||||||
</label>
|
</label>
|
||||||
<label>Email
|
<label>Email (valgfri)
|
||||||
<input name="email" type="email" autocomplete="email" required maxlength="240">
|
<input name="email" type="email" autocomplete="email" maxlength="240">
|
||||||
</label>
|
</label>
|
||||||
<button type="submit">Registrer besøgende</button>
|
<button type="submit">Registrer besøgende</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@ -210,9 +210,21 @@ a {
|
|||||||
.admin-actions {
|
.admin-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 12px;
|
gap: 6px;
|
||||||
margin-bottom: 20px;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
.toolbar-right { display: flex; align-items: center; justify-content: flex-end; gap: 12px; flex-wrap: wrap; }
|
||||||
|
.admin-actions button { margin: 0; padding: 8px 10px; font-size: 13px; }
|
||||||
|
th .sort-button { background: transparent; color: inherit; margin: 0; padding: 0; font-size: inherit; text-align: left; }
|
||||||
|
th .sort-button:hover { color: var(--accent); }
|
||||||
|
.sort-arrow { white-space: nowrap; }
|
||||||
|
.result-form { display: flex; flex-wrap: wrap; gap: 6px; min-width: 160px; }
|
||||||
|
.result-form input { width: 100px; flex: 1; padding: 8px; }
|
||||||
|
.result-form button { margin: 0; padding: 8px 10px; }
|
||||||
|
.result-status { width: 100%; font-size: 12px; }
|
||||||
|
.print-result { display: none; }
|
||||||
|
.x10-input { width: 80px; padding: 8px; }
|
||||||
|
.save-result-button { margin: 0 0 0 6px; padding: 8px 10px; }
|
||||||
|
|
||||||
.participant-map { margin: 0 0 24px; }
|
.participant-map { margin: 0 0 24px; }
|
||||||
.participant-map h2 { margin: 0 0 8px; font-size: 22px; }
|
.participant-map h2 { margin: 0 0 8px; font-size: 22px; }
|
||||||
@ -230,6 +242,7 @@ a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media print {
|
@media print {
|
||||||
|
.print-result { display: inline; }
|
||||||
@page { size: A4 landscape; margin: 12mm; }
|
@page { size: A4 landscape; margin: 12mm; }
|
||||||
body { min-height: 0; background: #fff; color: #000; }
|
body { min-height: 0; background: #fff; color: #000; }
|
||||||
.shell { width: 100%; margin: 0; padding: 0; }
|
.shell { width: 100%; margin: 0; padding: 0; }
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user