Weiter an den Locations gearbeitet
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
bin/main/de/oaa/xxx/location/LocationAdminController.class
Normal file
BIN
bin/main/de/oaa/xxx/location/LocationAdminController.class
Normal file
Binary file not shown.
BIN
bin/main/de/oaa/xxx/location/LocationChatCleanupService.class
Normal file
BIN
bin/main/de/oaa/xxx/location/LocationChatCleanupService.class
Normal file
Binary file not shown.
BIN
bin/main/de/oaa/xxx/location/LocationController$AdminDto.class
Normal file
BIN
bin/main/de/oaa/xxx/location/LocationController$AdminDto.class
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
bin/main/de/oaa/xxx/location/entity/LocationAdminEntity.class
Normal file
BIN
bin/main/de/oaa/xxx/location/entity/LocationAdminEntity.class
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -20,7 +20,7 @@
|
||||
.evt-date { font-size:0.88rem; color:var(--color-muted); margin-bottom:0.5rem; }
|
||||
.evt-desc { font-size:0.93rem; line-height:1.55; white-space:pre-wrap; word-break:break-word; margin-top:0.5rem; }
|
||||
|
||||
.attend-btn { display:inline-flex; align-items:center; gap:0.4rem; margin-top:0.75rem; }
|
||||
.attend-btn { display:inline-flex; align-items:center; gap:0.4rem; margin-top:0.75rem; flex-wrap:wrap; }
|
||||
|
||||
.section-title { font-size:1rem; font-weight:700; margin:1.5rem 0 0.75rem; }
|
||||
.gender-group { margin-bottom:1.25rem; }
|
||||
@@ -31,23 +31,92 @@
|
||||
.attendee-avatar { width:28px; height:28px; border-radius:50%; background:var(--color-secondary); object-fit:cover; flex-shrink:0; overflow:hidden; display:flex; align-items:center; justify-content:center; font-size:0.8rem; }
|
||||
.attendee-avatar img { width:100%; height:100%; object-fit:cover; }
|
||||
.count-badge { background:var(--color-secondary); border-radius:12px; padding:0.15rem 0.6rem; font-size:0.78rem; color:var(--color-muted); margin-left:0.25rem; display:inline-block; }
|
||||
|
||||
/* Modal */
|
||||
.modal-overlay { display:none; position:fixed; inset:0; background:rgba(0,0,0,.6); z-index:200; align-items:center; justify-content:center; }
|
||||
.modal-overlay.open { display:flex; }
|
||||
.modal { background:var(--color-card); border-radius:12px; width:min(520px,95vw); max-height:90vh; overflow-y:auto; padding:1.5rem; }
|
||||
.modal h3 { margin:0 0 1rem; }
|
||||
.modal-footer { display:flex; gap:0.75rem; justify-content:flex-end; margin-top:1.25rem; flex-wrap:wrap; }
|
||||
.img-preview { width:80px; height:80px; border-radius:8px; object-fit:cover; background:var(--color-secondary); display:flex; align-items:center; justify-content:center; font-size:1.5rem; flex-shrink:0; overflow:hidden; border:1px solid var(--color-secondary); }
|
||||
.img-preview img { width:100%; height:100%; object-fit:cover; }
|
||||
.img-row { display:flex; gap:0.75rem; align-items:center; margin-bottom:0.5rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="main">
|
||||
<a id="backLink" href="/community/events.html" class="back-link">← Veranstaltungen</a>
|
||||
<body class="app">
|
||||
|
||||
<div id="content">
|
||||
<p style="color:var(--color-muted);">Wird geladen…</p>
|
||||
<!-- ── Hinweis-Modal ──────────────────────────────────────────────────────────── -->
|
||||
<div class="modal-overlay" id="alertModal">
|
||||
<div class="modal" style="width:min(380px,95vw);">
|
||||
<p id="alertMessage" style="margin:0 0 1.25rem;font-size:0.95rem;"></p>
|
||||
<div class="modal-footer">
|
||||
<button class="btn" onclick="document.getElementById('alertModal').classList.remove('open')">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Bestätigungs-Modal ─────────────────────────────────────────────────────── -->
|
||||
<div class="modal-overlay" id="confirmModal">
|
||||
<div class="modal" style="width:min(380px,95vw);">
|
||||
<h3 id="confirmTitle" style="margin:0 0 0.75rem;"></h3>
|
||||
<p id="confirmMessage" style="margin:0 0 1.25rem;font-size:0.95rem;color:var(--color-muted);"></p>
|
||||
<div class="modal-footer">
|
||||
<button class="btn" style="background:var(--color-secondary);color:var(--color-text);" onclick="closeConfirm()">Abbrechen</button>
|
||||
<button class="btn" id="confirmOkBtn" style="background:#c0392b;">Löschen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Edit-Modal ────────────────────────────────────────────────────────────── -->
|
||||
<div class="modal-overlay" id="editModal">
|
||||
<div class="modal">
|
||||
<h3>Veranstaltung bearbeiten</h3>
|
||||
<div class="img-row">
|
||||
<div class="img-preview" id="editPicPreview">🗓</div>
|
||||
<div>
|
||||
<label class="btn" style="display:inline-block;cursor:pointer;font-size:0.85rem;">
|
||||
Bild ändern
|
||||
<input type="file" id="editPicFile" accept="image/*" style="display:none;" onchange="onEditPicChange(this)">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<label>Titel *</label>
|
||||
<input type="text" id="editTitle" maxlength="200">
|
||||
<label>Beschreibung <span style="color:var(--color-muted);font-size:0.8rem;">(max. 1000 Zeichen)</span></label>
|
||||
<textarea id="editDesc" maxlength="1000" rows="4"
|
||||
style="resize:vertical;width:100%;box-sizing:border-box;padding:0.65rem 0.9rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:1rem;outline:none;font-family:inherit;transition:border-color 0.2s;"
|
||||
onfocus="this.style.borderColor='var(--color-primary)'" onblur="this.style.borderColor='var(--color-secondary)'"></textarea>
|
||||
<label>Datum & Uhrzeit *</label>
|
||||
<input type="datetime-local" id="editStartAt">
|
||||
<div class="modal-footer">
|
||||
<button class="btn" style="background:var(--color-secondary);color:var(--color-text);" onclick="closeEditModal()">Abbrechen</button>
|
||||
<button class="btn" id="editSubmitBtn" onclick="submitEdit()">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main">
|
||||
<div class="content">
|
||||
<a id="backLink" href="/community/events.html" class="back-link">← Veranstaltungen</a>
|
||||
<div id="content">
|
||||
<p style="color:var(--color-muted);">Wird geladen…</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/icons.js"></script>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script>
|
||||
const params = new URLSearchParams(location.search);
|
||||
const eventId = params.get('id');
|
||||
let myUserId = null;
|
||||
let _evtData = null;
|
||||
let _editImg = null;
|
||||
|
||||
function showAlert(msg) {
|
||||
document.getElementById('alertMessage').textContent = msg;
|
||||
document.getElementById('alertModal').classList.add('open');
|
||||
}
|
||||
|
||||
function escHtml(s) {
|
||||
return (s || '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
@@ -78,17 +147,16 @@ async function loadPage() {
|
||||
if (!evtRes.ok) { document.getElementById('content').innerHTML = '<p>Veranstaltung nicht gefunden.</p>'; return; }
|
||||
if (meRes.ok) { const me = await meRes.json(); myUserId = me.userId; }
|
||||
|
||||
const evt = await evtRes.json();
|
||||
_evtData = await evtRes.json();
|
||||
|
||||
// Rücklink zur Location
|
||||
const backLink = document.getElementById('backLink');
|
||||
if (evt.locationId) {
|
||||
backLink.href = `/community/location-detail.html?id=${evt.locationId}`;
|
||||
backLink.textContent = `← ${escHtml(evt.locationName) || 'Location'}`;
|
||||
if (_evtData.locationId) {
|
||||
backLink.href = `/community/location-detail.html?id=${_evtData.locationId}`;
|
||||
backLink.textContent = `← ${_evtData.locationName || 'Location'}`;
|
||||
}
|
||||
document.title = `${evt.title} – xXx Sphere`;
|
||||
document.title = `${_evtData.title} – xXx Sphere`;
|
||||
|
||||
renderPage(evt);
|
||||
renderPage(_evtData);
|
||||
}
|
||||
|
||||
function renderPage(evt) {
|
||||
@@ -96,7 +164,9 @@ function renderPage(evt) {
|
||||
? `<img src="data:image/jpeg;base64,${evt.imageData}" alt="${escHtml(evt.title)}">`
|
||||
: '🗓';
|
||||
|
||||
// Teilnehmende nach Geschlecht gruppieren
|
||||
const isFuture = new Date(evt.startAt) > new Date();
|
||||
const canEdit = !!evt.isAdmin && isFuture;
|
||||
|
||||
const byGender = {};
|
||||
(evt.attendees || []).forEach(a => {
|
||||
const g = a.geschlecht || 'UNBEKANNT';
|
||||
@@ -134,13 +204,20 @@ function renderPage(evt) {
|
||||
${evt.locationName ? `<div class="evt-location">📍 <a href="/community/location-detail.html?id=${evt.locationId}" style="color:inherit;text-decoration:none;">${escHtml(evt.locationName)}</a></div>` : ''}
|
||||
<div class="evt-date">🗓 ${formatDate(evt.startAt)}</div>
|
||||
${evt.description ? `<div class="evt-desc">${escHtml(evt.description)}</div>` : ''}
|
||||
<div class="attend-btn">
|
||||
<button class="btn" id="attendBtn"
|
||||
style="${attending ? 'background:var(--color-secondary);color:var(--color-text);' : ''}"
|
||||
onclick="toggleAttend()">
|
||||
${attending ? '✓ Ich bin dabei' : '+ Ich bin dabei'}
|
||||
</button>
|
||||
<span style="color:var(--color-muted);font-size:0.85rem;" id="attendCount">${totalAttendees} Teilnehmer*in(nen)</span>
|
||||
<div style="display:flex;flex-direction:column;gap:0.5rem;margin-top:0.75rem;">
|
||||
${canEdit ? `
|
||||
<div style="display:flex;align-items:center;gap:0.4rem;flex-wrap:wrap;">
|
||||
<button class="btn" style="background:var(--color-secondary);color:var(--color-text);font-size:0.85rem;" onclick="openEditModal()">✎ Bearbeiten</button>
|
||||
<button class="btn" style="background:#c0392b;font-size:0.85rem;" onclick="openDeleteConfirm()">Löschen</button>
|
||||
</div>` : ''}
|
||||
<div style="display:flex;align-items:center;gap:0.4rem;flex-wrap:wrap;">
|
||||
<button class="btn" id="attendBtn"
|
||||
style="${attending ? 'background:var(--color-secondary);color:var(--color-text);' : ''}"
|
||||
onclick="toggleAttend()">
|
||||
${attending ? '✓ Ich bin dabei' : '+ Ich bin dabei'}
|
||||
</button>
|
||||
<span style="color:var(--color-muted);font-size:0.85rem;" id="attendCount">${totalAttendees} Teilnehmer*in(nen)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -154,9 +231,8 @@ function renderPage(evt) {
|
||||
|
||||
async function toggleAttend() {
|
||||
const res = await fetch(`/location-events/${eventId}/attend`, { method: 'POST' });
|
||||
if (!res.ok) { alert('Fehler beim Aktualisieren.'); return; }
|
||||
if (!res.ok) { showAlert('Fehler beim Aktualisieren.'); return; }
|
||||
const data = await res.json();
|
||||
|
||||
const btn = document.getElementById('attendBtn');
|
||||
const countEl = document.getElementById('attendCount');
|
||||
if (btn) {
|
||||
@@ -165,10 +241,107 @@ async function toggleAttend() {
|
||||
btn.style.color = data.attending ? 'var(--color-text)' : '';
|
||||
}
|
||||
if (countEl) countEl.textContent = `${data.attendeeCount} Teilnehmer*in(nen)`;
|
||||
|
||||
// Teilnehmendenliste neu laden
|
||||
const evtRes = await fetch(`/location-events/${eventId}`);
|
||||
if (evtRes.ok) { renderPage(await evtRes.json()); }
|
||||
if (evtRes.ok) { _evtData = await evtRes.json(); renderPage(_evtData); }
|
||||
}
|
||||
|
||||
// ── Edit Modal ────────────────────────────────────────────────────────────────
|
||||
function resizeImage(file, maxPx, quality) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
const url = URL.createObjectURL(file);
|
||||
img.onload = () => {
|
||||
URL.revokeObjectURL(url);
|
||||
let w = img.naturalWidth, h = img.naturalHeight;
|
||||
if (w > maxPx || h > maxPx) {
|
||||
if (w >= h) { h = Math.max(1, Math.round(maxPx * h / w)); w = maxPx; }
|
||||
else { w = Math.max(1, Math.round(maxPx * w / 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', quality || 0.85).split(',')[1]);
|
||||
};
|
||||
img.onerror = reject;
|
||||
img.src = url;
|
||||
});
|
||||
}
|
||||
|
||||
function openEditModal() {
|
||||
if (!_evtData) return;
|
||||
_editImg = null;
|
||||
document.getElementById('editTitle').value = _evtData.title || '';
|
||||
document.getElementById('editDesc').value = _evtData.description || '';
|
||||
const nowLocal = new Date(Date.now() - new Date().getTimezoneOffset() * 60000).toISOString().slice(0, 16);
|
||||
const startVal = _evtData.startAt ? _evtData.startAt.slice(0, 16) : '';
|
||||
document.getElementById('editStartAt').min = nowLocal;
|
||||
document.getElementById('editStartAt').value = startVal;
|
||||
const preview = document.getElementById('editPicPreview');
|
||||
preview.innerHTML = _evtData.imageData
|
||||
? `<img src="data:image/jpeg;base64,${_evtData.imageData}" alt="">`
|
||||
: '🗓';
|
||||
document.getElementById('editModal').classList.add('open');
|
||||
}
|
||||
|
||||
function closeEditModal() { document.getElementById('editModal').classList.remove('open'); }
|
||||
|
||||
async function onEditPicChange(input) {
|
||||
const file = input.files[0]; if (!file) return;
|
||||
_editImg = await resizeImage(file, 1024, 0.88);
|
||||
document.getElementById('editPicPreview').innerHTML = `<img src="data:image/jpeg;base64,${_editImg}" alt="">`;
|
||||
}
|
||||
|
||||
async function submitEdit() {
|
||||
const title = document.getElementById('editTitle').value.trim();
|
||||
const desc = document.getElementById('editDesc').value;
|
||||
const startAt = document.getElementById('editStartAt').value;
|
||||
if (!title) { showAlert('Bitte gib einen Titel ein.'); return; }
|
||||
if (!startAt) { showAlert('Bitte wähle Datum und Uhrzeit.'); return; }
|
||||
if (new Date(startAt) <= new Date()) { showAlert('Der Termin muss in der Zukunft liegen.'); return; }
|
||||
|
||||
const btn = document.getElementById('editSubmitBtn');
|
||||
btn.disabled = true; btn.textContent = 'Wird gespeichert…';
|
||||
try {
|
||||
const body = { title, description: desc };
|
||||
if (startAt) body.startAt = startAt + ':00';
|
||||
if (_editImg) body.imageData = _editImg;
|
||||
|
||||
const res = await fetch(`/locations/${_evtData.locationId}/events/${eventId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
if (res.status === 422) { showAlert('Der Termin muss in der Zukunft liegen.'); return; }
|
||||
if (!res.ok) { showAlert('Fehler beim Speichern.'); return; }
|
||||
_evtData = await res.json();
|
||||
closeEditModal();
|
||||
renderPage(_evtData);
|
||||
document.title = `${_evtData.title} – xXx Sphere`;
|
||||
} catch { showAlert('Fehler beim Speichern.'); }
|
||||
finally { btn.disabled = false; btn.textContent = 'Speichern'; }
|
||||
}
|
||||
|
||||
// ── Confirm / Delete ──────────────────────────────────────────────────────────
|
||||
let _confirmCallback = null;
|
||||
function openConfirm(title, message, onOk) {
|
||||
document.getElementById('confirmTitle').textContent = title;
|
||||
document.getElementById('confirmMessage').textContent = message;
|
||||
_confirmCallback = onOk;
|
||||
document.getElementById('confirmModal').classList.add('open');
|
||||
}
|
||||
function closeConfirm() { document.getElementById('confirmModal').classList.remove('open'); _confirmCallback = null; }
|
||||
document.getElementById('confirmOkBtn').addEventListener('click', () => { if (_confirmCallback) _confirmCallback(); closeConfirm(); });
|
||||
|
||||
function openDeleteConfirm() {
|
||||
openConfirm(
|
||||
'Veranstaltung löschen',
|
||||
'Soll diese Veranstaltung wirklich gelöscht werden? Alle angemeldeten Teilnehmenden werden benachrichtigt.',
|
||||
async () => {
|
||||
const res = await fetch(`/locations/${_evtData.locationId}/events/${eventId}`, { method: 'DELETE' });
|
||||
if (!res.ok) { showAlert('Fehler beim Löschen.'); return; }
|
||||
location.href = `/community/location-detail.html?id=${_evtData.locationId}`;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
loadPage();
|
||||
|
||||
@@ -273,7 +273,15 @@
|
||||
: '';
|
||||
|
||||
const gruppeIdAttr = p.gruppeId ? ` data-gruppe-id="${p.gruppeId}"` : '';
|
||||
return `<div class="post-card" id="pc-${p.postId}"${gruppeIdAttr} onclick="openLb('${p.postId}','${p.postType}')" style="cursor:pointer;">
|
||||
const hasTarget = !!p.targetUrl;
|
||||
const cardClick = hasTarget
|
||||
? `window.location.href='${p.targetUrl}'`
|
||||
: `openLb('${p.postId}','${p.postType}')`;
|
||||
const commentBtn = hasTarget ? '' : `
|
||||
<button class="post-action-btn" onclick="event.stopPropagation(); openLb('${p.postId}','${p.postType}')">
|
||||
💬 <span id="kc-${p.postId}">${p.kommentarCount}</span>
|
||||
</button>`;
|
||||
return `<div class="post-card" id="pc-${p.postId}"${gruppeIdAttr} onclick="${cardClick}" style="cursor:pointer;">
|
||||
<div class="post-header">
|
||||
<div class="post-avatar">${avatarHtml}</div>
|
||||
<div>
|
||||
@@ -289,9 +297,7 @@
|
||||
<button class="post-action-btn${p.likedByMe ? ' active' : ''}" id="lk-${p.postId}" onclick="event.stopPropagation(); likePost('${p.postId}','${p.postType}')">
|
||||
♥ <span id="lkc-${p.postId}">${p.likeCount}</span>
|
||||
</button>
|
||||
<button class="post-action-btn" onclick="event.stopPropagation(); openLb('${p.postId}','${p.postType}')">
|
||||
💬 <span id="kc-${p.postId}">${p.kommentarCount}</span>
|
||||
</button>
|
||||
${commentBtn}
|
||||
${meldenBtn}
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
.back-link:hover { color:var(--color-primary); }
|
||||
|
||||
.loc-header { display:flex; gap:1rem; align-items:flex-start; margin-bottom:1.25rem; flex-wrap:wrap; }
|
||||
.loc-avatar { width:96px; height:96px; border-radius:12px; background:var(--color-secondary); object-fit:cover; flex-shrink:0; display:flex; align-items:center; justify-content:center; font-size:2.5rem; overflow:hidden; border:2px solid var(--color-secondary); }
|
||||
.loc-avatar { width:120px; height:120px; border-radius:12px; background:var(--color-secondary); object-fit:cover; flex-shrink:0; display:flex; align-items:center; justify-content:center; font-size:2.5rem; overflow:hidden; border:2px solid var(--color-secondary); }
|
||||
.loc-avatar img { width:100%; height:100%; object-fit:cover; }
|
||||
.loc-meta { flex:1; min-width:0; }
|
||||
.loc-name { font-size:1.4rem; font-weight:700; margin:0 0 0.3rem; }
|
||||
@@ -26,7 +26,7 @@
|
||||
.hours-table td:first-child { font-weight:500; width:100px; }
|
||||
.hours-closed { color:var(--color-muted); }
|
||||
|
||||
.gallery-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(120px,1fr)); gap:0.6rem; }
|
||||
.gallery-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(180px,1fr)); gap:0.6rem; }
|
||||
.gallery-img-wrap { position:relative; aspect-ratio:1; border-radius:8px; overflow:hidden; background:var(--color-secondary); }
|
||||
.gallery-img-wrap img { width:100%; height:100%; object-fit:cover; cursor:pointer; transition:opacity 0.15s; }
|
||||
.gallery-img-wrap img:hover { opacity:0.88; }
|
||||
@@ -59,19 +59,72 @@
|
||||
/* Lightbox */
|
||||
.lb { display:none; position:fixed; inset:0; background:rgba(0,0,0,.9); z-index:300; align-items:center; justify-content:center; }
|
||||
.lb.open { display:flex; }
|
||||
.lb img { max-width:95vw; max-height:95vh; border-radius:8px; object-fit:contain; }
|
||||
.lb img { max-width:85vw; max-height:90vh; border-radius:8px; object-fit:contain; }
|
||||
.lb-close { position:absolute; top:1rem; right:1rem; background:none; border:none; color:#fff; font-size:1.5rem; cursor:pointer; }
|
||||
.lb-nav { position:absolute; top:50%; transform:translateY(-50%); background:rgba(255,255,255,.15); border:none; color:#fff; font-size:2.5rem; line-height:1; cursor:pointer; padding:0.3rem 0.8rem; border-radius:8px; transition:background 0.15s; user-select:none; }
|
||||
.lb-nav:hover { background:rgba(255,255,255,.3); }
|
||||
.lb-nav:disabled { opacity:0.2; cursor:default; }
|
||||
.lb-prev { left:1rem; }
|
||||
.lb-next { right:1rem; }
|
||||
|
||||
.owner-badge { display:inline-flex; align-items:center; gap:0.3rem; font-size:0.75rem; background:var(--color-secondary); border-radius:4px; padding:0.2rem 0.5rem; color:var(--color-muted); margin-top:0.3rem; }
|
||||
.owner-actions { display:flex; gap:0.5rem; flex-wrap:wrap; margin-top:0.75rem; }
|
||||
|
||||
/* Tabs */
|
||||
.tab-bar { display:flex; gap:0; border-bottom:2px solid var(--color-secondary); margin-bottom:1.25rem; flex-wrap:wrap; }
|
||||
.tab-btn { background:none; border:none; border-bottom:2px solid transparent; color:var(--color-muted); cursor:pointer; font-size:0.88rem; font-weight:600; padding:0.65rem 1rem; margin-bottom:-2px; border-radius:0; width:auto; margin-top:0; transition:color 0.15s, border-color 0.15s; white-space:nowrap; }
|
||||
.tab-btn:hover { color:var(--color-text); background:none; }
|
||||
.tab-btn.active { color:var(--color-text); border-bottom-color:var(--color-primary); }
|
||||
.tab-panel { display:none; }
|
||||
.tab-panel.active { display:block; }
|
||||
|
||||
/* Posteingang */
|
||||
.inbox-list { display:flex; flex-direction:column; gap:0.5rem; }
|
||||
.inbox-item { display:flex; align-items:center; gap:0.65rem; padding:0.65rem 0.75rem; border-radius:8px; background:var(--color-secondary); cursor:pointer; transition:opacity 0.15s; }
|
||||
.inbox-item:hover { opacity:0.85; }
|
||||
.inbox-avatar { width:36px; height:36px; border-radius:50%; background:var(--color-card); display:flex; align-items:center; justify-content:center; font-size:0.9rem; flex-shrink:0; overflow:hidden; }
|
||||
.inbox-avatar img { width:100%; height:100%; object-fit:cover; }
|
||||
.inbox-info { flex:1; min-width:0; }
|
||||
.inbox-name { font-weight:600; font-size:0.88rem; }
|
||||
.inbox-preview { font-size:0.78rem; color:var(--color-muted); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
||||
.inbox-unread { background:var(--color-primary); color:#fff; font-size:0.65rem; font-weight:700; border-radius:9999px; padding:0.1rem 0.35rem; flex-shrink:0; }
|
||||
|
||||
/* Posteingang Chat */
|
||||
.inbox-chat { display:none; flex-direction:column; gap:0; margin-top:0.75rem; border:1px solid var(--color-secondary); border-radius:10px; overflow:hidden; }
|
||||
.inbox-chat.open { display:flex; }
|
||||
.inbox-chat-header { display:flex; align-items:center; gap:0.5rem; padding:0.65rem 0.9rem; background:var(--color-secondary); font-weight:600; font-size:0.88rem; }
|
||||
.inbox-chat-back { background:none; border:none; color:var(--color-muted); cursor:pointer; font-size:1.1rem; padding:0; margin:0; width:auto; line-height:1; }
|
||||
.inbox-chat-close { background:none; border:none; color:var(--color-muted); cursor:pointer; font-size:1rem; padding:0.15rem 0.35rem; margin:0 0 0 auto; width:auto; line-height:1; border-radius:4px; transition:background 0.15s, color 0.15s; }
|
||||
.inbox-chat-close:hover { background:var(--color-card); color:var(--color-text); }
|
||||
.inbox-chat-messages { max-height:320px; overflow-y:auto; padding:0.75rem 1rem; display:flex; flex-direction:column; gap:0.4rem; }
|
||||
.inbox-bubble-wrap { display:flex; flex-direction:column; }
|
||||
.inbox-bubble-wrap.me { align-items:flex-end; }
|
||||
.inbox-bubble-wrap.them { align-items:flex-start; }
|
||||
.inbox-bubble { max-width:75%; padding:0.45rem 0.8rem; border-radius:12px; font-size:0.88rem; line-height:1.4; word-break:break-word; }
|
||||
.inbox-bubble-wrap.me .inbox-bubble { background:var(--color-primary); color:#fff; border-bottom-right-radius:4px; }
|
||||
.inbox-bubble-wrap.them .inbox-bubble { background:var(--color-secondary); color:var(--color-text); border-bottom-left-radius:4px; }
|
||||
.inbox-bubble-time { font-size:0.68rem; color:var(--color-muted); margin-top:0.1rem; padding:0 0.2rem; }
|
||||
.inbox-reply-area { display:flex; gap:0.5rem; padding:0.6rem 0.9rem; border-top:1px solid var(--color-secondary); align-items:center; }
|
||||
.inbox-reply-area input { flex:1; }
|
||||
.inbox-reply-btn { width:auto; margin-top:0; padding:0.5rem 1rem; flex-shrink:0; }
|
||||
.inbox-lock-hint { font-size:0.8rem; color:var(--color-muted); padding:0.5rem 0.9rem; border-top:1px solid var(--color-secondary); background:var(--color-secondary); }
|
||||
.inbox-reply-trigger { padding:0.6rem 0.9rem; border-top:1px solid var(--color-secondary); }
|
||||
.inbox-reply-trigger .btn { font-size:0.85rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="main">
|
||||
<a href="/community/locations.html" class="back-link">← Locations</a>
|
||||
<body class="app">
|
||||
|
||||
<div id="content">
|
||||
<p style="color:var(--color-muted);">Wird geladen…</p>
|
||||
<!-- ── Admin-Autocomplete (außerhalb .main, damit overflow-y:auto nicht clippt) ── -->
|
||||
<ul id="adminSearchList" style="position:fixed;display:none;background:var(--color-card);border:1px solid var(--color-secondary);border-radius:6px;z-index:400;list-style:none;margin:0;padding:0;max-height:200px;overflow-y:auto;box-shadow:0 4px 12px rgba(0,0,0,.15);"></ul>
|
||||
<ul id="ownerSearchList" style="position:fixed;display:none;background:var(--color-card);border:1px solid var(--color-secondary);border-radius:6px;z-index:400;list-style:none;margin:0;padding:0;max-height:200px;overflow-y:auto;box-shadow:0 4px 12px rgba(0,0,0,.15);"></ul>
|
||||
|
||||
<div class="main">
|
||||
<div class="content">
|
||||
<a href="/community/locations.html" class="back-link">← Locations</a>
|
||||
|
||||
<div id="content">
|
||||
<p style="color:var(--color-muted);">Wird geladen…</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -131,7 +184,7 @@
|
||||
<label>Titel *</label>
|
||||
<input type="text" id="eventTitle" maxlength="200">
|
||||
<label>Beschreibung <span style="color:var(--color-muted);font-size:0.8rem;">(max. 1000 Zeichen)</span></label>
|
||||
<textarea id="eventDesc" maxlength="1000" rows="4" style="resize:vertical;"></textarea>
|
||||
<textarea id="eventDesc" maxlength="1000" rows="4" style="resize:vertical;width:100%;box-sizing:border-box;padding:0.65rem 0.9rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:1rem;outline:none;font-family:inherit;transition:border-color 0.2s;" onfocus="this.style.borderColor='var(--color-primary)'" onblur="this.style.borderColor='var(--color-secondary)'"></textarea>
|
||||
<label>Datum & Uhrzeit *</label>
|
||||
<input type="datetime-local" id="eventStartAt">
|
||||
<div class="modal-footer">
|
||||
@@ -141,20 +194,45 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Hinweis-Modal ──────────────────────────────────────────────────────── -->
|
||||
<div class="modal-overlay" id="alertModal">
|
||||
<div class="modal" style="width:min(380px,95vw);">
|
||||
<p id="alertMessage" style="margin:0 0 1.25rem;font-size:0.95rem;"></p>
|
||||
<div class="modal-footer">
|
||||
<button class="btn" onclick="document.getElementById('alertModal').classList.remove('open')">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Bestätigungs-Modal ─────────────────────────────────────────────────── -->
|
||||
<div class="modal-overlay" id="confirmModal">
|
||||
<div class="modal" style="width:min(380px,95vw);">
|
||||
<h3 id="confirmTitle">Bestätigung</h3>
|
||||
<p id="confirmMessage" style="color:var(--color-muted);font-size:0.92rem;margin:0 0 0.25rem;"></p>
|
||||
<div class="modal-footer">
|
||||
<button class="btn" style="background:var(--color-secondary);color:var(--color-text);" onclick="closeConfirm()">Abbrechen</button>
|
||||
<button class="btn" id="confirmOkBtn" style="background:#c0392b;">Löschen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Galerie Lightbox ───────────────────────────────────────────────────── -->
|
||||
<div class="lb" id="lightbox" onclick="closeLightbox()">
|
||||
<button class="lb-close" onclick="closeLightbox()">✕</button>
|
||||
<img id="lbImg" src="" alt="">
|
||||
<button class="lb-nav lb-prev" id="lbPrev" onclick="event.stopPropagation();lbNav(-1)">‹</button>
|
||||
<img id="lbImg" src="" alt="" onclick="event.stopPropagation()">
|
||||
<button class="lb-nav lb-next" id="lbNext" onclick="event.stopPropagation();lbNav(1)">›</button>
|
||||
</div>
|
||||
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/icons.js"></script>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script>
|
||||
const params = new URLSearchParams(location.search);
|
||||
const locationId = params.get('id');
|
||||
let locDetail = null;
|
||||
let myUserId = null;
|
||||
let isOwner = false;
|
||||
let isAdmin = false;
|
||||
let isFollowing = false;
|
||||
|
||||
// ── Bild-Resize ───────────────────────────────────────────────────────────────
|
||||
@@ -192,6 +270,18 @@ function formatDate(dt) {
|
||||
+ ', ' + d.toLocaleTimeString('de-DE', { hour:'2-digit', minute:'2-digit' }) + ' Uhr';
|
||||
}
|
||||
|
||||
// ── Tabs ──────────────────────────────────────────────────────────────────────
|
||||
const VALID_TABS = ['grunddaten', 'admins', 'posteingang', 'veranstaltungen'];
|
||||
|
||||
function switchTab(name) {
|
||||
if (!VALID_TABS.includes(name)) name = 'grunddaten';
|
||||
document.querySelectorAll('.tab-btn').forEach(btn =>
|
||||
btn.classList.toggle('active', btn.dataset.tab === name));
|
||||
document.querySelectorAll('.tab-panel').forEach(panel =>
|
||||
panel.classList.toggle('active', panel.id === 'tab-' + name));
|
||||
history.replaceState(null, '', location.pathname + location.search + '#' + name);
|
||||
}
|
||||
|
||||
// ── Lade Seite ────────────────────────────────────────────────────────────────
|
||||
async function loadPage() {
|
||||
if (!locationId) { document.getElementById('content').innerHTML = '<p>Keine Location-ID angegeben.</p>'; return; }
|
||||
@@ -207,12 +297,24 @@ async function loadPage() {
|
||||
myUserId = me.userId;
|
||||
}
|
||||
|
||||
locDetail = await locRes.json();
|
||||
isOwner = locDetail.ownerId === myUserId;
|
||||
locDetail = await locRes.json();
|
||||
isOwner = locDetail.ownerId === myUserId;
|
||||
isAdmin = isOwner || !!locDetail.isAdmin;
|
||||
isFollowing = !!locDetail.following;
|
||||
|
||||
renderPage();
|
||||
loadEvents();
|
||||
|
||||
if (isAdmin) {
|
||||
const chatWithId = new URLSearchParams(location.search).get('chatWith');
|
||||
const hash = location.hash.replace('#', '');
|
||||
const initialTab = chatWithId ? 'posteingang' : (VALID_TABS.includes(hash) ? hash : 'grunddaten');
|
||||
switchTab(initialTab);
|
||||
renderAdminList();
|
||||
loadInbox();
|
||||
loadEvents();
|
||||
} else {
|
||||
loadEvents();
|
||||
}
|
||||
}
|
||||
|
||||
function renderPage() {
|
||||
@@ -230,6 +332,7 @@ function renderPage() {
|
||||
<button class="btn" id="followBtn" style="font-size:0.85rem;${isFollowing ? 'background:var(--color-primary);color:#fff;' : 'background:var(--color-secondary);color:var(--color-text);'}" onclick="toggleFollow()">
|
||||
${isFollowing ? '★ Abonniert' : '☆ Abonnieren'}
|
||||
</button>
|
||||
${loc.virtualUserId && myUserId && !isAdmin ? `<button class="btn" style="font-size:0.85rem;" onclick="contactLocation('${loc.virtualUserId}')">✉ Kontaktieren</button>` : ''}
|
||||
</div>`;
|
||||
|
||||
let hoursHtml = '';
|
||||
@@ -248,7 +351,7 @@ function renderPage() {
|
||||
|
||||
const galleryHtml = buildGalleryHtml(loc.gallery || []);
|
||||
|
||||
document.getElementById('content').innerHTML = `
|
||||
const locHeaderHtml = `
|
||||
<div class="loc-header">
|
||||
<div class="loc-avatar">${imgHtml}</div>
|
||||
<div class="loc-meta">
|
||||
@@ -257,10 +360,9 @@ function renderPage() {
|
||||
${loc.description ? `<div class="loc-desc">${escHtml(loc.description)}</div>` : ''}
|
||||
${ownerActions}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${hoursHtml}
|
||||
</div>`;
|
||||
|
||||
const gallerySection = `
|
||||
<div class="section-title">
|
||||
Galerie
|
||||
${isOwner ? `<label class="btn" style="font-size:0.8rem;cursor:pointer;">
|
||||
@@ -268,14 +370,87 @@ function renderPage() {
|
||||
<input type="file" accept="image/*" style="display:none;" onchange="uploadGalleryImage(this)">
|
||||
</label>` : ''}
|
||||
</div>
|
||||
<div class="gallery-grid" id="galleryGrid">${galleryHtml}</div>
|
||||
<div class="gallery-grid" id="galleryGrid">${galleryHtml}</div>`;
|
||||
|
||||
const eventsSection = `
|
||||
<div class="section-title">
|
||||
Veranstaltungen
|
||||
${isOwner ? `<button class="btn" style="font-size:0.8rem;" onclick="openEventModal()">+ Veranstaltung erstellen</button>` : ''}
|
||||
</div>
|
||||
<div class="event-list" id="eventList"><p style="color:var(--color-muted);font-size:0.9rem;">Wird geladen…</p></div>
|
||||
`;
|
||||
<div id="pastEventsSection" style="display:none;">
|
||||
<div class="section-title" style="margin-top:1.5rem;">Vergangene Veranstaltungen</div>
|
||||
<div class="event-list" id="pastEventList"></div>
|
||||
</div>`;
|
||||
|
||||
if (isAdmin) {
|
||||
document.getElementById('content').innerHTML = `
|
||||
<div class="tab-bar">
|
||||
<button class="tab-btn" data-tab="grunddaten" onclick="switchTab('grunddaten')">Grunddaten</button>
|
||||
<button class="tab-btn" data-tab="admins" onclick="switchTab('admins')">Administrator*Innen</button>
|
||||
<button class="tab-btn" data-tab="posteingang" onclick="switchTab('posteingang')">Posteingang</button>
|
||||
<button class="tab-btn" data-tab="veranstaltungen" onclick="switchTab('veranstaltungen')">Veranstaltungen</button>
|
||||
</div>
|
||||
|
||||
<div class="tab-panel" id="tab-grunddaten">
|
||||
${locHeaderHtml}
|
||||
${hoursHtml}
|
||||
${gallerySection}
|
||||
</div>
|
||||
|
||||
<div class="tab-panel" id="tab-admins">
|
||||
<div id="adminList" style="display:flex;flex-direction:column;gap:0.5rem;margin-bottom:0.75rem;"></div>
|
||||
<div style="display:flex;gap:0.5rem;align-items:center;flex-wrap:wrap;">
|
||||
<div style="flex:1;min-width:180px;">
|
||||
<input type="text" id="adminSearchInput" placeholder="Mitglied suchen…" autocomplete="off"
|
||||
oninput="onAdminSearch()" style="width:100%;box-sizing:border-box;">
|
||||
</div>
|
||||
<button class="btn" style="font-size:0.85rem;white-space:nowrap;" onclick="addAdminFromSearch()">+ Admin hinzufügen</button>
|
||||
</div>
|
||||
${isOwner ? `
|
||||
<div style="margin-top:1rem;padding-top:1rem;border-top:1px solid var(--color-secondary);">
|
||||
<div style="font-size:0.82rem;color:var(--color-muted);margin-bottom:0.5rem;">Inhaberrechte übertragen</div>
|
||||
<div style="display:flex;gap:0.5rem;align-items:center;flex-wrap:wrap;">
|
||||
<div style="flex:1;min-width:180px;">
|
||||
<input type="text" id="ownerSearchInput" placeholder="Mitglied suchen…" autocomplete="off"
|
||||
oninput="onOwnerSearch()" style="width:100%;box-sizing:border-box;">
|
||||
</div>
|
||||
<button class="btn" style="font-size:0.85rem;white-space:nowrap;background:#c0392b;" onclick="transferOwner()">Inhaberwechsel</button>
|
||||
</div>
|
||||
</div>` : ''}
|
||||
</div>
|
||||
|
||||
<div class="tab-panel" id="tab-posteingang">
|
||||
<div id="inboxList" class="inbox-list"><p style="color:var(--color-muted);font-size:0.9rem;">Wird geladen…</p></div>
|
||||
<div class="inbox-chat" id="inboxChat">
|
||||
<div class="inbox-chat-header">
|
||||
<button class="inbox-chat-back" onclick="closeInboxChat()" aria-label="Zurück">‹</button>
|
||||
<span id="inboxChatName" style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;"></span>
|
||||
<button class="inbox-chat-close" onclick="closeInboxChat()" aria-label="Schließen">✕</button>
|
||||
</div>
|
||||
<div class="inbox-chat-messages" id="inboxChatMessages"></div>
|
||||
<div class="inbox-lock-hint" id="inboxLockHint" style="display:none;"></div>
|
||||
<div class="inbox-reply-trigger" id="inboxReplyTrigger" style="display:none;">
|
||||
<button class="btn" onclick="startReplying()">✎ Antworten</button>
|
||||
</div>
|
||||
<div class="inbox-reply-area" id="inboxReplyArea" style="display:none;">
|
||||
<input type="text" id="inboxReplyInput" placeholder="Antwort eingeben…" autocomplete="off"
|
||||
onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();sendInboxReply();}">
|
||||
<button class="btn inbox-reply-btn" onclick="sendInboxReply()">↑</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-panel" id="tab-veranstaltungen">
|
||||
${eventsSection}
|
||||
</div>`;
|
||||
} else {
|
||||
document.getElementById('content').innerHTML = `
|
||||
${locHeaderHtml}
|
||||
${hoursHtml}
|
||||
${gallerySection}
|
||||
${eventsSection}`;
|
||||
}
|
||||
}
|
||||
|
||||
function buildGalleryHtml(gallery) {
|
||||
@@ -298,7 +473,7 @@ async function uploadGalleryImage(input) {
|
||||
headers: {'Content-Type':'application/json'},
|
||||
body: JSON.stringify({ imageData })
|
||||
});
|
||||
if (res.status === 422) { alert('Maximal 20 Galeriebilder erlaubt.'); return; }
|
||||
if (res.status === 422) { showAlert('Maximal 20 Galeriebilder erlaubt.'); return; }
|
||||
if (!res.ok) throw new Error();
|
||||
const img = await res.json();
|
||||
const grid = document.getElementById('galleryGrid');
|
||||
@@ -307,18 +482,34 @@ async function uploadGalleryImage(input) {
|
||||
<img src="data:image/jpeg;base64,${img.imageData}" alt="Galeriebild" onclick="openLightbox(this.src)">
|
||||
<button class="gallery-del-btn" onclick="deleteGalleryImage('${img.imageId}')">✕</button>
|
||||
</div>`);
|
||||
} catch { alert('Fehler beim Hochladen.'); }
|
||||
} catch { showAlert('Fehler beim Hochladen.'); }
|
||||
input.value = '';
|
||||
}
|
||||
|
||||
async function deleteGalleryImage(imageId) {
|
||||
if (!confirm('Bild löschen?')) return;
|
||||
const res = await fetch(`/locations/${locationId}/gallery/${imageId}`, { method: 'DELETE' });
|
||||
if (!res.ok) { alert('Fehler beim Löschen.'); return; }
|
||||
if (!res.ok) { showAlert('Fehler beim Löschen.'); return; }
|
||||
await loadPage();
|
||||
}
|
||||
|
||||
// ── Events ─────────────────────────────────────────────────────────────────────
|
||||
function buildEventCard(e, isPast) {
|
||||
const imgHtml = e.imageData
|
||||
? `<img src="data:image/jpeg;base64,${e.imageData}" alt="${escHtml(e.title)}">`
|
||||
: '🗓';
|
||||
const opacity = isPast ? 'opacity:0.6;' : '';
|
||||
return `
|
||||
<a class="event-card" href="/community/event-detail.html?id=${e.eventId}" style="${opacity}">
|
||||
<div class="event-card-img">${imgHtml}</div>
|
||||
<div class="event-card-body">
|
||||
<div class="event-card-title">${escHtml(e.title)}</div>
|
||||
<div class="event-card-date">${formatDate(e.startAt)}</div>
|
||||
<div class="event-card-attendees">${e.attendeeCount} Teilnehmer*in(nen)${e.attendingMe ? ' · Du nimmst teil' : ''}</div>
|
||||
</div>
|
||||
</a>`;
|
||||
}
|
||||
|
||||
async function loadEvents() {
|
||||
const res = await fetch(`/locations/${locationId}/events`);
|
||||
if (!res.ok) return;
|
||||
@@ -326,37 +517,50 @@ async function loadEvents() {
|
||||
const list = document.getElementById('eventList');
|
||||
if (!list) return;
|
||||
|
||||
if (events.length === 0) {
|
||||
list.innerHTML = '<p style="color:var(--color-muted);font-size:0.9rem;">Noch keine Veranstaltungen.</p>';
|
||||
return;
|
||||
}
|
||||
const now = new Date();
|
||||
const future = events.filter(e => new Date(e.startAt) >= now);
|
||||
const past = events.filter(e => new Date(e.startAt) < now)
|
||||
.slice(-5) // letzte 5
|
||||
.reverse(); // neueste zuerst
|
||||
|
||||
list.innerHTML = events.map(e => {
|
||||
const imgHtml = e.imageData
|
||||
? `<img src="data:image/jpeg;base64,${e.imageData}" alt="${escHtml(e.title)}">`
|
||||
: '🗓';
|
||||
const deleteBtn = isOwner
|
||||
? `<button class="btn" style="font-size:0.75rem;margin-top:0.3rem;background:var(--color-secondary);color:var(--color-text);padding:0.2rem 0.5rem;" onclick="event.preventDefault();deleteEvent('${e.eventId}')">Löschen</button>`
|
||||
: '';
|
||||
return `
|
||||
<a class="event-card" href="/community/event-detail.html?id=${e.eventId}">
|
||||
<div class="event-card-img">${imgHtml}</div>
|
||||
<div class="event-card-body">
|
||||
<div class="event-card-title">${escHtml(e.title)}</div>
|
||||
<div class="event-card-date">${formatDate(e.startAt)}</div>
|
||||
<div class="event-card-attendees">${e.attendeeCount} Teilnehmer*in(nen)${e.attendingMe ? ' · Du nimmst teil' : ''}</div>
|
||||
${deleteBtn}
|
||||
</div>
|
||||
</a>`;
|
||||
}).join('');
|
||||
list.innerHTML = future.length
|
||||
? future.map(e => buildEventCard(e, false)).join('')
|
||||
: '<p style="color:var(--color-muted);font-size:0.9rem;">Keine bevorstehenden Veranstaltungen.</p>';
|
||||
|
||||
const pastSection = document.getElementById('pastEventsSection');
|
||||
if (past.length && pastSection) {
|
||||
document.getElementById('pastEventList').innerHTML = past.map(e => buildEventCard(e, true)).join('');
|
||||
pastSection.style.display = '';
|
||||
} else if (pastSection) {
|
||||
pastSection.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// ── Lightbox ───────────────────────────────────────────────────────────────────
|
||||
let lbSrcs = [], lbIdx = 0;
|
||||
|
||||
function openLightbox(src) {
|
||||
document.getElementById('lbImg').src = src;
|
||||
lbSrcs = Array.from(document.querySelectorAll('#galleryGrid .gallery-img-wrap img')).map(i => i.src);
|
||||
lbIdx = Math.max(0, lbSrcs.indexOf(src));
|
||||
lbShow();
|
||||
document.getElementById('lightbox').classList.add('open');
|
||||
}
|
||||
function lbShow() {
|
||||
document.getElementById('lbImg').src = lbSrcs[lbIdx];
|
||||
document.getElementById('lbPrev').disabled = lbIdx === 0;
|
||||
document.getElementById('lbNext').disabled = lbIdx === lbSrcs.length - 1;
|
||||
}
|
||||
function lbNav(dir) {
|
||||
lbIdx = Math.max(0, Math.min(lbSrcs.length - 1, lbIdx + dir));
|
||||
lbShow();
|
||||
}
|
||||
function closeLightbox() { document.getElementById('lightbox').classList.remove('open'); }
|
||||
document.addEventListener('keydown', e => {
|
||||
if (!document.getElementById('lightbox').classList.contains('open')) return;
|
||||
if (e.key === 'Escape') closeLightbox();
|
||||
else if (e.key === 'ArrowLeft') lbNav(-1);
|
||||
else if (e.key === 'ArrowRight') lbNav(1);
|
||||
});
|
||||
|
||||
// ── Edit Modal ─────────────────────────────────────────────────────────────────
|
||||
let _editLq = null, _editHq = null, _editLat = null, _editLon = null, _editStreet = null, _editCity = null, _editCityTimer = null;
|
||||
@@ -489,7 +693,7 @@ document.addEventListener('click', e => {
|
||||
|
||||
async function submitEdit() {
|
||||
const name = document.getElementById('editName').value.trim();
|
||||
if (!name) { alert('Name darf nicht leer sein.'); return; }
|
||||
if (!name) { showAlert('Name darf nicht leer sein.'); return; }
|
||||
const addrVal = document.getElementById('editCity').value.trim();
|
||||
if (addrVal && _editLat == null) {
|
||||
document.getElementById('editLocMsg').textContent = 'Bitte eine Adresse aus der Vorschlagsliste auswählen oder per GPS ermitteln.';
|
||||
@@ -530,7 +734,7 @@ async function submitEdit() {
|
||||
closeEditModal();
|
||||
renderPage();
|
||||
loadEvents();
|
||||
} catch { alert('Fehler beim Speichern.'); }
|
||||
} catch { showAlert('Fehler beim Speichern.'); }
|
||||
finally { btn.disabled = false; btn.textContent = 'Speichern'; }
|
||||
}
|
||||
|
||||
@@ -547,14 +751,14 @@ async function toggleFollow() {
|
||||
btn.style.background = isFollowing ? 'var(--color-primary)' : 'var(--color-secondary)';
|
||||
btn.style.color = isFollowing ? '#fff' : 'var(--color-text)';
|
||||
}
|
||||
} catch (_) { alert('Fehler beim Aktualisieren des Abonnements.'); }
|
||||
} catch (_) { showAlert('Fehler beim Aktualisieren des Abonnements.'); }
|
||||
finally { if (btn) btn.disabled = false; }
|
||||
}
|
||||
|
||||
async function deleteLocation() {
|
||||
if (!confirm('Location wirklich löschen? Alle Veranstaltungen und Galeriebilder werden ebenfalls gelöscht.')) return;
|
||||
const res = await fetch(`/locations/${locationId}`, { method: 'DELETE' });
|
||||
if (!res.ok) { alert('Fehler beim Löschen.'); return; }
|
||||
if (!res.ok) { showAlert('Fehler beim Löschen.'); return; }
|
||||
window.location.href = '/community/locations.html';
|
||||
}
|
||||
|
||||
@@ -568,6 +772,9 @@ function openEventModal(evtId) {
|
||||
document.getElementById('eventTitle').value = '';
|
||||
document.getElementById('eventDesc').value = '';
|
||||
document.getElementById('eventStartAt').value = '';
|
||||
// Nur Termine in der Zukunft erlauben
|
||||
const nowLocal = new Date(Date.now() - new Date().getTimezoneOffset() * 60000).toISOString().slice(0, 16);
|
||||
document.getElementById('eventStartAt').min = nowLocal;
|
||||
document.getElementById('eventPicPreview').innerHTML = '🗓';
|
||||
document.getElementById('eventModal').classList.add('open');
|
||||
}
|
||||
@@ -582,8 +789,9 @@ async function onEventPicChange(input) {
|
||||
async function submitEvent() {
|
||||
const title = document.getElementById('eventTitle').value.trim();
|
||||
const startAt = document.getElementById('eventStartAt').value;
|
||||
if (!title) { alert('Bitte gib einen Titel ein.'); return; }
|
||||
if (!startAt) { alert('Bitte wähle Datum und Uhrzeit.'); return; }
|
||||
if (!title) { showAlert('Bitte gib einen Titel ein.'); return; }
|
||||
if (!startAt) { showAlert('Bitte wähle Datum und Uhrzeit.'); return; }
|
||||
if (new Date(startAt) <= new Date()) { showAlert('Der Termin muss in der Zukunft liegen.'); return; }
|
||||
|
||||
const btn = document.getElementById('eventSubmitBtn');
|
||||
btn.disabled = true; btn.textContent = 'Wird gespeichert…';
|
||||
@@ -610,15 +818,321 @@ async function submitEvent() {
|
||||
|
||||
closeEventModal();
|
||||
loadEvents();
|
||||
} catch { alert('Fehler beim Speichern.'); }
|
||||
} catch { showAlert('Fehler beim Speichern.'); }
|
||||
finally { btn.disabled = false; btn.textContent = 'Speichern'; }
|
||||
}
|
||||
|
||||
async function deleteEvent(eventId) {
|
||||
if (!confirm('Veranstaltung löschen?')) return;
|
||||
const res = await fetch(`/locations/${locationId}/events/${eventId}`, { method: 'DELETE' });
|
||||
if (!res.ok) { alert('Fehler beim Löschen.'); return; }
|
||||
loadEvents();
|
||||
function showAlert(message) {
|
||||
document.getElementById('alertMessage').textContent = message;
|
||||
document.getElementById('alertModal').classList.add('open');
|
||||
}
|
||||
|
||||
function openConfirm(title, message, onOk) {
|
||||
document.getElementById('confirmTitle').textContent = title;
|
||||
document.getElementById('confirmMessage').textContent = message;
|
||||
const btn = document.getElementById('confirmOkBtn');
|
||||
btn.onclick = () => { closeConfirm(); onOk(); };
|
||||
document.getElementById('confirmModal').classList.add('open');
|
||||
}
|
||||
function closeConfirm() { document.getElementById('confirmModal').classList.remove('open'); }
|
||||
|
||||
// ── Admin-Verwaltung ──────────────────────────────────────────────────────────
|
||||
let _adminSearchSelected = null;
|
||||
let _ownerSearchSelected = null;
|
||||
let _adminSearchTimer = null;
|
||||
let _ownerSearchTimer = null;
|
||||
|
||||
function renderAdminList() {
|
||||
const list = document.getElementById('adminList');
|
||||
if (!list || !locDetail.admins) return;
|
||||
list.innerHTML = locDetail.admins.map(a => {
|
||||
const pic = a.profilePicture
|
||||
? `<img src="data:image/jpeg;base64,${a.profilePicture}" alt="" style="width:100%;height:100%;object-fit:cover;border-radius:50%;">`
|
||||
: '◉';
|
||||
const badge = a.isOwner
|
||||
? `<span style="font-size:0.7rem;background:var(--color-primary);color:#fff;border-radius:4px;padding:0.1rem 0.4rem;margin-left:0.4rem;">Inhaber</span>`
|
||||
: '';
|
||||
const removeBtn = isAdmin && !a.isOwner
|
||||
? `<button onclick="removeAdmin('${a.userId}')" style="margin-left:auto;background:none;border:none;color:var(--color-muted);cursor:pointer;font-size:1rem;padding:0.2rem 0.4rem;" title="Entfernen">✕</button>`
|
||||
: '';
|
||||
return `<div style="display:flex;align-items:center;gap:0.6rem;padding:0.4rem 0;">
|
||||
<div style="width:32px;height:32px;border-radius:50%;background:var(--color-secondary);display:flex;align-items:center;justify-content:center;font-size:1rem;overflow:hidden;flex-shrink:0;">${pic}</div>
|
||||
<span style="font-size:0.9rem;">${escHtml(a.name)}</span>${badge}${removeBtn}
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
async function removeAdmin(userId) {
|
||||
const res = await fetch(`/locations/${locationId}/admins/${userId}`, { method: 'DELETE' });
|
||||
if (!res.ok) { showAlert('Fehler beim Entfernen.'); return; }
|
||||
locDetail.admins = locDetail.admins.filter(a => a.userId !== userId);
|
||||
renderAdminList();
|
||||
}
|
||||
|
||||
function onAdminSearch() {
|
||||
const q = document.getElementById('adminSearchInput').value.trim();
|
||||
_adminSearchSelected = null;
|
||||
clearTimeout(_adminSearchTimer);
|
||||
if (q.length < 2) { document.getElementById('adminSearchList').style.display = 'none'; return; }
|
||||
_adminSearchTimer = setTimeout(() => fetchUserSuggestions(q, 'adminSearchList', sel => { _adminSearchSelected = sel; }), 250);
|
||||
}
|
||||
|
||||
function onOwnerSearch() {
|
||||
const q = document.getElementById('ownerSearchInput').value.trim();
|
||||
_ownerSearchSelected = null;
|
||||
clearTimeout(_ownerSearchTimer);
|
||||
if (q.length < 2) { document.getElementById('ownerSearchList').style.display = 'none'; return; }
|
||||
_ownerSearchTimer = setTimeout(() => fetchUserSuggestions(q, 'ownerSearchList', sel => { _ownerSearchSelected = sel; }), 250);
|
||||
}
|
||||
|
||||
const _userCache = {};
|
||||
|
||||
async function fetchUserSuggestions(q, listId, onSelect) {
|
||||
try {
|
||||
const res = await fetch(`/social/users/search?q=${encodeURIComponent(q)}`);
|
||||
if (!res.ok) return;
|
||||
const users = await res.json();
|
||||
const ul = document.getElementById(listId);
|
||||
if (!users.length) { ul.style.display = 'none'; return; }
|
||||
users.forEach(u => { _userCache[u.userId] = u; });
|
||||
ul.innerHTML = users.map(u => `
|
||||
<li style="padding:0.45rem 0.75rem;cursor:pointer;font-size:0.9rem;border-bottom:1px solid var(--color-secondary);"
|
||||
onmousedown="event.preventDefault();selectUserSuggestion('${listId}','${u.userId}')">
|
||||
${escHtml(u.name)}
|
||||
</li>`).join('');
|
||||
ul.__selectFn = onSelect;
|
||||
// Dropdown unter dem zugehörigen Input positionieren
|
||||
const inputId = listId === 'adminSearchList' ? 'adminSearchInput' : 'ownerSearchInput';
|
||||
const rect = document.getElementById(inputId).getBoundingClientRect();
|
||||
ul.style.top = (rect.bottom + 2) + 'px';
|
||||
ul.style.left = rect.left + 'px';
|
||||
ul.style.width = rect.width + 'px';
|
||||
ul.style.display = '';
|
||||
} catch(_) {}
|
||||
}
|
||||
|
||||
function selectUserSuggestion(listId, userId) {
|
||||
const ul = document.getElementById(listId);
|
||||
const inputId = listId === 'adminSearchList' ? 'adminSearchInput' : 'ownerSearchInput';
|
||||
const user = _userCache[userId];
|
||||
if (!user) return;
|
||||
document.getElementById(inputId).value = user.name;
|
||||
ul.style.display = 'none';
|
||||
if (ul.__selectFn) ul.__selectFn(user);
|
||||
}
|
||||
|
||||
document.addEventListener('click', e => {
|
||||
['adminSearchList','ownerSearchList'].forEach(id => {
|
||||
const ul = document.getElementById(id);
|
||||
if (ul && !e.target.closest('#' + id) && e.target.id !== (id === 'adminSearchList' ? 'adminSearchInput' : 'ownerSearchInput'))
|
||||
ul.style.display = 'none';
|
||||
});
|
||||
});
|
||||
|
||||
async function addAdminFromSearch() {
|
||||
if (!_adminSearchSelected) { showAlert('Bitte wähle ein Mitglied aus der Liste.'); return; }
|
||||
const res = await fetch(`/locations/${locationId}/admins`, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type':'application/json'},
|
||||
body: JSON.stringify({ userId: _adminSearchSelected.userId })
|
||||
});
|
||||
if (res.status === 409) { showAlert('Diese Person ist bereits Admin.'); return; }
|
||||
if (!res.ok) { showAlert('Fehler beim Hinzufügen.'); return; }
|
||||
const added = await res.json();
|
||||
locDetail.admins = [...(locDetail.admins || []), added];
|
||||
renderAdminList();
|
||||
document.getElementById('adminSearchInput').value = '';
|
||||
_adminSearchSelected = null;
|
||||
}
|
||||
|
||||
async function transferOwner() {
|
||||
if (!_ownerSearchSelected) { showAlert('Bitte wähle ein Mitglied aus der Liste.'); return; }
|
||||
openConfirm(
|
||||
'Inhaberwechsel',
|
||||
`Inhaberrechte wirklich an „${_ownerSearchSelected.name}" übertragen? Diese Aktion kann nicht rückgängig gemacht werden.`,
|
||||
async () => {
|
||||
const res = await fetch(`/locations/${locationId}/admins/transfer-owner`, {
|
||||
method: 'PUT',
|
||||
headers: {'Content-Type':'application/json'},
|
||||
body: JSON.stringify({ userId: _ownerSearchSelected.userId })
|
||||
});
|
||||
if (!res.ok) { showAlert('Fehler beim Inhaberwechsel.'); return; }
|
||||
// Seite neu laden – aktueller User ist jetzt kein Inhaber mehr
|
||||
await loadPage();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// ── Kontaktieren ──────────────────────────────────────────────────────────────
|
||||
function contactLocation(virtualUserId) {
|
||||
window.location.href = '/community/nachrichten.html?partnerId=' + virtualUserId;
|
||||
}
|
||||
|
||||
// ── Posteingang (Admin) ───────────────────────────────────────────────────────
|
||||
let _inboxPartnerId = null;
|
||||
|
||||
async function loadInbox() {
|
||||
const el = document.getElementById('inboxList');
|
||||
if (!el) return;
|
||||
try {
|
||||
const res = await fetch(`/locations/${locationId}/inbox`);
|
||||
if (!res.ok) { el.innerHTML = '<p style="color:var(--color-muted);font-size:0.9rem;">Nicht verfügbar.</p>'; return; }
|
||||
const items = await res.json();
|
||||
if (items.length === 0) {
|
||||
el.innerHTML = '<p style="color:var(--color-muted);font-size:0.9rem;">Noch keine Nachrichten.</p>';
|
||||
return;
|
||||
}
|
||||
el.innerHTML = items.map(item => {
|
||||
const av = item.senderPicture
|
||||
? `<img src="data:image/png;base64,${item.senderPicture}" alt="">`
|
||||
: '◉';
|
||||
const unreadHtml = item.unreadCount > 0
|
||||
? `<span class="inbox-unread">${item.unreadCount}</span>` : '';
|
||||
return `<div class="inbox-item" onclick="openInboxChat('${item.senderId}','${escHtml(item.senderName)}')">
|
||||
<div class="inbox-avatar">${av}</div>
|
||||
<div class="inbox-info">
|
||||
<div class="inbox-name">${escHtml(item.senderName)}</div>
|
||||
<div class="inbox-preview">${escHtml(item.lastMessage)}</div>
|
||||
</div>
|
||||
${unreadHtml}
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
// URL-Parameter: Chat direkt öffnen (z.B. nach Klick auf Benachrichtigung)
|
||||
const chatWithId = new URLSearchParams(location.search).get('chatWith');
|
||||
if (chatWithId && !_inboxPartnerId) {
|
||||
const match = items.find(i => i.senderId === chatWithId);
|
||||
if (match) openInboxChat(match.senderId, match.senderName);
|
||||
}
|
||||
} catch { el.innerHTML = '<p style="color:var(--color-muted);font-size:0.9rem;">Fehler beim Laden.</p>'; }
|
||||
}
|
||||
|
||||
async function openInboxChat(partnerId, partnerName) {
|
||||
_inboxPartnerId = partnerId;
|
||||
document.getElementById('inboxChatName').textContent = partnerName;
|
||||
document.getElementById('inboxChatMessages').innerHTML = '<p style="color:var(--color-muted);font-size:0.85rem;text-align:center;">Wird geladen…</p>';
|
||||
document.getElementById('inboxChat').classList.add('open');
|
||||
document.getElementById('inboxReplyInput').value = '';
|
||||
// Eingabebereich zurücksetzen
|
||||
document.getElementById('inboxReplyTrigger').style.display = 'none';
|
||||
document.getElementById('inboxReplyArea').style.display = 'none';
|
||||
document.getElementById('inboxLockHint').style.display = 'none';
|
||||
|
||||
const virtualId = locDetail.virtualUserId;
|
||||
try {
|
||||
const res = await fetch(`/locations/${locationId}/inbox/${partnerId}`);
|
||||
if (!res.ok) throw new Error();
|
||||
const data = await res.json();
|
||||
const messages = data.messages || [];
|
||||
|
||||
const container = document.getElementById('inboxChatMessages');
|
||||
container.innerHTML = '';
|
||||
if (messages.length === 0) {
|
||||
container.innerHTML = '<p style="color:var(--color-muted);font-size:0.85rem;text-align:center;">Noch keine Nachrichten.</p>';
|
||||
} else {
|
||||
messages.forEach(m => {
|
||||
const isLocationSender = m.senderId === virtualId;
|
||||
const wrap = document.createElement('div');
|
||||
wrap.className = 'inbox-bubble-wrap ' + (isLocationSender ? 'me' : 'them');
|
||||
const time = new Date(m.sentAt).toLocaleTimeString('de', { hour: '2-digit', minute: '2-digit' });
|
||||
wrap.innerHTML = `<div class="inbox-bubble">${escHtml(m.text)}</div><div class="inbox-bubble-time">${time}</div>`;
|
||||
container.appendChild(wrap);
|
||||
});
|
||||
}
|
||||
requestAnimationFrame(() => { container.scrollTop = container.scrollHeight; });
|
||||
|
||||
// Lock-Status anzeigen
|
||||
applyLockUi(data);
|
||||
loadInbox();
|
||||
} catch {
|
||||
document.getElementById('inboxChatMessages').innerHTML = '<p style="color:var(--color-muted);font-size:0.85rem;text-align:center;">Fehler beim Laden.</p>';
|
||||
}
|
||||
}
|
||||
|
||||
function applyLockUi(data) {
|
||||
const trigger = document.getElementById('inboxReplyTrigger');
|
||||
const area = document.getElementById('inboxReplyArea');
|
||||
const lockHint = document.getElementById('inboxLockHint');
|
||||
|
||||
if (data.lockedByMe) {
|
||||
// Ich habe die Sperre → Eingabefeld direkt zeigen
|
||||
trigger.style.display = 'none';
|
||||
area.style.display = '';
|
||||
lockHint.style.display = 'none';
|
||||
document.getElementById('inboxReplyInput').focus();
|
||||
} else if (data.canReply) {
|
||||
// Frei → "Antworten"-Button zeigen
|
||||
trigger.style.display = '';
|
||||
area.style.display = 'none';
|
||||
lockHint.style.display = 'none';
|
||||
} else {
|
||||
// Gesperrt durch anderen Admin
|
||||
trigger.style.display = 'none';
|
||||
area.style.display = 'none';
|
||||
lockHint.style.display = '';
|
||||
lockHint.textContent = `Wird gerade von ${data.lockedByName || 'einem anderen Admin'} beantwortet.`;
|
||||
}
|
||||
}
|
||||
|
||||
async function startReplying() {
|
||||
if (!_inboxPartnerId) return;
|
||||
try {
|
||||
const res = await fetch(`/locations/${locationId}/inbox/${_inboxPartnerId}/lock`, {
|
||||
method: 'POST'
|
||||
});
|
||||
if (res.status === 409) {
|
||||
const body = await res.json();
|
||||
const name = body.lockedByName || 'einem anderen Admin';
|
||||
document.getElementById('inboxReplyTrigger').style.display = 'none';
|
||||
document.getElementById('inboxLockHint').style.display = '';
|
||||
document.getElementById('inboxLockHint').textContent =
|
||||
`Wird gerade von ${name} beantwortet.`;
|
||||
return;
|
||||
}
|
||||
if (!res.ok) { showAlert('Sperre konnte nicht erworben werden.'); return; }
|
||||
document.getElementById('inboxReplyTrigger').style.display = 'none';
|
||||
document.getElementById('inboxReplyArea').style.display = '';
|
||||
document.getElementById('inboxReplyInput').focus();
|
||||
} catch { showAlert('Fehler beim Sperren.'); }
|
||||
}
|
||||
|
||||
function closeInboxChat() {
|
||||
_inboxPartnerId = null;
|
||||
document.getElementById('inboxChat').classList.remove('open');
|
||||
document.getElementById('inboxReplyTrigger').style.display = 'none';
|
||||
document.getElementById('inboxReplyArea').style.display = 'none';
|
||||
document.getElementById('inboxLockHint').style.display = 'none';
|
||||
loadInbox();
|
||||
}
|
||||
|
||||
async function sendInboxReply() {
|
||||
if (!_inboxPartnerId) return;
|
||||
const input = document.getElementById('inboxReplyInput');
|
||||
const text = input.value.trim();
|
||||
if (!text) return;
|
||||
input.value = '';
|
||||
try {
|
||||
const res = await fetch(`/locations/${locationId}/inbox/${_inboxPartnerId}/reply`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ text })
|
||||
});
|
||||
if (res.status === 409) {
|
||||
const body = await res.json().catch(() => ({}));
|
||||
const name = body.lockedByName || 'einem anderen Admin';
|
||||
input.value = text;
|
||||
// Eingabe sperren, Hinweis zeigen
|
||||
document.getElementById('inboxReplyArea').style.display = 'none';
|
||||
document.getElementById('inboxLockHint').style.display = '';
|
||||
document.getElementById('inboxLockHint').textContent =
|
||||
`Wird gerade von ${name} beantwortet. Deine Nachricht wurde nicht gesendet.`;
|
||||
return;
|
||||
}
|
||||
if (!res.ok) { showAlert('Fehler beim Senden.'); input.value = text; return; }
|
||||
// Konversation neu laden (Lock ist nach Senden bei mir)
|
||||
const partnerName = document.getElementById('inboxChatName').textContent;
|
||||
await openInboxChat(_inboxPartnerId, partnerName);
|
||||
} catch { showAlert('Fehler beim Senden.'); input.value = text; }
|
||||
}
|
||||
|
||||
// ── Init ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -121,6 +121,8 @@
|
||||
</svg>
|
||||
<span class="filter-badge" id="filterBadge" style="display:none;position:absolute;top:-3px;right:-3px;"></span>
|
||||
</button>
|
||||
<button id="createLocBtn" class="btn" onclick="openCreateModal()"
|
||||
style="display:none;padding:0.35rem 0.85rem;font-size:0.85rem;">+ Location anlegen</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -134,9 +136,6 @@
|
||||
|
||||
<!-- ── Meine Locations ──────────────────────────────────────────── -->
|
||||
<div id="paneMine" class="tab-panel">
|
||||
<div style="display:flex; justify-content:flex-end; margin-bottom:1rem;">
|
||||
<button class="btn" onclick="openCreateModal()">+ Location anlegen</button>
|
||||
</div>
|
||||
<div class="loc-grid" id="mineGrid"></div>
|
||||
<p class="empty-hint" id="mineEmpty" style="display:none;">Du hast noch keine Locations angelegt.</p>
|
||||
</div>
|
||||
@@ -228,6 +227,8 @@ function switchTab(name, btn) {
|
||||
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
document.getElementById('pane' + name.charAt(0).toUpperCase() + name.slice(1)).classList.add('active');
|
||||
document.getElementById('filterOpenBtn').style.display = name === 'search' ? '' : 'none';
|
||||
document.getElementById('createLocBtn').style.display = name === 'mine' ? '' : 'none';
|
||||
}
|
||||
|
||||
// ── Filter-Drawer ─────────────────────────────────────────────────────────────
|
||||
@@ -378,8 +379,8 @@ async function loadNextBatch() {
|
||||
const a = document.createElement('a');
|
||||
a.className = 'loc-card';
|
||||
a.href = `/community/location-detail.html?id=${p.locationId}`;
|
||||
const imgHtml = p.profilePictureLq
|
||||
? `<img src="data:image/jpeg;base64,${p.profilePictureLq}" alt="${escHtml(p.name)}">`
|
||||
const imgHtml = p.profilePictureHq
|
||||
? `<img src="data:image/jpeg;base64,${p.profilePictureHq}" alt="${escHtml(p.name)}">`
|
||||
: '<span>📍</span>';
|
||||
a.innerHTML = `
|
||||
<div class="loc-card-img">${imgHtml}</div>
|
||||
@@ -421,8 +422,8 @@ async function loadMine() {
|
||||
const a = document.createElement('a');
|
||||
a.className = 'loc-card';
|
||||
a.href = `/community/location-detail.html?id=${p.locationId}`;
|
||||
const imgHtml = p.profilePictureLq
|
||||
? `<img src="data:image/jpeg;base64,${p.profilePictureLq}" alt="${escHtml(p.name)}">`
|
||||
const imgHtml = p.profilePictureHq
|
||||
? `<img src="data:image/jpeg;base64,${p.profilePictureHq}" alt="${escHtml(p.name)}">`
|
||||
: '<span>📍</span>';
|
||||
a.innerHTML = `
|
||||
<div class="loc-card-img">${imgHtml}</div>
|
||||
|
||||
@@ -341,7 +341,8 @@
|
||||
if (!user) return;
|
||||
myId = user.userId;
|
||||
loadConversations();
|
||||
const urlPartnerId = new URLSearchParams(window.location.search).get('userId');
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const urlPartnerId = params.get('partnerId') || params.get('userId');
|
||||
if (urlPartnerId) openThread(urlPartnerId);
|
||||
})
|
||||
.catch(() => {});
|
||||
@@ -363,31 +364,36 @@
|
||||
return;
|
||||
}
|
||||
convs.forEach(c => {
|
||||
const isLocation = c.partner.friendStatus === 'LOCATION';
|
||||
const av = c.partner.profilePicture
|
||||
? `<img src="data:image/png;base64,${c.partner.profilePicture}" alt="" style="cursor:zoom-in;" onclick="event.stopPropagation();openLightbox(this.src)">`
|
||||
: '◉';
|
||||
: (isLocation ? '📍' : '◉');
|
||||
const unreadHtml = c.unreadCount > 0
|
||||
? `<span class="conv-unread">${c.unreadCount}</span>`
|
||||
: '';
|
||||
const preview = c.lastMessage
|
||||
? (c.lastMessage.text.startsWith('data:image/') ? '📷 Bild' : esc(c.lastMessage.text.substring(0, 40)))
|
||||
: '';
|
||||
const locationBadge = isLocation
|
||||
? `<span style="font-size:0.6rem;background:var(--color-secondary);color:var(--color-muted);border-radius:4px;padding:0.1rem 0.3rem;margin-left:0.3rem;">Location</span>`
|
||||
: '';
|
||||
const li = document.createElement('li');
|
||||
li.className = 'conv-item' + (c.partner.userId === activePartnerId ? ' active' : '');
|
||||
li.dataset.partnerId = c.partner.userId;
|
||||
li.dataset.isLocation = isLocation ? '1' : '';
|
||||
li.innerHTML = `
|
||||
<div class="conv-avatar">${av}</div>
|
||||
<div class="conv-info">
|
||||
<div class="conv-name">${esc(c.partner.name)}</div>
|
||||
<div class="conv-name">${esc(c.partner.name)}${locationBadge}</div>
|
||||
<div class="conv-preview">${preview}</div>
|
||||
</div>
|
||||
${unreadHtml}`;
|
||||
li.addEventListener('click', () => openThread(c.partner.userId, c.partner.name, c.partner.profilePicture));
|
||||
li.addEventListener('click', () => openThread(c.partner.userId, c.partner.name, c.partner.profilePicture, isLocation));
|
||||
list.appendChild(li);
|
||||
});
|
||||
}
|
||||
|
||||
async function openThread(partnerId, partnerName, partnerPic) {
|
||||
async function openThread(partnerId, partnerName, partnerPic, isLocation) {
|
||||
activePartnerId = partnerId;
|
||||
oldestSentAt = null;
|
||||
newestSentAt = null;
|
||||
@@ -398,17 +404,51 @@
|
||||
li.classList.toggle('active', li.dataset.partnerId === partnerId);
|
||||
});
|
||||
|
||||
// isLocation ggf. aus DOM ermitteln oder per API auflösen
|
||||
if (isLocation === undefined) {
|
||||
const convItem = document.querySelector(`.conv-item[data-partner-id="${partnerId}"]`);
|
||||
if (convItem) {
|
||||
isLocation = convItem.dataset.isLocation === '1';
|
||||
} else {
|
||||
// Noch kein Conv-Item (neue/unbekannte Konversation) → per API prüfen
|
||||
try {
|
||||
const locRes = await fetch('/locations/virtual/' + partnerId);
|
||||
if (locRes.ok) {
|
||||
isLocation = true;
|
||||
const loc = await locRes.json();
|
||||
if (!partnerName) partnerName = loc.name;
|
||||
if (!partnerPic && loc.profilePictureLq) partnerPic = loc.profilePictureLq;
|
||||
} else {
|
||||
isLocation = false;
|
||||
}
|
||||
} catch { isLocation = false; }
|
||||
}
|
||||
}
|
||||
|
||||
if (!partnerName) {
|
||||
const convItem = document.querySelector(`.conv-item[data-partner-id="${partnerId}"]`);
|
||||
partnerName = convItem ? convItem.querySelector('.conv-name').textContent : '…';
|
||||
// .conv-name enthält ggf. das Location-Badge – nur Textinhalt nehmen
|
||||
partnerName = convItem ? convItem.querySelector('.conv-name').firstChild.textContent.trim() : '…';
|
||||
}
|
||||
|
||||
const locationBadge = isLocation
|
||||
? ` <span style="font-size:0.65rem;background:var(--color-secondary);color:var(--color-muted);border-radius:4px;padding:0.1rem 0.35rem;vertical-align:middle;">Location</span>`
|
||||
: '';
|
||||
if (isLocation) {
|
||||
document.getElementById('threadPartnerName').innerHTML =
|
||||
`${esc(partnerName)}${locationBadge}`;
|
||||
} else {
|
||||
document.getElementById('threadPartnerName').innerHTML =
|
||||
`<a href="/community/benutzer.html?userId=${partnerId}" style="color:inherit;text-decoration:none;">${esc(partnerName)}</a>`;
|
||||
}
|
||||
document.getElementById('threadPartnerName').innerHTML =
|
||||
`<a href="/community/benutzer.html?userId=${partnerId}" style="color:inherit;text-decoration:none;">${esc(partnerName)}</a>`;
|
||||
|
||||
const avatarEl = document.getElementById('threadPartnerAvatar');
|
||||
if (partnerPic) {
|
||||
avatarEl.innerHTML = `<img src="data:image/png;base64,${partnerPic}" alt="" style="cursor:zoom-in;" onclick="openLightbox(this.src)">`;
|
||||
avatarEl.style.display = '';
|
||||
} else if (isLocation) {
|
||||
avatarEl.innerHTML = '📍';
|
||||
avatarEl.style.display = '';
|
||||
} else {
|
||||
avatarEl.style.display = 'none';
|
||||
}
|
||||
@@ -548,7 +588,12 @@
|
||||
input.value = text;
|
||||
return;
|
||||
}
|
||||
await pollNewMessages();
|
||||
// War die Konversation leer, neu laden; sonst nur neue Nachrichten pollen
|
||||
if (newestSentAt) {
|
||||
await pollNewMessages();
|
||||
} else {
|
||||
await loadInitialThread();
|
||||
}
|
||||
} catch (e) { console.error(e); }
|
||||
}
|
||||
|
||||
|
||||
@@ -516,6 +516,17 @@ body.app {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.sidebar-cat-label {
|
||||
font-size: 0.68rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.07em;
|
||||
color: var(--color-muted);
|
||||
padding: 0.9rem 1.1rem 0.25rem;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.sidebar-group-toggle {
|
||||
cursor: pointer;
|
||||
justify-content: space-between;
|
||||
|
||||
@@ -44,23 +44,41 @@
|
||||
];
|
||||
|
||||
|
||||
// ── Community-Links (immer sichtbar, oberhalb der Spiele) ──
|
||||
const socialLinks = [
|
||||
{ href: '/userhome.html', icon: I('HOME') || '⌂', label: 'Home', badgeId: null },
|
||||
{ href: '/community/feed.html', icon: I('FEED'), label: 'Feed', badgeId: null },
|
||||
{ href: '/community/freunde.html', icon: I('FRIENDS'), label: 'Freunde', badgeId: 'socialFriendsBadge'},
|
||||
{ href: '/community/nachrichten.html', icon: I('MESSAGES'), label: 'Nachrichten', badgeId: null },
|
||||
{ href: '/community/benachrichtigungen.html', icon: I('NOTIFICATIONS'), label: 'Benachrichtigungen', badgeId: null },
|
||||
{ href: '/community/gruppen.html', icon: I('GROUPS'), label: 'Gruppen', badgeId: 'socialGruppenBadge'},
|
||||
{ href: '/community/locations.html', icon: I('LOCATION') || '📍', label: 'Locations', badgeId: null },
|
||||
{ href: '/community/events.html', icon: I('EVENT') || '🗓', label: 'Veranstaltungen', badgeId: null },
|
||||
{ href: '/games/common/einladungen.html', icon: I('INVITATIONS'), label: 'Einladungen', badgeId: null },
|
||||
];
|
||||
const socialNav = socialLinks.map(({ href, icon, label, badgeId }) => {
|
||||
// ── Hilfsfunktion: einzelner Nav-Link ──
|
||||
function navLink({ href, icon, label, badgeId }) {
|
||||
const cls = path === href ? ' class="active"' : '';
|
||||
const badge = badgeId ? `<span class="social-badge" id="${badgeId}" style="display:none;"></span>` : '';
|
||||
return `<li><a href="${href}"${cls}><span class="icon">${icon}</span> ${label}${badge}</a></li>`;
|
||||
}).join('');
|
||||
}
|
||||
const sep = `<li><hr style="border:none;border-top:1px solid var(--color-secondary);margin:0.4rem 1rem;"></li>`;
|
||||
const catLabel = label => `<li class="sidebar-cat-label">${label}</li>`;
|
||||
|
||||
// Home
|
||||
const homeNav = navLink({ href: '/userhome.html', icon: I('HOME') || '⌂', label: 'Home' });
|
||||
|
||||
// Kommunikation
|
||||
const commLinks = [
|
||||
{ href: '/community/nachrichten.html', icon: I('MESSAGES'), label: 'Nachrichten' },
|
||||
{ href: '/community/benachrichtigungen.html', icon: I('NOTIFICATIONS'), label: 'Benachrichtigungen' },
|
||||
{ href: '/games/common/einladungen.html', icon: I('INVITATIONS'), label: 'Einladungen' },
|
||||
];
|
||||
|
||||
// Social
|
||||
const socialLinks = [
|
||||
{ href: '/community/feed.html', icon: I('FEED'), label: 'Feed' },
|
||||
{ href: '/community/freunde.html', icon: I('FRIENDS'), label: 'Freunde', badgeId: 'socialFriendsBadge' },
|
||||
{ href: '/community/gruppen.html', icon: I('GROUPS'), label: 'Gruppen', badgeId: 'socialGruppenBadge' },
|
||||
{ href: '/community/locations.html', icon: I('LOCATION') || '📍', label: 'Locations' },
|
||||
{ href: '/community/events.html', icon: I('EVENT') || '🗓', label: 'Veranstaltungen' },
|
||||
];
|
||||
|
||||
const socialNav = [
|
||||
homeNav,
|
||||
sep,
|
||||
...commLinks.map(navLink),
|
||||
sep,
|
||||
...socialLinks.map(navLink),
|
||||
].join('');
|
||||
|
||||
const datingActive = path === '/dating.html';
|
||||
const datingCls = datingActive ? ' class="active"' : '';
|
||||
@@ -106,7 +124,7 @@
|
||||
<div class="sidebar-scroll-area">
|
||||
<ul>
|
||||
${socialNav}
|
||||
<li><hr style="border:none;border-top:1px solid var(--color-secondary);margin:0.4rem 1rem;"></li>
|
||||
${sep}
|
||||
${datingItem}
|
||||
<li><hr style="border:none;border-top:1px solid var(--color-secondary);margin:0.4rem 1rem;"></li>
|
||||
${nav}
|
||||
|
||||
@@ -107,6 +107,29 @@
|
||||
.match-badge {
|
||||
font-size: 0.65rem; color: var(--color-primary); text-align: center; font-weight: 600;
|
||||
}
|
||||
|
||||
/* ── Location-Events ── */
|
||||
.loc-event-list { display: flex; flex-direction: column; gap: 0.6rem; }
|
||||
.loc-event-card {
|
||||
display: flex; gap: 0.75rem; align-items: center;
|
||||
background: var(--color-secondary); border: 1px solid var(--color-secondary);
|
||||
border-radius: 10px; padding: 0.65rem 0.85rem;
|
||||
text-decoration: none; color: inherit;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
.loc-event-card:hover { border-color: var(--color-primary); }
|
||||
.loc-event-thumb {
|
||||
width: 48px; height: 48px; border-radius: 8px; flex-shrink: 0;
|
||||
background: var(--color-card); overflow: hidden;
|
||||
display: flex; align-items: center; justify-content: center; font-size: 1.4rem;
|
||||
}
|
||||
.loc-event-thumb img { width: 100%; height: 100%; object-fit: cover; }
|
||||
.loc-event-body { flex: 1; min-width: 0; }
|
||||
.loc-event-location { font-size: 0.75rem; color: var(--color-muted); margin-bottom: 0.1rem;
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.loc-event-title { font-size: 0.92rem; font-weight: 600;
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.loc-event-date { font-size: 0.75rem; color: var(--color-primary); margin-top: 0.15rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="app">
|
||||
@@ -132,38 +155,19 @@
|
||||
<div class="section-label">Matches 💕</div>
|
||||
<div class="dating-strip" id="matchesStrip"></div>
|
||||
</div>
|
||||
|
||||
<!-- Meine angemeldeten Events -->
|
||||
<div id="myEventsSection" style="display:none;">
|
||||
<div class="section-label">Meine Veranstaltungen 🎟</div>
|
||||
<div class="loc-event-list" id="myEventsList"></div>
|
||||
</div>
|
||||
|
||||
<!-- Nächste Events abonnierter Locations -->
|
||||
<div id="locEventsSection" style="display:none;">
|
||||
<div class="section-label">Nächste Veranstaltungen 📍</div>
|
||||
<div class="loc-event-list" id="locEventsList"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="game-grid">
|
||||
<div class="game-card">
|
||||
<div class="game-card-icon">❤️</div>
|
||||
<h2 class="game-card-title">Vanilla Game</h2>
|
||||
<p class="game-card-desc">
|
||||
|
||||
</p>
|
||||
<a href="/games/vanilla/sessionvanilla.html"><button class="game-card-btn">Neue Session starten</button></a>
|
||||
</div>
|
||||
|
||||
<div class="game-card">
|
||||
<div class="game-card-icon">⛓️</div>
|
||||
<h2 class="game-card-title">BDSM Game</h2>
|
||||
<p class="game-card-desc">
|
||||
Tauche ein in strukturierte Sessions mit Aufgaben, Toys und klaren Rollen.
|
||||
Definiere Grenzen, vergib Aufgaben und erlebe intensive Momente mit deinen Spielpartner*Innen.
|
||||
</p>
|
||||
<a href="/games/bdsm/neubdsm.html"><button class="game-card-btn">Neue Session starten</button></a>
|
||||
</div>
|
||||
|
||||
<div class="game-card">
|
||||
<div class="game-card-icon">🔒</div>
|
||||
<h2 class="game-card-title">Chastity Game</h2>
|
||||
<p class="game-card-desc">
|
||||
Erlebe Keuschheit auf eine neue Art: Kartenbasierte Locks, Keyholder-System,
|
||||
Community-Abstimmungen und tägliche Verifizierungen machen jedes Lock einzigartig.
|
||||
</p>
|
||||
<a href="/games/chastity/neulock.html"><button class="game-card-btn">Neues Lock erstellen</button></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/icons.js"></script>
|
||||
@@ -178,6 +182,8 @@
|
||||
if (user) {
|
||||
document.getElementById('greeting').textContent = 'Willkommen zurück, ' + user.name + '!';
|
||||
loadVisitors();
|
||||
loadMyEvents();
|
||||
loadLocEvents();
|
||||
if (user.datingAktiv) {
|
||||
loadWhoLikesMe();
|
||||
loadMatches();
|
||||
@@ -257,6 +263,47 @@
|
||||
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
|
||||
function renderEventCards(events, listId, sectionId) {
|
||||
if (!events.length) return;
|
||||
const list = document.getElementById(listId);
|
||||
list.innerHTML = events.map(e => {
|
||||
const thumb = e.imageData
|
||||
? `<img src="data:image/jpeg;base64,${e.imageData}" alt="">`
|
||||
: '🗓';
|
||||
const date = new Date(e.startAt);
|
||||
const dateStr = date.toLocaleDateString('de-DE', { weekday:'short', day:'numeric', month:'short' })
|
||||
+ ', ' + date.toLocaleTimeString('de-DE', { hour:'2-digit', minute:'2-digit' }) + ' Uhr';
|
||||
return `
|
||||
<a class="loc-event-card" href="/community/event-detail.html?id=${e.eventId}">
|
||||
<div class="loc-event-thumb">${thumb}</div>
|
||||
<div class="loc-event-body">
|
||||
<div class="loc-event-location">${esc(e.locationName)}</div>
|
||||
<div class="loc-event-title">${esc(e.title)}</div>
|
||||
<div class="loc-event-date">${dateStr}</div>
|
||||
</div>
|
||||
</a>`;
|
||||
}).join('');
|
||||
document.getElementById(sectionId).style.display = '';
|
||||
}
|
||||
|
||||
async function loadMyEvents() {
|
||||
try {
|
||||
const res = await fetch('/location-events/attending-next');
|
||||
if (!res.ok) return;
|
||||
const events = await res.json();
|
||||
renderEventCards(events, 'myEventsList', 'myEventsSection');
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
async function loadLocEvents() {
|
||||
try {
|
||||
const res = await fetch('/location-events/followed-next');
|
||||
if (!res.ok) return;
|
||||
const events = await res.json();
|
||||
renderEventCards(events, 'locEventsList', 'locEventsSection');
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
async function loadVisitors() {
|
||||
try {
|
||||
const res = await fetch('/social/profile-visits/my-visitors');
|
||||
|
||||
Reference in New Issue
Block a user