Add CSV file upload and auto-delimiter detection for pilot import

Allows uploading a CSV file directly instead of only pasting text, and
auto-detects comma vs semicolon separators to support more locales.
This commit is contained in:
Jan Meinl
2026-08-19 15:32:26 +02:00
parent 4b5f0eb326
commit 65bb2b351f
10 changed files with 97 additions and 16 deletions
+12 -3
View File
@@ -48,11 +48,20 @@ const API = {
createPilot: (id, b) => api("POST", `/api/competitions/${id}/pilots`, b),
updatePilot: (id, pid, b) => api("PATCH", `/api/competitions/${id}/pilots/${pid}`, b),
deletePilot: (id, pid) => api("DELETE", `/api/competitions/${id}/pilots/${pid}`),
importPilots: async (id, csv) => {
importPilots: async (id, data) => {
let body, headers = {};
if (data instanceof File) {
const form = new FormData();
form.append("file", data);
body = form;
} else {
headers["Content-Type"] = "text/csv";
body = data;
}
const res = await fetch(apiURL(`/api/competitions/${id}/pilots/import`), {
method: "POST",
headers: { "Content-Type": "text/csv" },
body: csv,
headers,
body,
credentials: "include",
});
if (!res.ok) throw new Error("import_failed");
+19 -1
View File
@@ -953,15 +953,33 @@
function openImportModal() {
const backdrop = el("div", { class: "modal-backdrop",
onclick: (e) => { if (e.target === backdrop) backdrop.remove(); } });
let selectedFile = null;
const fileNameSpan = el("span", { class: "muted small", style: { marginLeft: "0.5rem" } }, "");
const fileInput = el("input", { type: "file", accept: ".csv,text/csv", style: { display: "none" },
onchange: (e) => {
selectedFile = e.target.files[0] || null;
fileNameSpan.textContent = selectedFile ? selectedFile.name : "";
},
});
const textarea = el("textarea", { placeholder: t("csv_paste"), style: { width: "100%", minHeight: "180px" } });
backdrop.appendChild(el("div", { class: "modal" },
el("h3", null, t("import_csv")),
el("p", { class: "muted small", style: { marginBottom: "0.5rem" } }, t("csv_separator_hint")),
el("div", { style: { marginBottom: "0.75rem", display: "flex", alignItems: "center" } },
fileInput,
el("button", { onclick: () => fileInput.click() }, t("upload_file")),
fileNameSpan,
),
textarea,
el("div", { class: "row", style: { justifyContent: "flex-end", marginTop: "1rem" } },
el("button", { onclick: () => backdrop.remove() }, t("cancel")),
el("button", { class: "primary", onclick: async () => {
try {
await API.importPilots(competitionId, textarea.value);
const data = selectedFile || textarea.value;
if (!data || (typeof data === "string" && !data.trim())) {
alert(t("csv_empty")); return;
}
await API.importPilots(competitionId, data);
await loadPilots();
backdrop.remove();
render();
+4 -1
View File
@@ -142,5 +142,8 @@
"confirm_delete_competition": "Diesen Wettbewerb löschen? Alle Piloten, Strafen und Mitglieder werden unwiderruflich entfernt.",
"confirm_delete_competition_named": "Wettbewerb '{name}' löschen? Alle Piloten, Strafen und Mitglieder werden unwiderruflich entfernt.",
"backup_written": "Sicherung erstellt: {file}",
"competition_closed": "Wettbewerb ist beendet"
"competition_closed": "Wettbewerb ist beendet",
"upload_file": "Datei hochladen",
"csv_separator_hint": "Komma (,) und Semikolon (;) werden automatisch erkannt.",
"csv_empty": "Bitte CSV einfügen oder eine Datei wählen."
}
+4 -1
View File
@@ -142,5 +142,8 @@
"confirm_delete_competition": "Delete this competition? All pilots, penalties and members will be permanently removed.",
"confirm_delete_competition_named": "Delete competition '{name}'? All pilots, penalties and members will be permanently removed.",
"backup_written": "Backup written: {file}",
"competition_closed": "Competition is closed"
"competition_closed": "Competition is closed",
"upload_file": "Upload file",
"csv_separator_hint": "Comma (,) and semicolon (;) are detected automatically.",
"csv_empty": "Please paste CSV or choose a file."
}
+4 -1
View File
@@ -79,5 +79,8 @@
"save_settings": "Guardar ajustes",
"saved": "Guardado",
"yes": "Sí",
"no": "No"
"no": "No",
"upload_file": "Subir archivo",
"csv_separator_hint": "La coma (,) y el punto y coma (;) se detectan automáticamente.",
"csv_empty": "Por favor, pegue CSV o elija un archivo."
}
+4 -1
View File
@@ -79,5 +79,8 @@
"save_settings": "Enregistrer",
"saved": "Enregistré",
"yes": "Oui",
"no": "Non"
"no": "Non",
"upload_file": "Téléverser un fichier",
"csv_separator_hint": "La virgule (,) et le point-virgule (;) sont détectés automatiquement.",
"csv_empty": "Veuillez coller du CSV ou choisir un fichier."
}
+4 -1
View File
@@ -79,5 +79,8 @@
"save_settings": "Zapisz ustawienia",
"saved": "Zapisano",
"yes": "Tak",
"no": "Nie"
"no": "Nie",
"upload_file": "Prześlij plik",
"csv_separator_hint": "Przecinek (,) i średnik (;) są wykrywane automatycznie.",
"csv_empty": "Wklej CSV lub wybierz plik."
}
+4 -1
View File
@@ -79,5 +79,8 @@
"save_settings": "Guardar definições",
"saved": "Guardado",
"yes": "Sim",
"no": "Não"
"no": "Não",
"upload_file": "Carregar ficheiro",
"csv_separator_hint": "A vírgula (,) e o ponto e vírgula (;) são detectados automaticamente.",
"csv_empty": "Por favor, cole CSV ou escolha um ficheiro."
}
+4 -1
View File
@@ -79,5 +79,8 @@
"save_settings": "Сохранить настройки",
"saved": "Сохранено",
"yes": "Да",
"no": "Нет"
"no": "Нет",
"upload_file": "Загрузить файл",
"csv_separator_hint": "Запятая (,) и точка с запятой (;) определяются автоматически.",
"csv_empty": "Вставьте CSV или выберите файл."
}