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
This commit is contained in:
2026-04-25 14:07:43 +02:00
parent 7790fe6dda
commit 0ca6e891cc
2 changed files with 243 additions and 3 deletions
+46 -1
View File
@@ -361,6 +361,7 @@
<option value="text">Text oder URL</option> <option value="text">Text oder URL</option>
<option value="wifi">WLAN</option> <option value="wifi">WLAN</option>
<option value="event">Termin (Kalender)</option> <option value="event">Termin (Kalender)</option>
<option value="vcard">vCard (Kontakt)</option>
</select> </select>
<div id="section-text" class="content-section"> <div id="section-text" class="content-section">
@@ -434,6 +435,50 @@
<textarea id="event-description" placeholder="" rows="3"></textarea> <textarea id="event-description" placeholder="" rows="3"></textarea>
</div> </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="" autocomplete="off">
</div>
<div class="options-col">
<label for="vcard-last-name">Nachname:</label>
<input type="text" id="vcard-last-name" placeholder="" autocomplete="off">
</div>
</div>
<div class="options-row">
<div class="options-col">
<label for="vcard-org">Organisation:</label>
<input type="text" id="vcard-org" placeholder="" autocomplete="off">
</div>
<div class="options-col">
<label for="vcard-title">Position (optional):</label>
<input type="text" id="vcard-title" placeholder="" autocomplete="off">
</div>
</div>
<div class="options-row">
<div class="options-col">
<label for="vcard-phone">Telefon (optional):</label>
<input type="tel" id="vcard-phone" placeholder="" autocomplete="off">
</div>
<div class="options-col">
<label for="vcard-email">E-Mail (optional):</label>
<input type="email" id="vcard-email" placeholder="" autocomplete="off">
</div>
</div>
<label for="vcard-website">Webseite (optional):</label>
<input type="url" id="vcard-website" placeholder="" autocomplete="off">
<label for="vcard-address">Adresse (optional):</label>
<textarea id="vcard-address" placeholder="" rows="2"></textarea>
<label for="vcard-note">Notiz (optional):</label>
<textarea id="vcard-note" placeholder="" rows="2"></textarea>
</div>
<div class="options-row"> <div class="options-row">
<div class="options-col"> <div class="options-col">
<label for="size">QR Code Gr&ouml;sse:</label> <label for="size">QR Code Gr&ouml;sse:</label>
@@ -508,7 +553,7 @@
<p>Be excellent to each other.</p> <p>Be excellent to each other.</p>
<h3>Modi</h3> <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>. Deine Eingaben für den QR-Code werden nicht an einen Server gesendet.</p> <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>
+197 -2
View File
@@ -6,6 +6,7 @@ document.addEventListener('DOMContentLoaded', function() {
const sectionText = document.getElementById('section-text'); const sectionText = document.getElementById('section-text');
const sectionWifi = document.getElementById('section-wifi'); const sectionWifi = document.getElementById('section-wifi');
const sectionEvent = document.getElementById('section-event'); 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 eventTitleInput = document.getElementById('event-title');
@@ -17,6 +18,15 @@ document.addEventListener('DOMContentLoaded', function() {
const eventEndMinuteInput = document.getElementById('event-end-minute'); const eventEndMinuteInput = document.getElementById('event-end-minute');
const eventLocationInput = document.getElementById('event-location'); const eventLocationInput = document.getElementById('event-location');
const eventDescriptionInput = document.getElementById('event-description'); 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');
@@ -36,6 +46,7 @@ document.addEventListener('DOMContentLoaded', function() {
const MAX_EVENT_TITLE = 2000; const MAX_EVENT_TITLE = 2000;
const MAX_EVENT_LOCATION = 1000; const MAX_EVENT_LOCATION = 1000;
const MAX_EVENT_DESCRIPTION = 8000; const MAX_EVENT_DESCRIPTION = 8000;
const MAX_VCARD_FIELD = 500;
// Title Easter egg // Title Easter egg
let titleClickCount = 0; let titleClickCount = 0;
@@ -85,6 +96,7 @@ document.addEventListener('DOMContentLoaded', function() {
sectionText.style.display = mode === 'text' ? 'block' : 'none'; sectionText.style.display = mode === 'text' ? 'block' : 'none';
sectionWifi.style.display = mode === 'wifi' ? 'block' : 'none'; sectionWifi.style.display = mode === 'wifi' ? 'block' : 'none';
sectionEvent.style.display = mode === 'event' ? 'block' : 'none'; sectionEvent.style.display = mode === 'event' ? 'block' : 'none';
sectionVcard.style.display = mode === 'vcard' ? 'block' : 'none';
} }
contentModeSelect.addEventListener('change', function() { contentModeSelect.addEventListener('change', function() {
@@ -97,6 +109,8 @@ document.addEventListener('DOMContentLoaded', function() {
wifiSsidInput.focus(); wifiSsidInput.focus();
} else if (this.value === 'event') { } else if (this.value === 'event') {
eventTitleInput.focus(); eventTitleInput.focus();
} else if (this.value === 'vcard') {
vcardFirstNameInput.focus();
} }
}); });
@@ -117,7 +131,16 @@ document.addEventListener('DOMContentLoaded', function() {
eventEndMinuteInput.value !== '' || eventEndMinuteInput.value !== '' ||
eventLocationInput.value.trim().length > 0 || eventLocationInput.value.trim().length > 0 ||
eventDescriptionInput.value.trim().length > 0; eventDescriptionInput.value.trim().length > 0;
if (hasText || hasWifi || hasEvent) { 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';
@@ -141,6 +164,15 @@ document.addEventListener('DOMContentLoaded', function() {
eventEndMinuteInput.value = ''; eventEndMinuteInput.value = '';
eventLocationInput.value = ''; eventLocationInput.value = '';
eventDescriptionInput.value = ''; eventDescriptionInput.value = '';
vcardFirstNameInput.value = '';
vcardLastNameInput.value = '';
vcardOrgInput.value = '';
vcardTitleInput.value = '';
vcardPhoneInput.value = '';
vcardEmailInput.value = '';
vcardWebsiteInput.value = '';
vcardAddressInput.value = '';
vcardNoteInput.value = '';
contentModeSelect.value = 'text'; contentModeSelect.value = 'text';
toggleContentSections(); toggleContentSections();
updateBerlinPreview(); updateBerlinPreview();
@@ -230,6 +262,19 @@ document.addEventListener('DOMContentLoaded', function() {
generateQRCode(); generateQRCode();
toggleClearButton(); 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);
@@ -286,7 +331,7 @@ document.addEventListener('DOMContentLoaded', function() {
let explicitModeFromUrl = false; let explicitModeFromUrl = false;
if (urlParams.has('mode')) { if (urlParams.has('mode')) {
const m = urlParams.get('mode'); const m = urlParams.get('mode');
if (m === 'text' || m === 'wifi' || m === 'event') { if (m === 'text' || m === 'wifi' || m === 'event' || m === 'vcard') {
contentModeSelect.value = m; contentModeSelect.value = m;
explicitModeFromUrl = true; explicitModeFromUrl = true;
} }
@@ -336,6 +381,41 @@ document.addEventListener('DOMContentLoaded', function() {
if (urlParams.has('eventDescription')) { if (urlParams.has('eventDescription')) {
eventDescriptionInput.value = sanitizeUrlString(urlParams.get('eventDescription'), MAX_EVENT_DESCRIPTION); eventDescriptionInput.value = sanitizeUrlString(urlParams.get('eventDescription'), MAX_EVENT_DESCRIPTION);
} }
const hasVcardParams = urlParams.has('vcardFirstName') ||
urlParams.has('vcardLastName') ||
urlParams.has('vcardOrg') ||
urlParams.has('vcardPhone') ||
urlParams.has('vcardEmail');
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');
@@ -555,6 +635,59 @@ document.addEventListener('DOMContentLoaded', function() {
return lines.join('\r\n'); 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 generateQRCode() { function generateQRCode() {
const mode = contentModeSelect.value; const mode = contentModeSelect.value;
const text = textInput.value.trim(); const text = textInput.value.trim();
@@ -663,6 +796,38 @@ document.addEventListener('DOMContentLoaded', function() {
description: desc.slice(0, MAX_EVENT_DESCRIPTION) 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) {
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
});
textInput.value = qrText; textInput.value = qrText;
toggleClearButton(); toggleClearButton();
} else if (mode === 'wifi') { } else if (mode === 'wifi') {
@@ -688,6 +853,8 @@ document.addEventListener('DOMContentLoaded', function() {
errorMessage.textContent = 'Bitte geben Sie Text/URL oder WLAN-Daten ein'; errorMessage.textContent = 'Bitte geben Sie Text/URL oder WLAN-Daten ein';
} else if (mode === 'event') { } else if (mode === 'event') {
errorMessage.textContent = 'Termin konnte nicht erzeugt werden'; errorMessage.textContent = 'Termin konnte nicht erzeugt werden';
} else if (mode === 'vcard') {
errorMessage.textContent = 'vCard konnte nicht erzeugt werden';
} else { } else {
errorMessage.textContent = 'Bitte geben Sie Text oder eine URL ein'; errorMessage.textContent = 'Bitte geben Sie Text oder eine URL ein';
} }
@@ -827,6 +994,34 @@ document.addEventListener('DOMContentLoaded', function() {
if (eventDescriptionInput.value.trim()) { if (eventDescriptionInput.value.trim()) {
params.set('eventDescription', 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') { } else if (mode === 'wifi') {
if (wifiSsid) { if (wifiSsid) {
params.set('ssid', wifiSsid); params.set('ssid', wifiSsid);