Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
629 lines
32 KiB
HTML
629 lines
32 KiB
HTML
<!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 & 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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||
}
|
||
|
||
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>
|