diff --git a/index.html b/index.html index 13de877..0ff6b07 100644 --- a/index.html +++ b/index.html @@ -1004,6 +1004,59 @@ } .btn-save:hover { background: var(--red-dark); } + /* Profile Extras Section in Modal */ + .extras-section-divider { + border: none; + border-top: 1px solid var(--border); + margin: 16px 0 12px; + } + .extras-section-title { + font-size: 0.72rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 1.5px; + color: var(--muted); + margin-bottom: 12px; + } + .offline-photo-row { + display: flex; + align-items: center; + gap: 12px; + padding: 10px 12px; + background: var(--bg); + border-radius: 8px; + border: 1px solid var(--border); + margin-bottom: 8px; + } + .offline-photo-thumb { + width: 52px; height: 52px; + border-radius: 50%; + object-fit: cover; + border: 2px solid var(--border); + flex-shrink: 0; + } + .offline-photo-actions { + display: flex; + gap: 8px; + flex-wrap: wrap; + align-items: center; + } + .btn-file-label { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 5px 12px; + font-size: 0.82rem; + font-weight: 600; + cursor: pointer; + border-radius: 6px; + border: 1px solid var(--border); + background: white; + color: var(--muted); + transition: all 0.15s; + } + .btn-file-label:hover { background: white; color: var(--text); } + /* Import/Export */ .import-export-panel { padding: 16px; @@ -1307,13 +1360,32 @@ - + +
+
Dauerhaft gespeichert (bleibt nach BNI-Sync erhalten)
+ + + + + +
+ +
+ + + +
+
+ + +
+
Profildaten (Kurzlinks + Offline-Fotos)
+

+ Kurzlinks und lokale Fotos werden getrennt gespeichert und bleiben nach jedem BNI-Sync erhalten. + Diese Datei im Projektordner ablegen und nach einem Browser-Reset wieder importieren. +

+
+ + + +
@@ -1595,6 +1682,161 @@ function saveState(broadcast = true) { } } +// ============================================================ +// PROFILE EXTRAS (Kurzlinks + Offline-Fotos, persistent) +// ============================================================ +const EXTRAS_KEY = 'bni-profile-extras'; + +function loadExtras() { + try { + const s = localStorage.getItem(EXTRAS_KEY); + return s ? JSON.parse(s) : {}; + } catch(e) { return {}; } +} + +function saveExtras(extras) { + localStorage.setItem(EXTRAS_KEY, JSON.stringify(extras)); +} + +function extrasKey(name) { + return (name || '').toLowerCase().trim(); +} + +// Resize an image File to base64 JPEG (max maxPx on longest side) +function resizePhotoToBase64(file, maxPx = 400) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = (e) => { + const img = new Image(); + img.onload = () => { + let w = img.width, h = img.height; + if (w > maxPx || h > maxPx) { + if (w > h) { h = Math.round(h * maxPx / w); w = maxPx; } + else { w = Math.round(w * maxPx / h); h = maxPx; } + } + const canvas = document.createElement('canvas'); + canvas.width = w; canvas.height = h; + canvas.getContext('2d').drawImage(img, 0, 0, w, h); + resolve(canvas.toDataURL('image/jpeg', 0.82)); + }; + img.onerror = reject; + img.src = e.target.result; + }; + reader.onerror = reject; + reader.readAsDataURL(file); + }); +} + +// Try to load a remote photo as base64 via canvas (requires CORS) +function loadPhotoFromUrl(url) { + return new Promise((resolve, reject) => { + const img = new Image(); + img.crossOrigin = 'anonymous'; + img.onload = () => { + const maxPx = 400; + let w = img.width, h = img.height; + if (w > maxPx || h > maxPx) { + if (w > h) { h = Math.round(h * maxPx / w); w = maxPx; } + else { w = Math.round(w * maxPx / h); h = maxPx; } + } + const canvas = document.createElement('canvas'); + canvas.width = w; canvas.height = h; + canvas.getContext('2d').drawImage(img, 0, 0, w, h); + resolve(canvas.toDataURL('image/jpeg', 0.82)); + }; + img.onerror = () => reject(new Error('Foto konnte nicht geladen werden (CORS oder URL ungültig).')); + img.src = url; + }); +} + +// Module-level state for currently editing offline photo +let _editingOfflinePhotoB64 = null; + +function _updateOfflinePhotoUI(b64) { + _editingOfflinePhotoB64 = b64 || null; + const thumb = document.getElementById('offline-photo-thumb'); + const removeBtn = document.getElementById('btn-remove-offline-photo'); + if (b64) { + thumb.src = b64; + thumb.style.display = 'block'; + removeBtn.style.display = 'inline-flex'; + } else { + thumb.src = ''; + thumb.style.display = 'none'; + removeBtn.style.display = 'none'; + } +} + +function handleOfflinePhotoFile(input) { + const file = input.files && input.files[0]; + if (!file) return; + resizePhotoToBase64(file) + .then(b64 => _updateOfflinePhotoUI(b64)) + .catch(e => alert('Fehler beim Laden des Fotos: ' + e.message)); + input.value = ''; +} + +function loadOfflinePhotoFromUrl() { + const url = (document.getElementById('m-photo').value || '').trim(); + if (!url) { alert('Bitte zuerst eine Foto-URL eingeben.'); return; } + // Remove any previous hint + const existingHint = document.getElementById('offline-photo-url-hint'); + if (existingHint) existingHint.remove(); + loadPhotoFromUrl(url) + .then(b64 => _updateOfflinePhotoUI(b64)) + .catch(() => { + // Show inline hint with direct link instead of an alert + const hint = document.createElement('div'); + hint.id = 'offline-photo-url-hint'; + hint.style.cssText = 'font-size:0.82rem;color:var(--muted);margin-top:6px;line-height:1.5;'; + hint.innerHTML = `Server erlaubt keinen direkten Import. Foto öffnen ↗ → Rechtsklick → Bild speichern → dann über Datei wählen hochladen.`; + document.getElementById('offline-photo-row').insertAdjacentElement('afterend', hint); + }); +} + +function removeOfflinePhoto() { + _updateOfflinePhotoUI(null); +} + +// Export profildaten.json +function exportProfildaten() { + const extras = loadExtras(); + const blob = new Blob([JSON.stringify({ version: 1, extras }, null, 2)], { type: 'application/json' }); + const a = document.createElement('a'); + a.href = URL.createObjectURL(blob); + a.download = 'profildaten.json'; + a.click(); + document.getElementById('profildaten-status').textContent = '✓ Gespeichert'; + setTimeout(() => { document.getElementById('profildaten-status').textContent = ''; }, 3000); +} + +// Import profildaten.json +function importProfildaten(input) { + const file = input.files && input.files[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = (e) => { + try { + const data = JSON.parse(e.target.result); + const incoming = data.extras || data; // support both formats + const existing = loadExtras(); + // Merge: incoming data wins for kurzlink/photoB64 per member + Object.entries(incoming).forEach(([k, v]) => { + existing[k] = Object.assign(existing[k] || {}, v); + }); + saveExtras(existing); + const count = Object.keys(incoming).length; + document.getElementById('profildaten-status').textContent = `✓ ${count} Einträge geladen`; + setTimeout(() => { document.getElementById('profildaten-status').textContent = ''; }, 4000); + renderCurrentView(); + } catch(err) { + alert('Ungültige Datei: ' + err.message); + } + input.value = ''; + }; + reader.readAsText(file); +} + // ============================================================ // BROADCAST CHANNEL (cross-tab sync) // ============================================================ @@ -1889,14 +2131,14 @@ function renderDisplay() { qrWrap.style.display = 'block'; qrEl.innerHTML = ''; try { - const qrSize = Math.round(Math.min(window.innerHeight * 0.16, window.innerWidth * 0.09, 200)); + const qrSize = Math.round(Math.min(window.innerHeight * 0.24, window.innerWidth * 0.14, 300)); new QRCode(qrEl, { text: data.profileUrl, width: qrSize, height: qrSize, colorDark: '#1a2744', colorLight: '#ffffff', - correctLevel: QRCode.CorrectLevel.M, + correctLevel: QRCode.CorrectLevel.L, }); } catch(e) { qrWrap.style.display = 'none'; } } else { @@ -2097,13 +2339,15 @@ function getDisplayData(presenter) { } resolvedSub = { ...sub, photoUrl }; } + // Merge profile extras (kurzlink + offline photo override) + const ex = loadExtras()[extrasKey(presenter.name)] || {}; return { // Member stays in foreground name: presenter.name, company: presenter.company, specialty: presenter.specialty, - photoUrl: presenter.photoUrl || '', - profileUrl: presenter.profileUrl || '', + photoUrl: ex.photoB64 || presenter.photoUrl || '', + profileUrl: ex.kurzlink || presenter.profileUrl || '', // Substitute shown as secondary presenter presentedBy: resolvedSub, isVisitor: false, @@ -2572,7 +2816,9 @@ function openAddMember() { document.getElementById('m-specialty').value = ''; document.getElementById('m-photo').value = ''; document.getElementById('m-profile').value = ''; + document.getElementById('m-kurzlink').value = ''; document.getElementById('photo-preview-wrap').style.display = 'none'; + _updateOfflinePhotoUI(null); openModal('modal-member'); } @@ -2593,6 +2839,12 @@ function editMember(id) { } else { document.getElementById('photo-preview-wrap').style.display = 'none'; } + + // Load extras for this member + const ex = loadExtras()[extrasKey(m.name)] || {}; + document.getElementById('m-kurzlink').value = ex.kurzlink || ''; + _updateOfflinePhotoUI(ex.photoB64 || null); + openModal('modal-member'); } @@ -2614,6 +2866,7 @@ function saveMember() { const specialty = document.getElementById('m-specialty').value.trim(); const photoUrl = document.getElementById('m-photo').value.trim(); const profileUrl = document.getElementById('m-profile').value.trim(); + const kurzlink = document.getElementById('m-kurzlink').value.trim(); if (!name) { alert('Bitte Name eingeben.'); return; } @@ -2631,6 +2884,18 @@ function saveMember() { } } + // Save extras (kurzlink + offline photo) — keyed by name + const extras = loadExtras(); + const key = extrasKey(name); + const entry = extras[key] || {}; + if (kurzlink) entry.kurzlink = kurzlink; + else delete entry.kurzlink; + if (_editingOfflinePhotoB64) entry.photoB64 = _editingOfflinePhotoB64; + else delete entry.photoB64; + if (Object.keys(entry).length > 0) extras[key] = entry; + else delete extras[key]; + saveExtras(extras); + saveState(); closeMemberModal(); renderControl(); @@ -2651,6 +2916,8 @@ function deleteMember(id) { } function closeMemberModal() { + const hint = document.getElementById('offline-photo-url-hint'); + if (hint) hint.remove(); closeModal('modal-member'); } diff --git a/profildaten.json b/profildaten.json new file mode 100644 index 0000000..d9312e3 --- /dev/null +++ b/profildaten.json @@ -0,0 +1,4 @@ +{ + "version": 1, + "extras": {} +} diff --git a/profile-image-type-person-1367611.JPG b/profile-image-type-person-1367611.JPG new file mode 100644 index 0000000..92ddf47 Binary files /dev/null and b/profile-image-type-person-1367611.JPG differ diff --git a/profile-image-type-person-1368964.jpg b/profile-image-type-person-1368964.jpg new file mode 100644 index 0000000..73bb049 Binary files /dev/null and b/profile-image-type-person-1368964.jpg differ diff --git a/profile-image-type-person-1612262.jpeg b/profile-image-type-person-1612262.jpeg new file mode 100644 index 0000000..80fff8b Binary files /dev/null and b/profile-image-type-person-1612262.jpeg differ diff --git a/profile-image-type-person-1861412.jpg b/profile-image-type-person-1861412.jpg new file mode 100644 index 0000000..56d5b72 Binary files /dev/null and b/profile-image-type-person-1861412.jpg differ diff --git a/profile-image-type-person-1889624.jpg b/profile-image-type-person-1889624.jpg new file mode 100644 index 0000000..82c69cc Binary files /dev/null and b/profile-image-type-person-1889624.jpg differ diff --git a/profile-image-type-person-2132885.jpg b/profile-image-type-person-2132885.jpg new file mode 100644 index 0000000..de44234 Binary files /dev/null and b/profile-image-type-person-2132885.jpg differ diff --git a/profile-image-type-person-2155257.jpg b/profile-image-type-person-2155257.jpg new file mode 100644 index 0000000..788471c Binary files /dev/null and b/profile-image-type-person-2155257.jpg differ diff --git a/profile-image-type-person-2249834.jpg b/profile-image-type-person-2249834.jpg new file mode 100644 index 0000000..a1f0bce Binary files /dev/null and b/profile-image-type-person-2249834.jpg differ diff --git a/profile-image-type-person-2290957.jpg b/profile-image-type-person-2290957.jpg new file mode 100644 index 0000000..55a8de2 Binary files /dev/null and b/profile-image-type-person-2290957.jpg differ diff --git a/profile-image-type-person-2326664.jpg b/profile-image-type-person-2326664.jpg new file mode 100644 index 0000000..f5312ee Binary files /dev/null and b/profile-image-type-person-2326664.jpg differ diff --git a/profile-image-type-person-317636.jpg b/profile-image-type-person-317636.jpg new file mode 100644 index 0000000..3455a9d Binary files /dev/null and b/profile-image-type-person-317636.jpg differ