Compare commits

...

6 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
3 changed files with 293 additions and 4 deletions
+1
View File
@@ -0,0 +1 @@
*.zip
+70 -2
View File
@@ -348,6 +348,26 @@
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>
</head>
<body>
@@ -361,6 +381,7 @@
<option value="text">Text oder URL</option>
<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">
@@ -434,6 +455,53 @@
<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 class="options-row">
<div class="options-col">
<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 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 class="options-row">
<div class="options-col">
<label for="size">QR Code Gr&ouml;sse:</label>
@@ -508,13 +576,13 @@
<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>. 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 class="footer">
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>
</body></html>
+222 -2
View File
@@ -6,6 +6,7 @@ document.addEventListener('DOMContentLoaded', function() {
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 wifiPasswordInput = document.getElementById('wifi-password');
const eventTitleInput = document.getElementById('event-title');
@@ -17,6 +18,15 @@ document.addEventListener('DOMContentLoaded', function() {
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 errorCorrectionSelect = document.getElementById('errorCorrection');
const foregroundColor = document.getElementById('foreground');
@@ -32,10 +42,12 @@ document.addEventListener('DOMContentLoaded', function() {
const shareBtn = document.getElementById('share');
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
let titleClickCount = 0;
@@ -85,6 +97,7 @@ document.addEventListener('DOMContentLoaded', function() {
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() {
@@ -97,6 +110,8 @@ document.addEventListener('DOMContentLoaded', function() {
wifiSsidInput.focus();
} else if (this.value === 'event') {
eventTitleInput.focus();
} else if (this.value === 'vcard') {
vcardFirstNameInput.focus();
}
});
@@ -117,7 +132,16 @@ document.addEventListener('DOMContentLoaded', function() {
eventEndMinuteInput.value !== '' ||
eventLocationInput.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';
} else {
clearTextBtn.style.display = 'none';
@@ -141,6 +165,15 @@ document.addEventListener('DOMContentLoaded', function() {
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();
@@ -230,6 +263,19 @@ document.addEventListener('DOMContentLoaded', 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
downloadBtn.addEventListener('click', downloadQRCode);
@@ -286,7 +332,7 @@ document.addEventListener('DOMContentLoaded', function() {
let explicitModeFromUrl = false;
if (urlParams.has('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;
explicitModeFromUrl = true;
}
@@ -336,6 +382,45 @@ document.addEventListener('DOMContentLoaded', function() {
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')) {
const sizeValue = urlParams.get('size');
@@ -555,6 +640,74 @@ document.addEventListener('DOMContentLoaded', function() {
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() {
const mode = contentModeSelect.value;
const text = textInput.value.trim();
@@ -574,6 +727,9 @@ document.addEventListener('DOMContentLoaded', function() {
// Clear previous error
errorMessage.textContent = '';
downloadBtn.style.display = 'none';
if (mode !== 'vcard') {
updateVCardPreview('');
}
let qrText = '';
@@ -663,6 +819,40 @@ document.addEventListener('DOMContentLoaded', function() {
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') {
@@ -688,6 +878,8 @@ document.addEventListener('DOMContentLoaded', function() {
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';
}
@@ -827,6 +1019,34 @@ document.addEventListener('DOMContentLoaded', function() {
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);