Compare commits

...

10 Commits

Author SHA1 Message Date
elpatron 4e257a09f0 update: Footer-Kontakt auf Mailto-Link umstellen
Verlinkt den Namen in der Credit-Zeile direkt mit der Kontaktadresse per mailto, damit der Kontakt aus dem Footer unmittelbar erreichbar ist.

Made-with: Cursor
2026-04-25 14:18:03 +02:00
elpatron b0eaaf7694 chore: ZIP-Artefakt entfernen und Ignore-Regel ergänzen
Entfernt die versehentlich versionierte ZIP-Datei aus dem Repository und ergänzt eine generelle Ignore-Regel für ZIP-Dateien. Aktualisiert zusätzlich die Footer-Referenz auf Proxmox.

Made-with: Cursor
2026-04-25 14:16:44 +02:00
elpatron 30872ad780 update: vCard placeholders updated for organization and position fields
Changed the placeholder text in the vCard input fields to reflect new example values, enhancing user experience and clarity in the form.
2026-04-25 14:12:32 +02:00
elpatron 11d4586ea7 fix: vCard-Mode bei allen vCard-URL-Parametern erkennen
Erweitert die Auto-Erkennung für vCard-Links, damit auch Links mit nur Titel, Website, Adresse oder Notiz automatisch in den vCard-Modus wechseln. So funktionieren geteilte vCard-URLs konsistent ohne manuelle Moduswahl.

Made-with: Cursor
2026-04-25 14:09:36 +02:00
elpatron 319ee44aae ux: vCard-Eingabe mit Platzhaltern und Live-Vorschau verbessern
Ergänzt aussagekräftige Beispiel-Platzhalter im vCard-Formular und zeigt den generierten vCard-Text als Live-Vorschau im vCard-Modus an. Das erleichtert die Eingabe und macht das spätere QR-Ergebnis sofort nachvollziehbar.

Made-with: Cursor
2026-04-25 14:08:49 +02:00
elpatron 0ca6e891cc feat: vCard-QR-Modus mit Kontaktformular ergänzen
Erweitert den Generator um einen vCard-Modus inklusive eigener Eingabemaske, Validierung und vCard-3.0-Erzeugung, damit Kontakte direkt per QR geteilt werden können. Ergänzt außerdem Share-URL-Parameter und den Info-Text für den neuen Modus.

Made-with: Cursor
2026-04-25 14:07:43 +02:00
elpatron 7790fe6dda Plausible Analytics einbinden und CSP/Texte anpassen
Made-with: Cursor
2026-04-16 12:52:11 +02:00
elpatron 8dac716c28 docs: README um Termin-URL-Parameter und Beispiel ergänzen
- mode und event* Parameter dokumentiert
- Beispiel-Link für Kalender-QR
- Teilen-Hinweis um Termin-Daten erweitert

