Compare commits

...

2 Commits

Author SHA1 Message Date
elpatron ca3ed44ff3 fix: Termin-Uhrzeit ohne AM/PM (Stunde/Minute als Zahleneingabe)
Ersetzt type=time durch number 0–23 / 0–59, damit kein 12h-Picker mehr erscheint.
Validierung, URL-Parameter und Vorschau angepasst; lang de-DE im Termin-Block.

Made-with: Cursor
2026-04-16 08:17:49 +02:00
elpatron d5cd4427d8 feat: Kalender-Termine als QR (iCal Europe/Berlin) und UX für Datum/Zeit
- Modus Text/WLAN/Termin; VEVENT mit TZID Europe/Berlin und VTIMEZONE
- Eingabe: separates Datum und 24h-Uhrzeit; Vorschau TT/MM/JJJJ
- URL-Parameter und Teilen für Terminfelder
- Docker Compose: Healthcheck für Caddy-Container

Made-with: Cursor
2026-04-16 08:15:29 +02:00
3 changed files with 715 additions and 73 deletions
+6 -2
View File
@@ -1,5 +1,3 @@
version: '3.8'
services: services:
qr-generator: qr-generator:
build: . build: .
@@ -7,3 +5,9 @@ services:
- "8080:80" - "8080:80"
container_name: qr-generator container_name: qr-generator
restart: unless-stopped restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1/"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
+118 -3
View File
@@ -1,6 +1,6 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <html lang="de"><head><meta http-equiv="Content-Type" content="text/html; 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">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; object-src 'none';"> <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; object-src 'none';">
@@ -53,7 +53,7 @@
color: #374151; color: #374151;
} }
input, select { input, select, textarea {
width: 100%; width: 100%;
padding: 10px; padding: 10px;
margin-bottom: 15px; margin-bottom: 15px;
@@ -64,7 +64,12 @@
transition: border-color 0.2s ease; transition: border-color 0.2s ease;
} }
input:focus, select:focus { textarea {
min-height: 80px;
resize: vertical;
}
input:focus, select:focus, textarea:focus {
outline: none; outline: none;
border-color: #1e3a8a; border-color: #1e3a8a;
box-shadow: 0 0 0 3px rgba(30, 58, 138, 0.1); box-shadow: 0 0 0 3px rgba(30, 58, 138, 0.1);
@@ -295,6 +300,53 @@
text-decoration: underline; text-decoration: underline;
color: #1e40af; color: #1e40af;
} }
.event-berlin-preview {
font-size: 14px;
color: #4b5563;
margin: -8px 0 16px 0;
line-height: 1.5;
}
.event-berlin-preview:empty {
display: none;
}
.event-datetime-hint {
font-size: 13px;
color: #6b7280;
margin: -4px 0 12px 0;
line-height: 1.4;
}
.time-24h-block {
margin-bottom: 15px;
}
.time-24h-block .time-24h-row {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.time-24h-block input[type="number"] {
width: 4.25rem;
margin-bottom: 0;
text-align: center;
}
.time-24h-sep {
font-weight: 600;
color: #374151;
user-select: none;
}
.time-24h-hint {
font-size: 12px;
color: #6b7280;
margin-top: 4px;
}
</style> </style>
</head> </head>
<body> <body>
@@ -303,12 +355,22 @@
<button id="info-button" class="info-button" aria-label="Information">?</button> <button id="info-button" class="info-button" aria-label="Information">?</button>
<div class="input-section"> <div class="input-section">
<label for="content-mode">QR-Inhalt:</label>
<select id="content-mode">
<option value="text">Text oder URL</option>
<option value="wifi">WLAN</option>
<option value="event">Termin (Kalender)</option>
</select>
<div id="section-text" class="content-section">
<label for="text">Text oder URL:</label> <label for="text">Text oder URL:</label>
<div class="text-input-wrapper"> <div class="text-input-wrapper">
<input type="text" id="text" placeholder="https://qr.medisoftware.org" value="" autofocus=""> <input type="text" id="text" placeholder="https://qr.medisoftware.org" value="" autofocus="">
<button type="button" id="clear-text" class="clear-button" aria-label="Clear text" style="display: none;"></button> <button type="button" id="clear-text" class="clear-button" aria-label="Clear text" style="display: none;"></button>
</div> </div>
</div>
<div id="section-wifi" class="content-section" style="display: none;">
<div class="options-row"> <div class="options-row">
<div class="options-col"> <div class="options-col">
<label for="wifi-ssid">WLAN SSID:</label> <label for="wifi-ssid">WLAN SSID:</label>
@@ -320,6 +382,56 @@
<input type="password" id="wifi-password" placeholder=""> <input type="password" id="wifi-password" placeholder="">
</div> </div>
</div> </div>
</div>
<div id="section-event" class="content-section" style="display: none;" lang="de-DE">
<label for="event-title">Titel:</label>
<input type="text" id="event-title" placeholder="" autocomplete="off">
<p class="event-datetime-hint">Datum über das Kalenderfeld; Uhrzeit als <strong>Stunden 023</strong> und <strong>Minuten 059</strong> (reines 24h-Format, kein AM/PM). Ortszeit <strong>Europe/Berlin</strong>.</p>
<div class="options-row">
<div class="options-col">
<label for="event-start-date">Beginn Datum:</label>
<input type="date" id="event-start-date" autocomplete="off">
</div>
<div class="options-col">
<div class="time-24h-block">
<span class="label" style="display:block; margin-bottom:5px; font-weight:500; color:#374151;">Beginn Uhrzeit (24h):</span>
<div class="time-24h-row" role="group" aria-label="Beginn Uhrzeit 24 Stunden">
<input type="number" id="event-start-hour" min="0" max="23" step="1" placeholder="HH" inputmode="numeric" autocomplete="off" aria-label="Stunde Beginn, 0 bis 23">
<span class="time-24h-sep">:</span>
<input type="number" id="event-start-minute" min="0" max="59" step="1" placeholder="MM" inputmode="numeric" autocomplete="off" aria-label="Minute Beginn, 0 bis 59">
</div>
<p class="time-24h-hint">Stunde 023, Minute 059</p>
</div>
</div>
</div>
<div class="options-row">
<div class="options-col">
<label for="event-end-date">Ende (optional) Datum:</label>
<input type="date" id="event-end-date" autocomplete="off">
</div>
<div class="options-col">
<div class="time-24h-block">
<span class="label" style="display:block; margin-bottom:5px; font-weight:500; color:#374151;">Ende (optional) Uhrzeit (24h):</span>
<div class="time-24h-row" role="group" aria-label="Ende Uhrzeit 24 Stunden">
<input type="number" id="event-end-hour" min="0" max="23" step="1" placeholder="HH" inputmode="numeric" autocomplete="off" aria-label="Stunde Ende, 0 bis 23">
<span class="time-24h-sep">:</span>
<input type="number" id="event-end-minute" min="0" max="59" step="1" placeholder="MM" inputmode="numeric" autocomplete="off" aria-label="Minute Ende, 0 bis 59">
</div>
<p class="time-24h-hint">Stunde 023, Minute 059</p>
</div>
</div>
</div>
<p class="event-berlin-preview" id="event-berlin-preview" aria-live="polite"></p>
<label for="event-location">Ort (optional):</label>
<input type="text" id="event-location" placeholder="" autocomplete="off">
<label for="event-description">Beschreibung (optional):</label>
<textarea id="event-description" placeholder="" rows="3"></textarea>
</div>
<div class="options-row"> <div class="options-row">
<div class="options-col"> <div class="options-col">
@@ -393,6 +505,9 @@
<p>This is the web we were trying to build.</p> <p>This is the web we were trying to build.</p>
<p>Be excellent to each other.</p> <p>Be excellent to each other.</p>
<h3>Modi</h3>
<p>Unter <strong>QR-Inhalt</strong> wählst du, was kodiert wird: freier Text oder URL, WLAN-Zugangsdaten (WIFI-QR) oder einen <strong>Kalendertermin</strong>. Im Modus „Termin“ erzeugt der QR-Code einen Standard-iCalendar-Eintrag (VEVENT), den viele Smartphones beim Scannen in die Kalender-App übernehmen. Datumseingabe als <strong>TT/MM/JJJJ</strong>, Uhrzeit im <strong>24-Stunden-Format</strong>; die Zeitzone ist <strong>Europe/Berlin</strong>. Es wird nichts an einen Server gesendet.</p>
</div> </div>
</div> </div>
+569 -46
View File
@@ -2,8 +2,21 @@ document.addEventListener('DOMContentLoaded', function() {
const textInput = document.getElementById('text'); const textInput = document.getElementById('text');
const clearTextBtn = document.getElementById('clear-text'); const clearTextBtn = document.getElementById('clear-text');
const contentModeSelect = document.getElementById('content-mode');
const sectionText = document.getElementById('section-text');
const sectionWifi = document.getElementById('section-wifi');
const sectionEvent = document.getElementById('section-event');
const wifiSsidInput = document.getElementById('wifi-ssid'); const wifiSsidInput = document.getElementById('wifi-ssid');
const wifiPasswordInput = document.getElementById('wifi-password'); const wifiPasswordInput = document.getElementById('wifi-password');
const eventTitleInput = document.getElementById('event-title');
const eventStartDateInput = document.getElementById('event-start-date');
const eventStartHourInput = document.getElementById('event-start-hour');
const eventStartMinuteInput = document.getElementById('event-start-minute');
const eventEndDateInput = document.getElementById('event-end-date');
const eventEndHourInput = document.getElementById('event-end-hour');
const eventEndMinuteInput = document.getElementById('event-end-minute');
const eventLocationInput = document.getElementById('event-location');
const eventDescriptionInput = document.getElementById('event-description');
const sizeSelect = document.getElementById('size'); const sizeSelect = document.getElementById('size');
const errorCorrectionSelect = document.getElementById('errorCorrection'); const errorCorrectionSelect = document.getElementById('errorCorrection');
const foregroundColor = document.getElementById('foreground'); const foregroundColor = document.getElementById('foreground');
@@ -18,6 +31,11 @@ document.addEventListener('DOMContentLoaded', function() {
const titleElement = document.getElementById('title'); const titleElement = document.getElementById('title');
const shareBtn = document.getElementById('share'); const shareBtn = document.getElementById('share');
const shareHint = document.getElementById('share-hint'); const shareHint = document.getElementById('share-hint');
const eventBerlinPreview = document.getElementById('event-berlin-preview');
const MAX_EVENT_TITLE = 2000;
const MAX_EVENT_LOCATION = 1000;
const MAX_EVENT_DESCRIPTION = 8000;
// Title Easter egg // Title Easter egg
let titleClickCount = 0; let titleClickCount = 0;
@@ -62,13 +80,44 @@ document.addEventListener('DOMContentLoaded', function() {
} }
}); });
function toggleContentSections() {
const mode = contentModeSelect.value;
sectionText.style.display = mode === 'text' ? 'block' : 'none';
sectionWifi.style.display = mode === 'wifi' ? 'block' : 'none';
sectionEvent.style.display = mode === 'event' ? 'block' : 'none';
}
contentModeSelect.addEventListener('change', function() {
toggleContentSections();
updateBerlinPreview();
generateQRCode();
if (this.value === 'text') {
textInput.focus();
} else if (this.value === 'wifi') {
wifiSsidInput.focus();
} else if (this.value === 'event') {
eventTitleInput.focus();
}
});
// Set random compliment as default text and select it // Set random compliment as default text and select it
textInput.value = 'https://medisoftware.de'; textInput.value = 'https://medisoftware.de';
textInput.select(); textInput.select();
// Show/hide clear button based on text input content // Show/hide clear button based on input content
function toggleClearButton() { function toggleClearButton() {
if (textInput.value.length > 0) { const hasText = textInput.value.length > 0;
const hasWifi = wifiSsidInput.value.trim().length > 0 || wifiPasswordInput.value.trim().length > 0;
const hasEvent = eventTitleInput.value.trim().length > 0 ||
eventStartDateInput.value.length > 0 ||
eventStartHourInput.value !== '' ||
eventStartMinuteInput.value !== '' ||
eventEndDateInput.value.length > 0 ||
eventEndHourInput.value !== '' ||
eventEndMinuteInput.value !== '' ||
eventLocationInput.value.trim().length > 0 ||
eventDescriptionInput.value.trim().length > 0;
if (hasText || hasWifi || hasEvent) {
clearTextBtn.style.display = 'block'; clearTextBtn.style.display = 'block';
} else { } else {
clearTextBtn.style.display = 'none'; clearTextBtn.style.display = 'none';
@@ -83,6 +132,18 @@ document.addEventListener('DOMContentLoaded', function() {
textInput.value = ''; textInput.value = '';
wifiSsidInput.value = ''; wifiSsidInput.value = '';
wifiPasswordInput.value = ''; wifiPasswordInput.value = '';
eventTitleInput.value = '';
eventStartDateInput.value = '';
eventStartHourInput.value = '';
eventStartMinuteInput.value = '';
eventEndDateInput.value = '';
eventEndHourInput.value = '';
eventEndMinuteInput.value = '';
eventLocationInput.value = '';
eventDescriptionInput.value = '';
contentModeSelect.value = 'text';
toggleContentSections();
updateBerlinPreview();
toggleClearButton(); toggleClearButton();
textInput.focus(); textInput.focus();
generateQRCode(); generateQRCode();
@@ -91,10 +152,7 @@ document.addEventListener('DOMContentLoaded', function() {
// Update clear button visibility when text changes // Update clear button visibility when text changes
textInput.addEventListener('input', function() { textInput.addEventListener('input', function() {
toggleClearButton(); toggleClearButton();
// Only generate QR code if no wifi data is being entered if (contentModeSelect.value === 'text') {
const wifiSsid = wifiSsidInput.value.trim();
const wifiPassword = wifiPasswordInput.value.trim();
if (!wifiSsid && !wifiPassword) {
generateQRCode(); generateQRCode();
} }
}); });
@@ -109,22 +167,133 @@ document.addEventListener('DOMContentLoaded', function() {
// Generate QR code when wifi settings change // Generate QR code when wifi settings change
wifiSsidInput.addEventListener('input', function() { wifiSsidInput.addEventListener('input', function() {
if (contentModeSelect.value === 'wifi') {
updateTextDisplay(); updateTextDisplay();
}
generateQRCode(); generateQRCode();
toggleClearButton();
}); });
wifiPasswordInput.addEventListener('input', function() { wifiPasswordInput.addEventListener('input', function() {
if (contentModeSelect.value === 'wifi') {
updateTextDisplay(); updateTextDisplay();
}
generateQRCode(); generateQRCode();
toggleClearButton();
});
eventTitleInput.addEventListener('input', function() {
updateBerlinPreview();
generateQRCode();
toggleClearButton();
});
function onEventDateTimeInput() {
updateBerlinPreview();
generateQRCode();
toggleClearButton();
}
eventStartDateInput.addEventListener('input', onEventDateTimeInput);
eventStartDateInput.addEventListener('change', onEventDateTimeInput);
eventStartHourInput.addEventListener('input', onEventDateTimeInput);
eventStartHourInput.addEventListener('change', onEventDateTimeInput);
eventStartMinuteInput.addEventListener('input', onEventDateTimeInput);
eventStartMinuteInput.addEventListener('change', onEventDateTimeInput);
eventEndDateInput.addEventListener('input', onEventDateTimeInput);
eventEndDateInput.addEventListener('change', onEventDateTimeInput);
eventEndHourInput.addEventListener('input', onEventDateTimeInput);
eventEndHourInput.addEventListener('change', onEventDateTimeInput);
eventEndMinuteInput.addEventListener('input', onEventDateTimeInput);
eventEndMinuteInput.addEventListener('change', onEventDateTimeInput);
function clampTimeNumberInput(el, max) {
if (el.value === '') {
return;
}
var v = parseInt(el.value, 10);
if (isNaN(v)) {
return;
}
if (v < 0) {
el.value = '0';
} else if (v > max) {
el.value = String(max);
}
}
eventStartHourInput.addEventListener('blur', function() { clampTimeNumberInput(eventStartHourInput, 23); });
eventStartMinuteInput.addEventListener('blur', function() { clampTimeNumberInput(eventStartMinuteInput, 59); });
eventEndHourInput.addEventListener('blur', function() { clampTimeNumberInput(eventEndHourInput, 23); });
eventEndMinuteInput.addEventListener('blur', function() { clampTimeNumberInput(eventEndMinuteInput, 59); });
eventLocationInput.addEventListener('input', function() {
generateQRCode();
toggleClearButton();
});
eventDescriptionInput.addEventListener('input', function() {
generateQRCode();
toggleClearButton();
}); });
// Download QR code as image // Download QR code as image
downloadBtn.addEventListener('click', downloadQRCode); downloadBtn.addEventListener('click', downloadQRCode);
function sanitizeUrlString(s, maxLen) {
if (typeof s !== 'string') {
return '';
}
return s.replace(/\0/g, '').slice(0, maxLen);
}
function isValidDatetimeLocal(s) {
return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/.test(s);
}
function combineDateTimeFromParts(dateStr, hourStr, minuteStr) {
if (!dateStr || hourStr === '' || minuteStr === '') {
return '';
}
if (!/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) {
return '';
}
var h = parseInt(String(hourStr).trim(), 10);
var mi = parseInt(String(minuteStr).trim(), 10);
if (isNaN(h) || isNaN(mi)) {
return '';
}
if (h < 0 || h > 23 || mi < 0 || mi > 59) {
return '';
}
var pad = function(n) { return String(n).padStart(2, '0'); };
return dateStr + 'T' + pad(h) + ':' + pad(mi);
}
function applyCombinedToDateTimeInputs(combined, dateInput, hourInput, minuteInput) {
if (!isValidDatetimeLocal(combined)) {
return;
}
var splitT = combined.split('T');
var d = splitT[0];
var t = splitT[1];
var tm = /^(\d{2}):(\d{2})$/.exec(t);
if (!tm) {
return;
}
dateInput.value = d;
hourInput.value = String(parseInt(tm[1], 10));
minuteInput.value = String(parseInt(tm[2], 10));
}
// Or check for URL parameters if present // Or check for URL parameters if present
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
let explicitModeFromUrl = false;
if (urlParams.has('mode')) {
const m = urlParams.get('mode');
if (m === 'text' || m === 'wifi' || m === 'event') {
contentModeSelect.value = m;
explicitModeFromUrl = true;
}
}
if (urlParams.has('text')) { if (urlParams.has('text')) {
const safeText = urlParams.get('text'); const safeText = urlParams.get('text');
// Nur erlaubte Zeichen (Buchstaben, Zahlen, gängige URL-Zeichen)
if (/^[\w\-.:/?&=#%+@!;,]*$/i.test(safeText)) { if (/^[\w\-.:/?&=#%+@!;,]*$/i.test(safeText)) {
textInput.value = safeText; textInput.value = safeText;
} }
@@ -141,6 +310,33 @@ document.addEventListener('DOMContentLoaded', function() {
wifiPasswordInput.value = safePassword; wifiPasswordInput.value = safePassword;
} }
} }
const hasEventParams = urlParams.has('eventTitle') || urlParams.has('eventStart');
if (!explicitModeFromUrl && hasEventParams) {
contentModeSelect.value = 'event';
}
if (urlParams.has('eventTitle')) {
eventTitleInput.value = sanitizeUrlString(urlParams.get('eventTitle'), MAX_EVENT_TITLE);
}
if (urlParams.has('eventStart')) {
const es = urlParams.get('eventStart');
if (isValidDatetimeLocal(es)) {
applyCombinedToDateTimeInputs(es, eventStartDateInput, eventStartHourInput, eventStartMinuteInput);
}
}
if (urlParams.has('eventEnd')) {
const ee = urlParams.get('eventEnd');
if (isValidDatetimeLocal(ee)) {
applyCombinedToDateTimeInputs(ee, eventEndDateInput, eventEndHourInput, eventEndMinuteInput);
}
}
if (urlParams.has('eventLocation')) {
eventLocationInput.value = sanitizeUrlString(urlParams.get('eventLocation'), MAX_EVENT_LOCATION);
}
if (urlParams.has('eventDescription')) {
eventDescriptionInput.value = sanitizeUrlString(urlParams.get('eventDescription'), MAX_EVENT_DESCRIPTION);
}
if (urlParams.has('size')) { if (urlParams.has('size')) {
const sizeValue = urlParams.get('size'); const sizeValue = urlParams.get('size');
if (["128", "256", "512", "1024"].includes(sizeValue)) { if (["128", "256", "512", "1024"].includes(sizeValue)) {
@@ -165,13 +361,206 @@ document.addEventListener('DOMContentLoaded', function() {
backgroundColor.value = bgValue; backgroundColor.value = bgValue;
} }
} }
toggleContentSections();
updateBerlinPreview();
generateQRCode(); generateQRCode();
function daysInMonth(y, mo) {
return new Date(y, mo, 0).getDate();
}
function parseDatetimeLocalValue(s) {
const m = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})$/.exec(s);
if (!m) {
return null;
}
return {
y: parseInt(m[1], 10),
mo: parseInt(m[2], 10),
d: parseInt(m[3], 10),
h: parseInt(m[4], 10),
mi: parseInt(m[5], 10)
};
}
function addHoursWall(parts, deltaH) {
let y = parts.y;
let mo = parts.mo;
let d = parts.d;
let h = parts.h + deltaH;
let mi = parts.mi;
while (h >= 24) {
h -= 24;
d += 1;
const dim = daysInMonth(y, mo);
if (d > dim) {
d = 1;
mo += 1;
if (mo > 12) {
mo = 1;
y += 1;
}
}
}
return { y: y, mo: mo, d: d, h: h, mi: mi };
}
function wallTimeToICal(p) {
const pad = function(n) { return String(n).padStart(2, '0'); };
return String(p.y) + pad(p.mo) + pad(p.d) + 'T' + pad(p.h) + pad(p.mi) + '00';
}
function compareWallTime(a, b) {
return (a.y * 1e8 + a.mo * 1e6 + a.d * 1e4 + a.h * 100 + a.mi) -
(b.y * 1e8 + b.mo * 1e6 + b.d * 1e4 + b.h * 100 + b.mi);
}
function formatSlashDateTime24hBerlin(p) {
const pad = function(n) { return String(n).padStart(2, '0'); };
return pad(p.d) + '/' + pad(p.mo) + '/' + p.y + ', ' + pad(p.h) + ':' + pad(p.mi);
}
function updateBerlinPreview() {
if (!eventBerlinPreview) {
return;
}
if (contentModeSelect.value !== 'event') {
eventBerlinPreview.textContent = '';
return;
}
const startRaw = combineDateTimeFromParts(
eventStartDateInput.value,
eventStartHourInput.value,
eventStartMinuteInput.value
);
const endD = eventEndDateInput.value;
const endH = eventEndHourInput.value;
const endM = eventEndMinuteInput.value;
const endTimeComplete = endH !== '' && endM !== '';
const endTimeAny = endH !== '' || endM !== '';
let endRaw = '';
if (endD && endTimeComplete) {
endRaw = combineDateTimeFromParts(endD, endH, endM);
} else if (endD && !endTimeComplete) {
eventBerlinPreview.textContent =
'Bitte geben Sie für das Ende Datum sowie Stunde (023) und Minute (059) an, oder lassen Sie das Ende vollständig leer.';
return;
} else if (!endD && endTimeAny) {
eventBerlinPreview.textContent =
'Bitte geben Sie für das Ende ein Datum ein, oder entfernen Sie Stunde/Minute.';
return;
}
const sp = parseDatetimeLocalValue(startRaw);
if (!sp) {
eventBerlinPreview.textContent = '';
return;
}
let endDisplay;
if (endRaw) {
const ep = parseDatetimeLocalValue(endRaw);
if (!ep) {
eventBerlinPreview.textContent = 'Beginn: ' + formatSlashDateTime24hBerlin(sp) + ' (Europe/Berlin)';
return;
}
endDisplay = ep;
} else {
endDisplay = addHoursWall(sp, 1);
}
eventBerlinPreview.textContent =
'Beginn: ' + formatSlashDateTime24hBerlin(sp) +
' · Ende: ' + formatSlashDateTime24hBerlin(endDisplay) +
' (Europe/Berlin)';
}
function escapeICalText(str) {
return String(str)
.replace(/\r\n/g, '\n')
.replace(/\r/g, '')
.replace(/\\/g, '\\\\')
.replace(/;/g, '\\;')
.replace(/,/g, '\\,')
.replace(/\n/g, '\\n');
}
function formatICalUTCStamp() {
const d = new Date();
const pad = function(n) { return String(n).padStart(2, '0'); };
return (
d.getUTCFullYear() +
pad(d.getUTCMonth() + 1) +
pad(d.getUTCDate()) +
'T' +
pad(d.getUTCHours()) +
pad(d.getUTCMinutes()) +
pad(d.getUTCSeconds()) +
'Z'
);
}
function newEventUid() {
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
return crypto.randomUUID() + '@qr-generator.local';
}
return 'evt-' + String(Date.now()) + '-' + String(Math.random()).slice(2) + '@qr-generator.local';
}
function buildICalEvent(opts) {
const summary = opts.summary;
const dtStart = opts.dtStart;
const dtEnd = opts.dtEnd;
const location = opts.location;
const description = opts.description;
const vtimezoneBerlin = [
'BEGIN:VTIMEZONE',
'TZID:Europe/Berlin',
'BEGIN:DAYLIGHT',
'DTSTART:19810329T020000',
'RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU',
'TZNAME:CEST',
'TZOFFSETFROM:+0100',
'TZOFFSETTO:+0200',
'END:DAYLIGHT',
'BEGIN:STANDARD',
'DTSTART:19961027T030000',
'RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU',
'TZNAME:CET',
'TZOFFSETFROM:+0200',
'TZOFFSETTO:+0100',
'END:STANDARD',
'END:VTIMEZONE'
].join('\r\n');
const lines = [
'BEGIN:VCALENDAR',
'VERSION:2.0',
'PRODID:-//qr-generator//DE',
'CALSCALE:GREGORIAN',
vtimezoneBerlin,
'BEGIN:VEVENT',
'UID:' + opts.uid,
'DTSTAMP:' + opts.dtStamp,
'DTSTART;TZID=Europe/Berlin:' + dtStart,
'DTEND;TZID=Europe/Berlin:' + dtEnd,
'SUMMARY:' + escapeICalText(summary)
];
if (location && location.trim()) {
lines.push('LOCATION:' + escapeICalText(location.trim()));
}
if (description && description.trim()) {
lines.push('DESCRIPTION:' + escapeICalText(description.trim()));
}
lines.push('END:VEVENT', 'END:VCALENDAR');
return lines.join('\r\n');
}
function generateQRCode() { function generateQRCode() {
const mode = contentModeSelect.value;
const text = textInput.value.trim(); const text = textInput.value.trim();
const wifiSsid = wifiSsidInput.value.trim(); const wifiSsid = wifiSsidInput.value.trim();
const wifiPassword = wifiPasswordInput.value.trim(); const wifiPassword = wifiPasswordInput.value.trim();
const size = parseInt(sizeSelect.value); const size = parseInt(sizeSelect.value, 10);
const errorCorrection = errorCorrectionSelect.value.toLowerCase(); const errorCorrection = errorCorrectionSelect.value.toLowerCase();
const fgColor = foregroundColor.value; const fgColor = foregroundColor.value;
const bgColor = backgroundColor.value; const bgColor = backgroundColor.value;
@@ -186,28 +575,122 @@ document.addEventListener('DOMContentLoaded', function() {
errorMessage.textContent = ''; errorMessage.textContent = '';
downloadBtn.style.display = 'none'; downloadBtn.style.display = 'none';
// Determine what to encode let qrText = '';
let qrText = text;
// If wifi credentials are provided, generate wifi QR code if (mode === 'event') {
if (wifiSsid && wifiPassword) { const title = eventTitleInput.value.trim();
// Wifi QR code format: WIFI:S:<SSID>;T:WPA;P:<password>;; const startRaw = combineDateTimeFromParts(
qrText = `WIFI:S:${escapeSpecialChars(wifiSsid)};T:WPA;P:${escapeSpecialChars(wifiPassword)};;`; eventStartDateInput.value,
} else if (wifiSsid && !wifiPassword) { eventStartHourInput.value,
// SSID only (open network) eventStartMinuteInput.value
qrText = `WIFI:S:${escapeSpecialChars(wifiSsid)};T:nopass;;`; );
} else if (!wifiSsid && wifiPassword) { const endD = eventEndDateInput.value;
// Password without SSID is invalid const endH = eventEndHourInput.value;
errorMessage.textContent = 'Bitte geben Sie eine SSID ein, wenn Sie ein Passwort angeben'; const endM = eventEndMinuteInput.value;
const endTimeComplete = endH !== '' && endM !== '';
const endTimeAny = endH !== '' || endM !== '';
const loc = eventLocationInput.value;
const desc = eventDescriptionInput.value;
if (!title) {
errorMessage.textContent = 'Bitte geben Sie einen Titel für den Termin ein';
qrcodeCanvas.style.display = 'none';
qrcodeImg.style.display = 'none';
return;
}
if (!startRaw) {
errorMessage.textContent =
'Bitte geben Sie Datum sowie Stunde (023) und Minute (059) für den Beginn ein';
qrcodeCanvas.style.display = 'none'; qrcodeCanvas.style.display = 'none';
qrcodeImg.style.display = 'none'; qrcodeImg.style.display = 'none';
return; return;
} }
if (endD && !endTimeComplete) {
errorMessage.textContent =
'Für das Ende: bitte Stunde und Minute ergänzen (023 / 059) oder Ende leer lassen';
qrcodeCanvas.style.display = 'none';
qrcodeImg.style.display = 'none';
return;
}
if (!endD && endTimeAny) {
errorMessage.textContent = 'Für das Ende bitte zuerst ein Datum wählen oder Zeitfelder leeren';
qrcodeCanvas.style.display = 'none';
qrcodeImg.style.display = 'none';
return;
}
const startParts = parseDatetimeLocalValue(startRaw);
if (!startParts) {
errorMessage.textContent = 'Ungültige Startzeit (Datum und 24h-Uhrzeit prüfen)';
qrcodeCanvas.style.display = 'none';
qrcodeImg.style.display = 'none';
return;
}
const dtStart = wallTimeToICal(startParts);
let endParts;
const endRaw = endD && endTimeComplete ? combineDateTimeFromParts(endD, endH, endM) : '';
if (endRaw) {
endParts = parseDatetimeLocalValue(endRaw);
if (!endParts) {
errorMessage.textContent = 'Ungültige Endzeit';
qrcodeCanvas.style.display = 'none';
qrcodeImg.style.display = 'none';
return;
}
} else {
endParts = addHoursWall(startParts, 1);
}
if (compareWallTime(endParts, startParts) <= 0) {
errorMessage.textContent = 'Die Endzeit muss nach dem Beginn liegen (Ortszeit Europe/Berlin)';
qrcodeCanvas.style.display = 'none';
qrcodeImg.style.display = 'none';
return;
}
const dtEndStr = wallTimeToICal(endParts);
qrText = buildICalEvent({
uid: newEventUid(),
dtStamp: formatICalUTCStamp(),
summary: title.slice(0, MAX_EVENT_TITLE),
dtStart: dtStart,
dtEnd: dtEndStr,
location: loc.slice(0, MAX_EVENT_LOCATION),
description: desc.slice(0, MAX_EVENT_DESCRIPTION)
});
textInput.value = qrText;
toggleClearButton();
} else if (mode === 'wifi') {
if (wifiSsid && wifiPassword) {
qrText = 'WIFI:S:' + escapeSpecialChars(wifiSsid) + ';T:WPA;P:' + escapeSpecialChars(wifiPassword) + ';;';
} else if (wifiSsid && !wifiPassword) {
qrText = 'WIFI:S:' + escapeSpecialChars(wifiSsid) + ';T:nopass;;';
} else if (!wifiSsid && wifiPassword) {
errorMessage.textContent = 'Bitte geben Sie eine SSID ein, wenn Sie ein Passwort angeben';
qrcodeCanvas.style.display = 'none';
qrcodeImg.style.display = 'none';
return;
} else {
qrText = text;
}
} else {
qrText = text;
}
// Validate input // Validate input
if (qrText === '') { if (qrText === '') {
errorMessage.textContent = 'Bitte geben Sie Text/URL oder Wifi-Daten ein'; if (mode === 'wifi') {
// Hide both canvas and image errorMessage.textContent = 'Bitte geben Sie Text/URL oder WLAN-Daten ein';
} else if (mode === 'event') {
errorMessage.textContent = 'Termin konnte nicht erzeugt werden';
} else {
errorMessage.textContent = 'Bitte geben Sie Text oder eine URL ein';
}
qrcodeCanvas.style.display = 'none'; qrcodeCanvas.style.display = 'none';
qrcodeImg.style.display = 'none'; qrcodeImg.style.display = 'none';
return; return;
@@ -270,34 +753,29 @@ document.addEventListener('DOMContentLoaded', function() {
downloadBtn.textContent = 'Downloaded!'; downloadBtn.textContent = 'Downloaded!';
// Reset button after a short delay // Reset button after a short delay
setTimeout(() => { setTimeout(function() {
downloadBtn.textContent = originalText; downloadBtn.textContent = originalText;
}, 2000); }, 2000);
} }
// Function to update the text display with current QR code content // Function to update the text display with current QR code content (WLAN)
function updateTextDisplay() { function updateTextDisplay() {
const wifiSsid = wifiSsidInput.value.trim(); const wifiSsid = wifiSsidInput.value.trim();
const wifiPassword = wifiPasswordInput.value.trim(); const wifiPassword = wifiPasswordInput.value.trim();
if (wifiSsid && wifiPassword) { if (wifiSsid && wifiPassword) {
// Show wifi QR code format textInput.value = 'WIFI:S:' + escapeSpecialChars(wifiSsid) + ';T:WPA;P:' + escapeSpecialChars(wifiPassword) + ';;';
textInput.value = `WIFI:S:${escapeSpecialChars(wifiSsid)};T:WPA;P:${escapeSpecialChars(wifiPassword)};;`;
} else if (wifiSsid && !wifiPassword) { } else if (wifiSsid && !wifiPassword) {
// Show open network format textInput.value = 'WIFI:S:' + escapeSpecialChars(wifiSsid) + ';T:nopass;;';
textInput.value = `WIFI:S:${escapeSpecialChars(wifiSsid)};T:nopass;;`;
} else {
// If no wifi data, keep original text input value
// Don't change it here to avoid overwriting user input
} }
// Update clear button visibility
toggleClearButton(); toggleClearButton();
} }
function updateShareHint() { function updateShareHint() {
const mode = contentModeSelect.value;
const wifiPassword = wifiPasswordInput.value.trim(); const wifiPassword = wifiPasswordInput.value.trim();
if (wifiPassword) { if (mode === 'wifi' && wifiPassword) {
shareHint.style.display = 'block'; shareHint.style.display = 'block';
} else { } else {
shareHint.style.display = 'none'; shareHint.style.display = 'none';
@@ -307,11 +785,12 @@ document.addEventListener('DOMContentLoaded', function() {
wifiPasswordInput.addEventListener('input', updateShareHint); wifiPasswordInput.addEventListener('input', updateShareHint);
wifiSsidInput.addEventListener('input', updateShareHint); wifiSsidInput.addEventListener('input', updateShareHint);
textInput.addEventListener('input', updateShareHint); textInput.addEventListener('input', updateShareHint);
// Initial anzeigen, falls Passwort schon gesetzt contentModeSelect.addEventListener('change', updateShareHint);
updateShareHint(); updateShareHint();
shareBtn.addEventListener('click', function() { shareBtn.addEventListener('click', function() {
const params = new URLSearchParams(); const params = new URLSearchParams();
const mode = contentModeSelect.value;
const text = textInput.value.trim(); const text = textInput.value.trim();
const wifiSsid = wifiSsidInput.value.trim(); const wifiSsid = wifiSsidInput.value.trim();
const wifiPassword = wifiPasswordInput.value.trim(); const wifiPassword = wifiPasswordInput.value.trim();
@@ -320,22 +799,66 @@ document.addEventListener('DOMContentLoaded', function() {
const fgColor = foregroundColor.value; const fgColor = foregroundColor.value;
const bgColor = backgroundColor.value; const bgColor = backgroundColor.value;
// Entscheide, was kodiert werden soll params.set('mode', mode);
if (wifiSsid) params.set('ssid', wifiSsid);
if (wifiPassword) params.set('password', wifiPassword); if (mode === 'event') {
if (!wifiSsid && !wifiPassword && text) params.set('text', text); if (eventTitleInput.value.trim()) {
if (size !== '256') params.set('size', size); params.set('eventTitle', eventTitleInput.value.trim());
if (errorCorrection !== 'M') params.set('errorCorrection', errorCorrection); }
if (fgColor !== '#000000') params.set('foreground', fgColor); const shareStart = combineDateTimeFromParts(
if (bgColor !== '#ffffff') params.set('background', bgColor); eventStartDateInput.value,
eventStartHourInput.value,
eventStartMinuteInput.value
);
const shareEnd = combineDateTimeFromParts(
eventEndDateInput.value,
eventEndHourInput.value,
eventEndMinuteInput.value
);
if (shareStart) {
params.set('eventStart', shareStart);
}
if (shareEnd) {
params.set('eventEnd', shareEnd);
}
if (eventLocationInput.value.trim()) {
params.set('eventLocation', eventLocationInput.value.trim());
}
if (eventDescriptionInput.value.trim()) {
params.set('eventDescription', eventDescriptionInput.value.trim());
}
} else if (mode === 'wifi') {
if (wifiSsid) {
params.set('ssid', wifiSsid);
}
if (wifiPassword) {
params.set('password', wifiPassword);
}
} else {
if (text) {
params.set('text', text);
}
}
if (size !== '256') {
params.set('size', size);
}
if (errorCorrection !== 'M') {
params.set('errorCorrection', errorCorrection);
}
if (fgColor !== '#000000') {
params.set('foreground', fgColor);
}
if (bgColor !== '#ffffff') {
params.set('background', bgColor);
}
const url = window.location.origin + window.location.pathname + '?' + params.toString(); const url = window.location.origin + window.location.pathname + '?' + params.toString();
// In Zwischenablage kopieren navigator.clipboard.writeText(url).then(function() {
navigator.clipboard.writeText(url).then(() => {
const original = shareBtn.textContent; const original = shareBtn.textContent;
shareBtn.textContent = 'Link kopiert!'; shareBtn.textContent = 'Link kopiert!';
setTimeout(() => { shareBtn.textContent = original; }, 2000); setTimeout(function() { shareBtn.textContent = original; }, 2000);
}, () => { }, function() {
alert('Konnte Link nicht kopieren.'); alert('Konnte Link nicht kopieren.');
}); });
}); });