I'm posting a second version with addresses.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Adressetiketten mit Modal-Setup</title>
<style>
:root {
--top-offset-mm: 5mm;
--etikett-hoehe-mm: 67.7mm;
--etikett-breite-mm: 99.1mm;
--etiketten-gap-mm: 2mm;
--seiten-header-height-mm: 30mm;
--title-in-header-top: 10mm;
--etikett-date-top: 10px;
--etikett-line-top: 30px;
--etikett-title-top: 40px;
--etikett-footer-bottom: 10px;
--etiketten-spalten: 2;
--etiketten-zeilen: 4;
--seiten-breite-mm: 210mm;
--seiten-hoehe-mm: 297mm;
--test-mode: 0;
}
@media print {
@page { size: A4 portrait; margin: 0; }
#configurator-button, dialog, #liveTestModeToggleLabel { display: none !important; }
}
body { font-family: Arial, sans-serif; margin: 0; padding: 0; }
.page {
width: var(--seiten-breite-mm);
height: var(--seiten-hoehe-mm);
padding: var(--top-offset-mm) 5mm 10mm 5mm;
box-sizing: border-box;
page-break-after: always;
}
.seiten-header {
height: var(--seiten-header-height-mm);
position: relative;
box-sizing: border-box;
border: calc(var(--test-mode) * 2px) dashed orange;
}
.seitenueberschrift {
position: absolute;
top: var(--title-in-header-top);
left: 0;
right: 0;
text-align: center;
font-size: 12pt;
font-weight: bold;
border: calc(var(--test-mode) * 1px) dashed blue;
}
.etiketten-container {
display: grid;
grid-template-columns: repeat(var(--etiketten-spalten), var(--etikett-breite-mm));
grid-template-rows: repeat(var(--etiketten-zeilen), var(--etikett-hoehe-mm));
gap: var(--etiketten-gap-mm);
width: 100%;
box-sizing: border-box;
border: calc(var(--test-mode) * 2px) dashed red;
}
.etikett {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
text-align: center;
font-weight: bold;
width: var(--etikett-breite-mm);
height: var(--etikett-hoehe-mm);
padding: 2mm;
box-sizing: border-box;
border: 1px solid #ddd;
outline: calc(var(--test-mode) * 2px) dashed green;
}
.date {
font-size: 10pt;
position: absolute;
top: var(--etikett-date-top);
right: 10px;
}
.line {
position: absolute;
top: var(--etikett-line-top);
left: 0;
right: 0;
height: 1px;
background-color: black;
}
.title {
font-size: 12pt;
text-transform: uppercase;
position: absolute;
top: var(--etikett-title-top);
left: 0;
right: 0;
}
.footer {
font-size: 9pt;
position: absolute;
bottom: var(--etikett-footer-bottom);
left: 50%;
transform: translateX(-50%);
}
.address {
margin-top: 60px;
font-size: 10pt;
text-align: center;
line-height: 1.4;
}
#configurator-button { margin: 10px; }
#liveTestModeToggleLabel { margin: 10px; font-size: 14px; }
dialog {
border: 1px solid #ccc;
padding: 20px;
width: 600px;
}
.form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
fieldset { border: 1px solid #ccc; padding: 10px; }
legend { font-weight: bold; margin-bottom: 5px; }
label { display: flex; flex-direction: column; margin-bottom: 10px; }
</style>
</head>
<body>
<button id="configurator-button" onclick="document.getElementById('configurator').showModal()">⚙️ Einstellungen</button>
<label id="liveTestModeToggleLabel">
<input type="checkbox" id="liveTestModeToggle"> Testmodus (live)
</label>
<dialog id="configurator">
<form method="dialog">
<div class="form-grid">
<fieldset>
<legend>Seite</legend>
<label>Breite (mm)<input type="number" id="seitenBreite" value="210"></label>
<label>Höhe (mm)<input type="number" id="seitenHoehe" value="297"></label>
<label>Header-Höhe (mm)<input type="number" id="headerHeight" value="30"></label>
</fieldset>
<fieldset>
<legend>Etiketten</legend>
<label>Spalten<input type="number" id="spalten" value="2"></label>
<label>Zeilen<input type="number" id="zeilen" value="4"></label>
<label>Breite (mm)<input type="number" id="width" value="99.1"></label>
<label>Höhe (mm)<input type="number" id="height" value="67.7"></label>
<label>Abstand (mm)<input type="number" id="gap" value="2"></label>
</fieldset>
<fieldset>
<legend>Layout</legend>
<label>Top-Offset (mm)<input type="number" id="topOffset" value="5"></label>
<label>Überschrift-Top (mm)<input type="number" id="titleInHeaderTop" value="10"></label>
<label>Date-Top (px)<input type="number" id="etikettDateTop" value="10"></label>
<label>Line-Top (px)<input type="number" id="etikettLineTop" value="30"></label>
<label>Title-Top (px)<input type="number" id="etikettTitleTop" value="40"></label>
<label>Footer-Bottom (px)<input type="number" id="etikettFooterBottom" value="10"></label>
</fieldset>
<fieldset>
<legend>Optionen</legend>
<label><input type="checkbox" id="pageBreakOnOrg" checked> Seitenumbruch bei Organisation</label>
<label><input type="checkbox" id="testModeToggle"> Testmodus</label>
</fieldset>
</div>
<menu>
<button type="button" onclick="applySettings()">✅ Übernehmen</button>
<button type="button" onclick="document.getElementById('configurator').close()">✖️ Abbrechen</button>
</menu>
</form>
</dialog>
<div id="seitenContainer"></div>
<script>
let einstellungen = {};
let etikettenDaten = [];
function setCSSVars(cfg) {
const r = document.documentElement;
r.style.setProperty('--top-offset-mm', cfg.topOffsetMm + 'mm');
r.style.setProperty('--etikett-hoehe-mm', cfg.etikettenHoeheMm + 'mm');
r.style.setProperty('--etikett-breite-mm', cfg.etikettenBreiteMm + 'mm');
r.style.setProperty('--etiketten-gap-mm', cfg.etikettenGapMm + 'mm');
r.style.setProperty('--seiten-header-height-mm', cfg.headerHeightMm + 'mm');
r.style.setProperty('--title-in-header-top', cfg.titleInHeaderTopMm + 'mm');
r.style.setProperty('--etikett-date-top', cfg.etikettDateTop + 'px');
r.style.setProperty('--etikett-line-top', cfg.etikettLineTop + 'px');
r.style.setProperty('--etikett-title-top', cfg.etikettTitleTop + 'px');
r.style.setProperty('--etikett-footer-bottom', cfg.etikettFooterBottom + 'px');
r.style.setProperty('--etiketten-spalten', cfg.spalten);
r.style.setProperty('--etiketten-zeilen', cfg.zeilen);
r.style.setProperty('--seiten-breite-mm', cfg.seitenBreite + 'mm');
r.style.setProperty('--seiten-hoehe-mm', cfg.seitenHoehe + 'mm');
r.style.setProperty('--test-mode', cfg.testModeEnabled ? 1 : 0);
}
function applySettings() {
const cfg = {
topOffsetMm: parseFloat(document.getElementById("topOffset").value),
etikettenHoeheMm: parseFloat(document.getElementById("height").value),
etikettenBreiteMm: parseFloat(document.getElementById("width").value),
etikettenGapMm: parseFloat(document.getElementById("gap").value),
spalten: parseInt(document.getElementById("spalten").value),
zeilen: parseInt(document.getElementById("zeilen").value),
seitenBreite: parseFloat(document.getElementById("seitenBreite").value),
seitenHoehe: parseFloat(document.getElementById("seitenHoehe").value),
headerHeightMm: parseFloat(document.getElementById("headerHeight").value),
titleInHeaderTopMm: parseFloat(document.getElementById("titleInHeaderTop").value),
etikettDateTop: parseFloat(document.getElementById("etikettDateTop").value),
etikettLineTop: parseFloat(document.getElementById("etikettLineTop").value),
etikettTitleTop: parseFloat(document.getElementById("etikettTitleTop").value),
etikettFooterBottom: parseFloat(document.getElementById("etikettFooterBottom").value),
testModeEnabled: document.getElementById("testModeToggle").checked,
pageBreakOnOrg: document.getElementById("pageBreakOnOrg").checked
};
einstellungen = cfg;
setCSSVars(cfg);
document.getElementById("liveTestModeToggle").checked = cfg.testModeEnabled;
renderEtiketten();
}
function createEtikett(item) {
const e = document.createElement("div");
e.className = "etikett";
e.innerHTML = `
<div class="date">${new Date().toLocaleDateString("de-DE")}</div>
<div class="line"></div>
<div class="title">${item.firma}</div>
<div class="address">
${item.abteilung}<br>${item.name}<br>${item.strasse}<br>${item.plz} ${item.ort}
</div>
<div class="footer">Etikettentest</div>
`;
return e;
}
function renderEtiketten() {
const c = document.getElementById("seitenContainer");
c.innerHTML = "";
const proSeite = einstellungen.spalten * einstellungen.zeilen;
if (einstellungen.pageBreakOnOrg) {
const grouped = {};
for (const item of etikettenDaten) {
const org = item.organisation || "Allgemein";
if (!grouped[org]) grouped[org] = [];
grouped[org].push(item);
}
for (const org in grouped) {
const list = grouped[org];
for (let i = 0; i < list.length; i += proSeite) {
const page = document.createElement("div");
page.className = "page";
page.innerHTML = `
<div class="seiten-header"><div class="seitenueberschrift">${org}</div></div>
<div class="etiketten-container"></div>`;
list.slice(i, i + proSeite).forEach(item =>
page.querySelector(".etiketten-container").appendChild(createEtikett(item))
);
c.appendChild(page);
}
}
} else {
for (let i = 0; i < etikettenDaten.length; i += proSeite) {
const page = document.createElement("div");
page.className = "page";
page.innerHTML = `
<div class="seiten-header"><div class="seitenueberschrift">Etiketten</div></div>
<div class="etiketten-container"></div>`;
etikettenDaten.slice(i, i + proSeite).forEach(item =>
page.querySelector(".etiketten-container").appendChild(createEtikett(item))
);
c.appendChild(page);
}
}
}
document.getElementById("liveTestModeToggle").addEventListener("change", e => {
document.documentElement.style.setProperty('--test-mode', e.target.checked ? 1 : 0);
});
fetch("organisation.json")
.then(res => res.json())
.then(data => {
etikettenDaten = data;
applySettings();
});
</script>
</body>
</html>
[
{ "organisation": "Sportverein A", "firma": "SV A", "abteilung": "Turnen", "name": "Anna Aktiv", "strasse": "Sportweg 1", "plz": "12345", "ort": "Fitstadt" },
{ "organisation": "Sportverein A", "firma": "SV A", "abteilung": "Leichtathletik", "name": "Lars Läufer", "strasse": "Laufstraße 2", "plz": "12345", "ort": "Fitstadt" },
{ "organisation": "Sportverein A", "firma": "SV A", "abteilung": "Jugend", "name": "Jana Jung", "strasse": "Jugendplatz 3", "plz": "12345", "ort": "Fitstadt" },
{ "organisation": "Sportverein B", "firma": "SV B", "abteilung": "Basketball", "name": "Ben Ball", "strasse": "Hallenweg 1", "plz": "23456", "ort": "Teamsburg" },
{ "organisation": "Sportverein B", "firma": "SV B", "abteilung": "Tischtennis", "name": "Tina Tisch", "strasse": "Plattenstraße 5", "plz": "23456", "ort": "Teamsburg" },
{ "organisation": "Sportverein B", "firma": "SV B", "abteilung": "Schwimmen", "name": "Sven Schwimm", "strasse": "Beckenstraße 9", "plz": "23456", "ort": "Teamsburg" },
{ "organisation": "Sportverein B", "firma": "SV B", "abteilung": "Boxen", "name": "Bernd Boxer", "strasse": "Ringallee 6", "plz": "23456", "ort": "Teamsburg" },
{ "organisation": "Sportverein B", "firma": "SV B", "abteilung": "Radsport", "name": "Rita Rad", "strasse": "Pedalweg 8", "plz": "23456", "ort": "Teamsburg" },
{ "organisation": "Sportverein B", "firma": "SV B", "abteilung": "Wandern", "name": "Willi Weg", "strasse": "Pfadstraße 4", "plz": "23456", "ort": "Teamsburg" },
{ "organisation": "Sportverein B", "firma": "SV B", "abteilung": "Klettern", "name": "Klara Kliff", "strasse": "Bergweg 2", "plz": "23456", "ort": "Teamsburg" },
{ "organisation": "Sportverein C", "firma": "SV C", "abteilung": "Allgemein", "name": "Caro Coach", "strasse": "Feldstraße 9", "plz": "34567", "ort": "Sportstadt" },
{ "organisation": "Sportverein C", "firma": "SV C", "abteilung": "Yoga", "name": "Yvonne Yoga", "strasse": "Balanceweg 3", "plz": "34567", "ort": "Sportstadt" },
{ "organisation": "Sportverein C", "firma": "SV C", "abteilung": "Pilates", "name": "Paula Pilates", "strasse": "Flexstraße 5", "plz": "34567", "ort": "Sportstadt" },
{ "organisation": "Sportverein C", "firma": "SV C", "abteilung": "Tanzen", "name": "Tanja Tanz", "strasse": "Rhythmusweg 6", "plz": "34567", "ort": "Sportstadt" },
{ "organisation": "Sportverein C", "firma": "SV C", "abteilung": "Badminton", "name": "Bastian Feder", "strasse": "Netzstraße 7", "plz": "34567", "ort": "Sportstadt" },
{ "organisation": "Sportverein C", "firma": "SV C", "abteilung": "Fechten", "name": "Felix Florett", "strasse": "Duellweg 2", "plz": "34567", "ort": "Sportstadt" },
{ "organisation": "Sportverein C", "firma": "SV C", "abteilung": "Judo", "name": "Julia Griff", "strasse": "Tatamiweg 4", "plz": "34567", "ort": "Sportstadt" },
{ "organisation": "Sportverein C", "firma": "SV C", "abteilung": "Volleyball", "name": "Valerie Netz", "strasse": "Sprungstraße 8", "plz": "34567", "ort": "Sportstadt" },
{ "organisation": "Sportverein C", "firma": "SV C", "abteilung": "Handball", "name": "Heiko Wurf", "strasse": "Torweg 9", "plz": "34567", "ort": "Sportstadt" },
{ "organisation": "Sportverein C", "firma": "SV C", "abteilung": "Rudern", "name": "Ralf Ruder", "strasse": "Uferstraße 10", "plz": "34567", "ort": "Sportstadt" }
]