Made-with: Cursor
2026-04-16 09:00:57 +02:00
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
5 changed files with 1024 additions and 81 deletions
+1
View File
@@ -0,0 +1 @@
*.zip
+16 -5
View File
@@ -7,7 +7,7 @@ Dieses Projekt basiert auf dem Code von https://qr.alster.space/. Er wurde um ve
## Features ## Features
- Vollständig clientseitig (keine Server-Kommunikation) - Vollständig clientseitig (keine Server-Kommunikation)
- URL-Parameter für alle Einstellungen - URL-Parameter für alle Einstellungen (inkl. Modus Text/WLAN/Termin)
- Anpassbare Größen und Farben - Anpassbare Größen und Farben
- Verschiedene Fehlerkorrektur-Level - Verschiedene Fehlerkorrektur-Level
- Download-Funktion - Download-Funktion
@@ -69,9 +69,15 @@ Die App bietet spezielle Eingabefelder für WiFi-Daten:
Die Anwendung unterstützt folgende URL-Parameter: Die Anwendung unterstützt folgende URL-Parameter:
- `text` - Text oder URL für den QR-Code - `mode` - Inhaltstyp: `text`, `wifi` oder `event` (Termin/Kalender). Ohne `mode` wird bei vorhandenen Termin-Feldern automatisch der Termin-Modus gewählt.
- `ssid` - WiFi SSID - `text` - Text oder URL für den QR-Code (Modus Text)
- `password` - WiFi Passwort - `ssid` - WiFi SSID (Modus WLAN)
- `password` - WiFi Passwort (Modus WLAN)
- `eventTitle` - Titel des Termins (Modus `event`)
- `eventStart` - Beginn im Format `YYYY-MM-DDTHH:mm` (lokale Ortszeit Europe/Berlin, z.B. `2026-04-16T14:30`)
- `eventEnd` - Ende (optional), gleiches Format wie `eventStart`
- `eventLocation` - Ort (optional)
- `eventDescription` - Beschreibung (optional)
- `size` - Größe (128, 256, 512, 1024) - `size` - Größe (128, 256, 512, 1024)
- `errorCorrection` - Fehlerkorrektur (L, M, Q, H) - `errorCorrection` - Fehlerkorrektur (L, M, Q, H)
- `foreground` - Vordergrundfarbe (Hex-Code) - `foreground` - Vordergrundfarbe (Hex-Code)
@@ -92,6 +98,11 @@ http://localhost:8080/?ssid=MeinWLAN&password=MeinPasswort123
http://localhost:8080/?ssid=OffenesWLAN http://localhost:8080/?ssid=OffenesWLAN
``` ```
**Termin (Kalender, iCal im QR):**
```
http://localhost:8080/?mode=event&eventTitle=Team-Meeting&eventStart=2026-04-16T14:30&eventEnd=2026-04-16T15:30
```
**Achtung:** Das WLAN-Passwort ist im Link im Klartext sichtbar! **Achtung:** Das WLAN-Passwort ist im Link im Klartext sichtbar!
## WiFi QR-Code Format ## WiFi QR-Code Format
@@ -133,7 +144,7 @@ Das Smartphone erkennt automatisch, dass es sich um WiFi-Daten handelt und biete
### Teilen-Funktion ### Teilen-Funktion
Mit dem Button "Teilen" kann ein Link mit allen aktuellen Einstellungen (inkl. WiFi-Daten) in die Zwischenablage kopiert werden. Dieser Link kann weitergegeben werden und öffnet die App direkt mit den gewählten Einstellungen. Mit dem Button "Teilen" kann ein Link mit allen aktuellen Einstellungen (inkl. WiFi- oder Termin-Daten) in die Zwischenablage kopiert werden. Dieser Link kann weitergegeben werden und öffnet die App direkt mit den gewählten Einstellungen.
**Achtung:** Wenn ein WLAN-Passwort eingegeben ist, wird dieses im Link im Klartext übertragen! **Achtung:** Wenn ein WLAN-Passwort eingegeben ist, wird dieses im Link im Klartext übertragen!
+7 -3
View File
@@ -1,9 +1,13 @@
version: '3.8'
services: services:
qr-generator: qr-generator:
build: . build: .
ports: ports:
- "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
+202 -18
View File
@@ -1,11 +1,12 @@
<!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' https://plausible.elpatron.me; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' https://plausible.elpatron.me; object-src 'none';">
<title>QR ohne Schnickschnack</title> <title>QR ohne Schnickschnack</title>
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' fill='white' rx='6' ry='6'/><rect x='4' y='4' width='8' height='8' fill='black'/><rect x='20' y='4' width='8' height='8' fill='black'/><rect x='4' y='20' width='8' height='8' fill='black'/><rect x='14' y='14' width='4' height='4' fill='black'/></svg>"> <link rel="icon" type="image/svg+xml" href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' fill='white' rx='6' ry='6'/><rect x='4' y='4' width='8' height='8' fill='black'/><rect x='20' y='4' width='8' height='8' fill='black'/><rect x='4' y='20' width='8' height='8' fill='black'/><rect x='14' y='14' width='4' height='4' fill='black'/></svg>">
<script defer data-domain="qr.elpatron.me" src="https://plausible.elpatron.me/js/script.js"></script>
<script src="./assets/qrious.min.js"></script> <script src="./assets/qrious.min.js"></script>
<script src="./main.js"></script> <script src="./main.js"></script>
<!-- <!--
@@ -53,7 +54,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 +65,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 +301,73 @@
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;
}
.vcard-preview-label {
margin: 4px 0 6px 0;
color: #374151;
font-weight: 500;
}
.vcard-preview {
margin: 0 0 15px 0;
padding: 10px;
border: 1px solid #e5e7eb;
border-radius: 6px;
background: #f9fafb;
color: #374151;
font-size: 12px;
line-height: 1.45;
white-space: pre-wrap;
word-break: break-word;
min-height: 80px;
}
</style> </style>
</head> </head>
<body> <body>
@@ -303,22 +376,130 @@
<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="text">Text oder URL:</label> <label for="content-mode">QR-Inhalt:</label>
<div class="text-input-wrapper"> <select id="content-mode">
<input type="text" id="text" placeholder="https://qr.medisoftware.org" value="" autofocus=""> <option value="text">Text oder URL</option>
<button type="button" id="clear-text" class="clear-button" aria-label="Clear text" style="display: none;"></button> <option value="wifi">WLAN</option>
<option value="event">Termin (Kalender)</option>
<option value="vcard">vCard (Kontakt)</option>
</select>
<div id="section-text" class="content-section">
<label for="text">Text oder URL:</label>
<div class="text-input-wrapper">
<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>
</div>
</div> </div>
<div class="options-row"> <div id="section-wifi" class="content-section" style="display: none;">
<div class="options-col"> <div class="options-row">
<label for="wifi-ssid">WLAN SSID:</label> <div class="options-col">
<input type="text" id="wifi-ssid" placeholder=""> <label for="wifi-ssid">WLAN SSID:</label>
<input type="text" id="wifi-ssid" placeholder="">
</div>
<div class="options-col">
<label for="wifi-password">WLAN Passwort:</label>
<input type="password" id="wifi-password" placeholder="">
</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 id="section-vcard" class="content-section" style="display: none;">
<div class="options-row">
<div class="options-col">
<label for="vcard-first-name">Vorname:</label>
<input type="text" id="vcard-first-name" placeholder="Max" autocomplete="off">
</div>
<div class="options-col">
<label for="vcard-last-name">Nachname:</label>
<input type="text" id="vcard-last-name" placeholder="Mustermann" autocomplete="off">
</div>
</div> </div>
<div class="options-col"> <div class="options-row">
<label for="wifi-password">WLAN Passwort:</label> <div class="options-col">
<input type="password" id="wifi-password" placeholder=""> <label for="vcard-org">Organisation:</label>
<input type="text" id="vcard-org" placeholder="Knorrlabs Inc." autocomplete="off">
</div>
<div class="options-col">
<label for="vcard-title">Position (optional):</label>
<input type="text" id="vcard-title" placeholder="Master Of Desaster" autocomplete="off">
</div>
</div> </div>
<div class="options-row">
<div class="options-col">
<label for="vcard-phone">Telefon (optional):</label>
<input type="tel" id="vcard-phone" placeholder="+49 30 1234567" autocomplete="off">
</div>
<div class="options-col">
<label for="vcard-email">E-Mail (optional):</label>
<input type="email" id="vcard-email" placeholder="max@beispiel.de" autocomplete="off">
</div>
</div>
<label for="vcard-website">Webseite (optional):</label>
<input type="url" id="vcard-website" placeholder="https://beispiel.de" autocomplete="off">
<label for="vcard-address">Adresse (optional):</label>
<textarea id="vcard-address" placeholder="Musterstraße 1, 10115 Berlin" rows="2"></textarea>
<label for="vcard-note">Notiz (optional):</label>
<textarea id="vcard-note" placeholder="Erreichbar Mo-Fr 9-17 Uhr" rows="2"></textarea>
<p class="vcard-preview-label">vCard-Vorschau:</p>
<pre id="vcard-preview" class="vcard-preview" aria-live="polite">Bitte mindestens Vorname, Nachname oder Organisation eingeben.</pre>
</div> </div>
<div class="options-row"> <div class="options-row">
@@ -384,7 +565,7 @@
<p>Not today.</p> <p>Not today.</p>
<p>Rather than bemoan the fact that there's no easy way to get QR ohne Schnickschnak without the low-key parasitic monetization, I found a library (qrious) and put together just enough Javascript code to generate QR codes in the browser. No ads, no trackers, no sending your data to someone else's server for who-knows-what-reason.</p> <p>Rather than bemoan the fact that there's no easy way to get QR ohne Schnickschnak without the low-key parasitic monetization, I found a library (qrious) and put together just enough Javascript code to generate QR codes in the browser. No ads; page views are counted with self-hosted Plausible (privacy-friendly, no cookies). Your QR content is not sent anywhere.</p>
<p>Go ahead and inspect the page source. Save it to your computer, copy it, remix it, whatever you want.</p> <p>Go ahead and inspect the page source. Save it to your computer, copy it, remix it, whatever you want.</p>
@@ -393,12 +574,15 @@
<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), ein <strong>Kalendertermin</strong> oder eine <strong>vCard</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>. Im Modus „vCard“ wird ein Kontakt im vCard-Format (3.0) erzeugt, den viele Geräte direkt als Kontakt speichern. Deine Eingaben für den QR-Code werden nicht an einen Server gesendet.</p>
</div> </div>
</div> </div>
<div class="footer"> <div class="footer">
Dieser QR-Code-Generator funktioniert vollständig in deinem Browser. Es werden keine Daten an einen Server gesendet. <i>Quellcode und README bei <a href="https://gitea.elpatron.me/elpatron/QR-Code-Generator" target="_blank">Gitea</a></i>. Dieser QR-Code-Generator funktioniert vollständig in deinem Browser; QR-Inhalte werden nicht hochgeladen. Seitenaufrufe werden mit selbst gehostetem Plausible aggregiert (ohne Cookies). <i>Quellcode und README bei <a href="https://gitea.elpatron.me/elpatron/QR-Code-Generator" target="_blank">Gitea</a></i>.
<p class="credit">Made with ❤️ and 🍪 by Markus. Self hosted on <a href="https://unraid.net" target="_blank">Unraid</a> for <a href="https://medisoftware.de" target="_blank">medisoftware</a>. Credits: <a href="https://qr.alster.space/" target="_blank">alsternerd</a></p> <p class="credit">Made with ❤️ and 🍪 by <a href="mailto:elpatron+qr@mailbox.org">Markus F.J. Busche</a>. Self hosted on <a href="https://www.proxmox.com/" target="_blank">Proxmox</a>. Credits: <a href="https://qr.alster.space/" target="_blank">alsternerd</a></p>
</div> </div>
</body></html> </body></html>
+798 -55
View File
@@ -2,8 +2,31 @@ 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 sectionVcard = document.getElementById('section-vcard');
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 vcardFirstNameInput = document.getElementById('vcard-first-name');
const vcardLastNameInput = document.getElementById('vcard-last-name');
const vcardOrgInput = document.getElementById('vcard-org');
const vcardTitleInput = document.getElementById('vcard-title');
const vcardPhoneInput = document.getElementById('vcard-phone');
const vcardEmailInput = document.getElementById('vcard-email');
const vcardWebsiteInput = document.getElementById('vcard-website');
const vcardAddressInput = document.getElementById('vcard-address');
const vcardNoteInput = document.getElementById('vcard-note');
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 +41,13 @@ 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 vcardPreview = document.getElementById('vcard-preview');
const MAX_EVENT_TITLE = 2000;
const MAX_EVENT_LOCATION = 1000;
const MAX_EVENT_DESCRIPTION = 8000;
const MAX_VCARD_FIELD = 500;
// Title Easter egg // Title Easter egg
let titleClickCount = 0; let titleClickCount = 0;
@@ -62,13 +92,56 @@ 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';
sectionVcard.style.display = mode === 'vcard' ? '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();
} else if (this.value === 'vcard') {
vcardFirstNameInput.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;
const hasVCard = vcardFirstNameInput.value.trim().length > 0 ||
vcardLastNameInput.value.trim().length > 0 ||
vcardOrgInput.value.trim().length > 0 ||
vcardTitleInput.value.trim().length > 0 ||
vcardPhoneInput.value.trim().length > 0 ||
vcardEmailInput.value.trim().length > 0 ||
vcardWebsiteInput.value.trim().length > 0 ||
vcardAddressInput.value.trim().length > 0 ||
vcardNoteInput.value.trim().length > 0;
if (hasText || hasWifi || hasEvent || hasVCard) {
clearTextBtn.style.display = 'block'; clearTextBtn.style.display = 'block';
} else { } else {
clearTextBtn.style.display = 'none'; clearTextBtn.style.display = 'none';
@@ -83,6 +156,27 @@ 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 = '';
vcardFirstNameInput.value = '';
vcardLastNameInput.value = '';
vcardOrgInput.value = '';
vcardTitleInput.value = '';
vcardPhoneInput.value = '';
vcardEmailInput.value = '';
vcardWebsiteInput.value = '';
vcardAddressInput.value = '';
vcardNoteInput.value = '';
contentModeSelect.value = 'text';
toggleContentSections();
updateBerlinPreview();
toggleClearButton(); toggleClearButton();
textInput.focus(); textInput.focus();
generateQRCode(); generateQRCode();
@@ -91,10 +185,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 +200,146 @@ 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() {
updateTextDisplay(); if (contentModeSelect.value === 'wifi') {
updateTextDisplay();
}
generateQRCode(); generateQRCode();
toggleClearButton();
}); });
wifiPasswordInput.addEventListener('input', function() { wifiPasswordInput.addEventListener('input', function() {
updateTextDisplay(); if (contentModeSelect.value === 'wifi') {
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();
});
function onVcardInput() {
generateQRCode();
toggleClearButton();
}
vcardFirstNameInput.addEventListener('input', onVcardInput);
vcardLastNameInput.addEventListener('input', onVcardInput);
vcardOrgInput.addEventListener('input', onVcardInput);
vcardTitleInput.addEventListener('input', onVcardInput);
vcardPhoneInput.addEventListener('input', onVcardInput);
vcardEmailInput.addEventListener('input', onVcardInput);
vcardWebsiteInput.addEventListener('input', onVcardInput);
vcardAddressInput.addEventListener('input', onVcardInput);
vcardNoteInput.addEventListener('input', onVcardInput);
// 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' || m === 'vcard') {
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 +356,72 @@ 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);
}
const hasVcardParams = urlParams.has('vcardFirstName') ||
urlParams.has('vcardLastName') ||
urlParams.has('vcardOrg') ||
urlParams.has('vcardTitle') ||
urlParams.has('vcardPhone') ||
urlParams.has('vcardEmail') ||
urlParams.has('vcardWebsite') ||
urlParams.has('vcardAddress') ||
urlParams.has('vcardNote');
if (!explicitModeFromUrl && !hasEventParams && hasVcardParams) {
contentModeSelect.value = 'vcard';
}
if (urlParams.has('vcardFirstName')) {
vcardFirstNameInput.value = sanitizeUrlString(urlParams.get('vcardFirstName'), MAX_VCARD_FIELD);
}
if (urlParams.has('vcardLastName')) {
vcardLastNameInput.value = sanitizeUrlString(urlParams.get('vcardLastName'), MAX_VCARD_FIELD);
}
if (urlParams.has('vcardOrg')) {
vcardOrgInput.value = sanitizeUrlString(urlParams.get('vcardOrg'), MAX_VCARD_FIELD);
}
if (urlParams.has('vcardTitle')) {
vcardTitleInput.value = sanitizeUrlString(urlParams.get('vcardTitle'), MAX_VCARD_FIELD);
}
if (urlParams.has('vcardPhone')) {
vcardPhoneInput.value = sanitizeUrlString(urlParams.get('vcardPhone'), MAX_VCARD_FIELD);
}
if (urlParams.has('vcardEmail')) {
vcardEmailInput.value = sanitizeUrlString(urlParams.get('vcardEmail'), MAX_VCARD_FIELD);
}
if (urlParams.has('vcardWebsite')) {
vcardWebsiteInput.value = sanitizeUrlString(urlParams.get('vcardWebsite'), MAX_VCARD_FIELD);
}
if (urlParams.has('vcardAddress')) {
vcardAddressInput.value = sanitizeUrlString(urlParams.get('vcardAddress'), MAX_VCARD_FIELD);
}
if (urlParams.has('vcardNote')) {
vcardNoteInput.value = sanitizeUrlString(urlParams.get('vcardNote'), MAX_VCARD_FIELD);
}
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 +446,274 @@ 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 escapeVCardText(str) {
return String(str)
.replace(/\r\n/g, '\n')
.replace(/\r/g, '')
.replace(/\\/g, '\\\\')
.replace(/;/g, '\\;')
.replace(/,/g, '\\,')
.replace(/\n/g, '\\n');
}
function buildVCard(opts) {
const firstName = opts.firstName.trim();
const lastName = opts.lastName.trim();
const org = opts.org.trim();
const title = opts.title.trim();
const phone = opts.phone.trim();
const email = opts.email.trim();
const website = opts.website.trim();
const address = opts.address.trim();
const note = opts.note.trim();
const displayName = [firstName, lastName].filter(Boolean).join(' ').trim() || org;
const lines = [
'BEGIN:VCARD',
'VERSION:3.0',
'N:' + escapeVCardText(lastName) + ';' + escapeVCardText(firstName) + ';;;',
'FN:' + escapeVCardText(displayName)
];
if (org) {
lines.push('ORG:' + escapeVCardText(org));
}
if (title) {
lines.push('TITLE:' + escapeVCardText(title));
}
if (phone) {
lines.push('TEL;TYPE=CELL:' + escapeVCardText(phone));
}
if (email) {
lines.push('EMAIL;TYPE=INTERNET:' + escapeVCardText(email));
}
if (website) {
lines.push('URL:' + escapeVCardText(website));
}
if (address) {
lines.push('ADR;TYPE=WORK:;;' + escapeVCardText(address) + ';;;;');
}
if (note) {
lines.push('NOTE:' + escapeVCardText(note));
}
lines.push('END:VCARD');
return lines.join('\r\n');
}
function updateVCardPreview(text) {
if (!vcardPreview) {
return;
}
if (contentModeSelect.value !== 'vcard') {
vcardPreview.textContent = '';
return;
}
if (text && String(text).trim()) {
vcardPreview.textContent = String(text);
return;
}
vcardPreview.textContent = 'Bitte mindestens Vorname, Nachname oder Organisation eingeben.';
}
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;
@@ -185,29 +727,162 @@ document.addEventListener('DOMContentLoaded', function() {
// Clear previous error // Clear previous error
errorMessage.textContent = ''; errorMessage.textContent = '';
downloadBtn.style.display = 'none'; downloadBtn.style.display = 'none';
if (mode !== 'vcard') {
updateVCardPreview('');
}
// Determine what to encode let qrText = '';
let qrText = text;
if (mode === 'event') {
// If wifi credentials are provided, generate wifi QR code const title = eventTitleInput.value.trim();
if (wifiSsid && wifiPassword) { const startRaw = combineDateTimeFromParts(
// Wifi QR code format: WIFI:S:<SSID>;T:WPA;P:<password>;; eventStartDateInput.value,
qrText = `WIFI:S:${escapeSpecialChars(wifiSsid)};T:WPA;P:${escapeSpecialChars(wifiPassword)};;`; eventStartHourInput.value,
} else if (wifiSsid && !wifiPassword) { eventStartMinuteInput.value
// SSID only (open network) );
qrText = `WIFI:S:${escapeSpecialChars(wifiSsid)};T:nopass;;`; const endD = eventEndDateInput.value;
} else if (!wifiSsid && wifiPassword) { const endH = eventEndHourInput.value;
// Password without SSID is invalid const endM = eventEndMinuteInput.value;
errorMessage.textContent = 'Bitte geben Sie eine SSID ein, wenn Sie ein Passwort angeben'; const endTimeComplete = endH !== '' && endM !== '';
qrcodeCanvas.style.display = 'none'; const endTimeAny = endH !== '' || endM !== '';
qrcodeImg.style.display = 'none'; const loc = eventLocationInput.value;
return; 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';
qrcodeImg.style.display = 'none';
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 === 'vcard') {
const firstName = vcardFirstNameInput.value.slice(0, MAX_VCARD_FIELD);
const lastName = vcardLastNameInput.value.slice(0, MAX_VCARD_FIELD);
const org = vcardOrgInput.value.slice(0, MAX_VCARD_FIELD);
const title = vcardTitleInput.value.slice(0, MAX_VCARD_FIELD);
const phone = vcardPhoneInput.value.slice(0, MAX_VCARD_FIELD);
const email = vcardEmailInput.value.slice(0, MAX_VCARD_FIELD);
const website = vcardWebsiteInput.value.slice(0, MAX_VCARD_FIELD);
const address = vcardAddressInput.value.slice(0, MAX_VCARD_FIELD);
const note = vcardNoteInput.value.slice(0, MAX_VCARD_FIELD);
const hasIdentity = firstName.trim() || lastName.trim() || org.trim();
if (!hasIdentity) {
updateVCardPreview('');
errorMessage.textContent = 'Bitte geben Sie mindestens Vorname, Nachname oder Organisation an';
qrcodeCanvas.style.display = 'none';
qrcodeImg.style.display = 'none';
return;
}
qrText = buildVCard({
firstName: firstName,
lastName: lastName,
org: org,
title: title,
phone: phone,
email: email,
website: website,
address: address,
note: note
});
updateVCardPreview(qrText);
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 if (mode === 'vcard') {
errorMessage.textContent = 'vCard 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 +945,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 +977,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,23 +991,95 @@ 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 === 'vcard') {
if (vcardFirstNameInput.value.trim()) {
params.set('vcardFirstName', vcardFirstNameInput.value.trim());
}
if (vcardLastNameInput.value.trim()) {
params.set('vcardLastName', vcardLastNameInput.value.trim());
}
if (vcardOrgInput.value.trim()) {
params.set('vcardOrg', vcardOrgInput.value.trim());
}
if (vcardTitleInput.value.trim()) {
params.set('vcardTitle', vcardTitleInput.value.trim());
}
if (vcardPhoneInput.value.trim()) {
params.set('vcardPhone', vcardPhoneInput.value.trim());
}
if (vcardEmailInput.value.trim()) {
params.set('vcardEmail', vcardEmailInput.value.trim());
}
if (vcardWebsiteInput.value.trim()) {
params.set('vcardWebsite', vcardWebsiteInput.value.trim());
}
if (vcardAddressInput.value.trim()) {
params.set('vcardAddress', vcardAddressInput.value.trim());
}
if (vcardNoteInput.value.trim()) {
params.set('vcardNote', vcardNoteInput.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.');
}); });
}); });
}); });