Files
xxx-sphere-web/bin/main/static/community/location-detail.html
Mario 0f9f109067
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
An den Verantaltungen und Locations gearbeitet
2026-04-05 23:04:09 +02:00

629 lines
32 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Location xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<style>
.back-link { display:inline-flex; align-items:center; gap:0.35rem; color:var(--color-muted); font-size:0.88rem; text-decoration:none; margin-bottom:1rem; }
.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 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; }
.loc-city { color:var(--color-muted); font-size:0.88rem; margin-bottom:0.4rem; }
.loc-desc { font-size:0.93rem; line-height:1.55; white-space:pre-wrap; word-break:break-word; margin-top:0.5rem; }
.section-title { font-size:1rem; font-weight:700; margin:1.5rem 0 0.75rem; display:flex; align-items:center; justify-content:space-between; }
.hours-table { width:100%; border-collapse:collapse; font-size:0.88rem; }
.hours-table td { padding:0.3rem 0.5rem; border-bottom:1px solid var(--color-secondary); }
.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-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; }
.gallery-del-btn { position:absolute; top:4px; right:4px; background:rgba(0,0,0,.6); border:none; color:#fff; border-radius:50%; width:22px; height:22px; font-size:0.7rem; cursor:pointer; display:flex; align-items:center; justify-content:center; padding:0; margin:0; }
.gallery-upload-btn { aspect-ratio:1; border:2px dashed var(--color-secondary); border-radius:8px; display:flex; align-items:center; justify-content:center; font-size:1.5rem; color:var(--color-muted); cursor:pointer; transition:border-color 0.15s; background:none; }
.gallery-upload-btn:hover { border-color:var(--color-primary); color:var(--color-primary); }
.event-list { display:flex; flex-direction:column; gap:0.75rem; }
.event-card { background:var(--color-card); border:1px solid var(--color-secondary); border-radius:10px; display:flex; gap:0.75rem; padding:0.75rem; text-decoration:none; color:inherit; transition:border-color 0.15s; cursor:pointer; }
.event-card:hover { border-color:var(--color-primary); }
.event-card-img { width:64px; height:64px; border-radius:8px; object-fit:cover; background:var(--color-secondary); flex-shrink:0; overflow:hidden; display:flex; align-items:center; justify-content:center; font-size:1.4rem; }
.event-card-img img { width:100%; height:100%; object-fit:cover; }
.event-card-body { flex:1; min-width:0; }
.event-card-title { font-weight:600; font-size:0.92rem; margin:0 0 0.2rem; }
.event-card-date { font-size:0.78rem; color:var(--color-muted); }
.event-card-attendees { font-size:0.78rem; color:var(--color-muted); }
/* 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; }
.hours-grid { display:grid; grid-template-columns:auto 1fr 1fr auto; gap:0.4rem 0.5rem; align-items:center; font-size:0.85rem; margin-top:0.5rem; }
.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; }
.suggest-list { position:absolute; top:100%; left:0; right:0; background:var(--color-card); border:1px solid var(--color-secondary); border-top:none; border-radius:0 0 6px 6px; z-index:50; display:none; list-style:none; margin:0; padding:0; max-height:220px; overflow-y:auto; }
/* 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-close { position:absolute; top:1rem; right:1rem; background:none; border:none; color:#fff; font-size:1.5rem; cursor:pointer; }
.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; }
</style>
</head>
<body>
<div class="main">
<a href="/community/locations.html" class="back-link">← Locations</a>
<div id="content">
<p style="color:var(--color-muted);">Wird geladen…</p>
</div>
</div>
<!-- ── Edit-Modal ──────────────────────────────────────────────────────────── -->
<div class="modal-overlay" id="editModal">
<div class="modal">
<h3>Location 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;">
Profilbild ändern
<input type="file" id="editPicFile" accept="image/*" style="display:none;" onchange="onEditPicChange(this)">
</label>
</div>
</div>
<label>Name *</label>
<input type="text" id="editName" 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>Adresse</label>
<div id="editStadtRow">
<div style="position:relative;">
<input type="text" id="editCity" placeholder="Straße, Hausnummer, Stadt…" autocomplete="off"
style="width:100%;box-sizing:border-box;padding:0.55rem 2rem 0.55rem 0.8rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.95rem;outline:none;"
oninput="onEditCityInput()">
<button id="editCityClear" onclick="clearEditCity()" title="Auswahl aufheben"
style="display:none;position:absolute;right:0.4rem;top:50%;transform:translateY(-50%);background:none;border:none;color:var(--color-muted);font-size:1.1rem;cursor:pointer;padding:0.1rem 0.3rem;margin:0;width:auto;line-height:1;">×</button>
<ul id="editCitySuggestions" style="display:none;position:absolute;top:100%;left:0;right:0;
background:var(--color-card);border:1px solid var(--color-secondary);border-radius:6px;
z-index:100;list-style:none;margin:0.2rem 0 0;padding:0;max-height:200px;overflow-y:auto;"></ul>
</div>
<div id="editLocMsg" style="font-size:0.82rem;color:var(--color-muted);margin-top:0.25rem;min-height:1.1em;"></div>
</div>
<label style="margin-top:0.75rem;display:block;">Öffnungszeiten</label>
<div class="hours-grid" id="editHoursGrid"></div>
<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>
<!-- ── Event erstellen/bearbeiten Modal ───────────────────────────────────── -->
<div class="modal-overlay" id="eventModal">
<div class="modal">
<h3 id="eventModalTitle">Veranstaltung erstellen</h3>
<div class="img-row">
<div class="img-preview" id="eventPicPreview">🗓</div>
<div>
<label class="btn" style="display:inline-block;cursor:pointer;font-size:0.85rem;">
Bild wählen
<input type="file" id="eventPicFile" accept="image/*" style="display:none;" onchange="onEventPicChange(this)">
</label>
</div>
</div>
<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>
<label>Datum &amp; Uhrzeit *</label>
<input type="datetime-local" id="eventStartAt">
<div class="modal-footer">
<button class="btn" style="background:var(--color-secondary);color:var(--color-text);" onclick="closeEventModal()">Abbrechen</button>
<button class="btn" id="eventSubmitBtn" onclick="submitEvent()">Speichern</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="">
</div>
<script src="/js/sidebar.js"></script>
<script src="/js/icons.js"></script>
<script>
const params = new URLSearchParams(location.search);
const locationId = params.get('id');
let locDetail = null;
let myUserId = null;
let isOwner = false;
let isFollowing = false;
// ── Bild-Resize ───────────────────────────────────────────────────────────────
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 escHtml(s) {
return (s || '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
const DAY_NAMES = ['Montag','Dienstag','Mittwoch','Donnerstag','Freitag','Samstag','Sonntag'];
function formatDate(dt) {
if (!dt) return '';
const d = new Date(dt);
return d.toLocaleDateString('de-DE', { weekday:'short', day:'2-digit', month:'2-digit', year:'numeric' })
+ ', ' + d.toLocaleTimeString('de-DE', { hour:'2-digit', minute:'2-digit' }) + ' Uhr';
}
// ── Lade Seite ────────────────────────────────────────────────────────────────
async function loadPage() {
if (!locationId) { document.getElementById('content').innerHTML = '<p>Keine Location-ID angegeben.</p>'; return; }
const [meRes, locRes] = await Promise.all([
fetch('/login/me'),
fetch(`/locations/${locationId}`)
]);
if (!locRes.ok) { document.getElementById('content').innerHTML = '<p>Location nicht gefunden.</p>'; return; }
if (meRes.ok) {
const me = await meRes.json();
myUserId = me.userId;
}
locDetail = await locRes.json();
isOwner = locDetail.ownerId === myUserId;
isFollowing = !!locDetail.following;
renderPage();
loadEvents();
}
function renderPage() {
const loc = locDetail;
const imgHtml = loc.profilePictureHq || loc.profilePictureLq
? `<img src="data:image/jpeg;base64,${loc.profilePictureHq || loc.profilePictureLq}" alt="${escHtml(loc.name)}">`
: '📍';
const ownerActions = isOwner ? `
<div class="owner-actions">
<button class="btn" style="font-size:0.85rem;" onclick="openEditModal()">✎ Bearbeiten</button>
<button class="btn" style="background:var(--color-secondary);color:var(--color-text);font-size:0.85rem;" onclick="deleteLocation()">Löschen</button>
</div>` : `
<div class="owner-actions">
<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>
</div>`;
let hoursHtml = '';
if (loc.openingHours && loc.openingHours.length > 0) {
const rowsHtml = loc.openingHours.map(h => {
const dayName = DAY_NAMES[h.dayOfWeek - 1] || '';
const timeText = h.closed
? '<span class="hours-closed">Geschlossen</span>'
: `${h.openTime || '--:--'} ${h.closeTime || '--:--'}`;
return `<tr><td>${dayName}</td><td>${timeText}</td></tr>`;
}).join('');
hoursHtml = `
<div class="section-title">Öffnungszeiten</div>
<table class="hours-table"><tbody>${rowsHtml}</tbody></table>`;
}
const galleryHtml = buildGalleryHtml(loc.gallery || []);
document.getElementById('content').innerHTML = `
<div class="loc-header">
<div class="loc-avatar">${imgHtml}</div>
<div class="loc-meta">
<div class="loc-name">${escHtml(loc.name)}</div>
${(loc.street || loc.city) ? `<div class="loc-city">📍 ${escHtml([loc.street, loc.city].filter(Boolean).join(', '))}</div>` : ''}
${loc.description ? `<div class="loc-desc">${escHtml(loc.description)}</div>` : ''}
${ownerActions}
</div>
</div>
${hoursHtml}
<div class="section-title">
Galerie
${isOwner ? `<label class="btn" style="font-size:0.8rem;cursor:pointer;">
+ Bild hinzufügen
<input type="file" accept="image/*" style="display:none;" onchange="uploadGalleryImage(this)">
</label>` : ''}
</div>
<div class="gallery-grid" id="galleryGrid">${galleryHtml}</div>
<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>
`;
}
function buildGalleryHtml(gallery) {
return gallery.map(img => `
<div class="gallery-img-wrap">
<img src="data:image/jpeg;base64,${img.imageData}" alt="Galeriebild"
onclick="openLightbox(this.src)">
${isOwner ? `<button class="gallery-del-btn" onclick="deleteGalleryImage('${img.imageId}')">✕</button>` : ''}
</div>`).join('');
}
// ── Galerie ────────────────────────────────────────────────────────────────────
async function uploadGalleryImage(input) {
const file = input.files[0];
if (!file) return;
try {
const imageData = await resizeImage(file, 1024, 0.88);
const res = await fetch(`/locations/${locationId}/gallery`, {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({ imageData })
});
if (res.status === 422) { alert('Maximal 20 Galeriebilder erlaubt.'); return; }
if (!res.ok) throw new Error();
const img = await res.json();
const grid = document.getElementById('galleryGrid');
grid.insertAdjacentHTML('beforeend', `
<div class="gallery-img-wrap">
<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.'); }
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; }
await loadPage();
}
// ── Events ─────────────────────────────────────────────────────────────────────
async function loadEvents() {
const res = await fetch(`/locations/${locationId}/events`);
if (!res.ok) return;
const events = await res.json();
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;
}
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('');
}
// ── Lightbox ───────────────────────────────────────────────────────────────────
function openLightbox(src) {
document.getElementById('lbImg').src = src;
document.getElementById('lightbox').classList.add('open');
}
function closeLightbox() { document.getElementById('lightbox').classList.remove('open'); }
// ── Edit Modal ─────────────────────────────────────────────────────────────────
let _editLq = null, _editHq = null, _editLat = null, _editLon = null, _editStreet = null, _editCity = null, _editCityTimer = null;
function buildHoursGrid(gridId, existing) {
const grid = document.getElementById(gridId);
grid.innerHTML = '';
const byDay = {};
(existing || []).forEach(h => { byDay[h.dayOfWeek] = h; });
DAY_NAMES.forEach((name, i) => {
const day = i + 1;
const h = byDay[day] || {};
grid.insertAdjacentHTML('beforeend', `
<span>${name}</span>
<input type="time" id="open_${day}_${gridId}" value="${h.openTime || ''}" style="font-size:0.82rem;padding:0.25rem 0.4rem;">
<input type="time" id="close_${day}_${gridId}" value="${h.closeTime || ''}" style="font-size:0.82rem;padding:0.25rem 0.4rem;">
<label style="display:flex;align-items:center;gap:0.25rem;font-size:0.82rem;white-space:nowrap;">
<input type="checkbox" id="closed_${day}_${gridId}" ${h.closed ? 'checked' : ''}> Geschlossen
</label>
`);
});
}
function collectHours(gridId) {
const result = [];
for (let d = 1; d <= 7; d++) {
const open = document.getElementById(`open_${d}_${gridId}`)?.value;
const close = document.getElementById(`close_${d}_${gridId}`)?.value;
const closed = document.getElementById(`closed_${d}_${gridId}`)?.checked;
if (open || close || closed) {
result.push({ dayOfWeek: d, openTime: open || null, closeTime: close || null, closed: !!closed });
}
}
return result;
}
function openEditModal() {
const loc = locDetail;
_editLq = null; _editHq = null;
_editLat = loc.lat; _editLon = loc.lon;
_editStreet = loc.street || null; _editCity = loc.city || null;
document.getElementById('editName').value = loc.name || '';
document.getElementById('editDesc').value = loc.description || '';
const cityInp = document.getElementById('editCity');
const addressLabel = [loc.street, loc.city].filter(Boolean).join(', ');
cityInp.value = addressLabel;
cityInp.readOnly = !!addressLabel;
document.getElementById('editCityClear').style.display = addressLabel ? '' : 'none';
document.getElementById('editLocMsg').textContent = '';
const picSrc = loc.profilePictureHq || loc.profilePictureLq;
document.getElementById('editPicPreview').innerHTML = picSrc
? `<img src="data:image/jpeg;base64,${picSrc}" alt="Vorschau">`
: '📍';
buildHoursGrid('editHoursGrid', loc.openingHours || []);
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;
_editLq = await resizeImage(file, 120, 0.75);
_editHq = await resizeImage(file, 1024, 0.88);
document.getElementById('editPicPreview').innerHTML = `<img src="data:image/jpeg;base64,${_editHq}" alt="Vorschau">`;
}
function onEditCityInput() {
const q = document.getElementById('editCity').value.trim();
_editLat = null; _editLon = null; _editStreet = null; _editCity = null;
document.getElementById('editCityClear').style.display = 'none';
clearTimeout(_editCityTimer);
if (q.length < 2) { document.getElementById('editCitySuggestions').style.display = 'none'; return; }
_editCityTimer = setTimeout(() => fetchAddressSuggestions(q), 300);
}
function fmtAddress(r) {
const road = r.address.road || r.address.pedestrian || r.address.path || '';
const hn = r.address.house_number || '';
const street = (road + (hn ? ' ' + hn : '')).trim();
const plz = r.address.postcode || '';
const city = r.address.city || r.address.town || r.address.village || r.address.county || '';
const parts = [];
if (street) parts.push(street);
const cityPart = (plz && city) ? plz + ' ' + city : (plz || city);
if (cityPart) parts.push(cityPart);
return { label: parts.join(', '), street, city };
}
async function fetchAddressSuggestions(q) {
try {
const url = `https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(q)}&format=json&addressdetails=1&limit=5`;
const res = await fetch(url, { headers: { 'Accept-Language': 'de' } });
if (!res.ok) return;
const results = await res.json();
const ul = document.getElementById('editCitySuggestions');
if (!results.length) { ul.style.display = 'none'; return; }
ul.innerHTML = results.map(r => {
const { label, street, city } = fmtAddress(r);
const esc = s => s.replace(/\\/g,'\\\\').replace(/'/g,"\\'");
return `<li style="padding:0.5rem 0.75rem;cursor:pointer;font-size:0.9rem;border-bottom:1px solid var(--color-secondary);"
onmousedown="selectEditAddress(event,'${esc(label)}','${esc(street)}','${esc(city)}',${parseFloat(r.lat)},${parseFloat(r.lon)})">${label}</li>`;
}).join('');
ul.style.display = '';
} catch (_) {}
}
function selectEditAddress(e, label, street, city, lat, lon) {
e.preventDefault();
const inp = document.getElementById('editCity');
inp.value = label; inp.readOnly = true;
_editStreet = street || null;
_editCity = city || null;
_editLat = lat; _editLon = lon;
document.getElementById('editCityClear').style.display = '';
document.getElementById('editCitySuggestions').style.display = 'none';
document.getElementById('editLocMsg').textContent = '';
}
function clearEditCity() {
const inp = document.getElementById('editCity');
inp.value = ''; inp.readOnly = false;
_editLat = null; _editLon = null; _editStreet = null; _editCity = null;
document.getElementById('editCityClear').style.display = 'none';
document.getElementById('editLocMsg').textContent = '';
inp.focus();
}
document.addEventListener('click', e => {
if (!e.target.closest('#editStadtRow')) document.getElementById('editCitySuggestions').style.display = 'none';
});
async function submitEdit() {
const name = document.getElementById('editName').value.trim();
if (!name) { alert('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.';
document.getElementById('editCity').focus();
return;
}
const btn = document.getElementById('editSubmitBtn');
btn.disabled = true; btn.textContent = 'Wird gespeichert…';
try {
const body = {
name,
description: document.getElementById('editDesc').value.trim() || null,
street: _editStreet,
city: _editCity,
lat: _editLat,
lon: _editLon
};
if (_editLq) body.profilePictureLq = _editLq;
if (_editHq) body.profilePictureHq = _editHq;
const res = await fetch(`/locations/${locationId}`, {
method: 'PUT',
headers: {'Content-Type':'application/json'},
body: JSON.stringify(body)
});
if (!res.ok) throw new Error();
const hours = collectHours('editHoursGrid');
await fetch(`/locations/${locationId}/opening-hours`, {
method: 'PUT',
headers: {'Content-Type':'application/json'},
body: JSON.stringify(hours)
});
locDetail = await (await fetch(`/locations/${locationId}`)).json();
closeEditModal();
renderPage();
loadEvents();
} catch { alert('Fehler beim Speichern.'); }
finally { btn.disabled = false; btn.textContent = 'Speichern'; }
}
async function toggleFollow() {
const btn = document.getElementById('followBtn');
if (btn) btn.disabled = true;
try {
const res = await fetch(`/locations/${locationId}/follow`, { method: 'POST' });
if (!res.ok) throw new Error();
const data = await res.json();
isFollowing = data.following;
if (btn) {
btn.textContent = isFollowing ? '★ Abonniert' : '☆ Abonnieren';
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.'); }
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; }
window.location.href = '/community/locations.html';
}
// ── Event Modal ────────────────────────────────────────────────────────────────
let _evtImg = null, _editEventId = null;
function openEventModal(evtId) {
_editEventId = evtId || null;
_evtImg = null;
document.getElementById('eventModalTitle').textContent = evtId ? 'Veranstaltung bearbeiten' : 'Veranstaltung erstellen';
document.getElementById('eventTitle').value = '';
document.getElementById('eventDesc').value = '';
document.getElementById('eventStartAt').value = '';
document.getElementById('eventPicPreview').innerHTML = '🗓';
document.getElementById('eventModal').classList.add('open');
}
function closeEventModal() { document.getElementById('eventModal').classList.remove('open'); }
async function onEventPicChange(input) {
const file = input.files[0]; if (!file) return;
_evtImg = await resizeImage(file, 1024, 0.88);
document.getElementById('eventPicPreview').innerHTML = `<img src="data:image/jpeg;base64,${_evtImg}" alt="Vorschau">`;
}
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; }
const btn = document.getElementById('eventSubmitBtn');
btn.disabled = true; btn.textContent = 'Wird gespeichert…';
try {
const body = {
title,
description: document.getElementById('eventDesc').value.trim() || null,
imageData: _evtImg,
startAt: startAt + ':00'
};
const url = _editEventId
? `/locations/${locationId}/events/${_editEventId}`
: `/locations/${locationId}/events`;
const method = _editEventId ? 'PUT' : 'POST';
const res = await fetch(url, {
method,
headers: {'Content-Type':'application/json'},
body: JSON.stringify(body)
});
if (!res.ok) throw new Error();
closeEventModal();
loadEvents();
} catch { alert('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();
}
// ── Init ──────────────────────────────────────────────────────────────────────
loadPage();
</script>
</body>
</html>