Files
xxx-sphere-web/bin/main/static/community/locations.html
Mario 2b0ce62d33
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
Menp überarbeitet
2026-04-08 16:52:43 +02:00

649 lines
35 KiB
HTML
Raw Permalink 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>Locations xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<style>
.tabs { display:flex; gap:0; align-items:center; margin-bottom:1.25rem; border-bottom:1px solid var(--color-secondary); }
.tab-btn { background:none; border:none; border-bottom:3px solid transparent; border-radius:0; padding:0.6rem 1.4rem; font-size:0.95rem; font-weight:600; color:var(--color-muted); cursor:pointer; margin-bottom:-1px; transition:color 0.15s,border-color 0.15s; width:auto; margin-top:0; }
.tab-btn:hover { color:var(--color-text); background:none; }
.tab-btn.active { color:var(--color-primary); border-bottom-color:var(--color-primary); }
.tab-panel { display:none; }
.tab-panel.active { display:block; }
/* ── Filter-Drawer ── */
.filter-overlay-bg { position:fixed; inset:0; background:rgba(0,0,0,0.55); z-index:200; opacity:0; pointer-events:none; transition:opacity 0.2s; }
.filter-overlay-bg.open { opacity:1; pointer-events:all; }
.filter-drawer { position:fixed; top:0; right:0; bottom:0; width:min(320px,92vw); background:var(--color-card); border-left:1px solid var(--color-secondary); z-index:201; display:flex; flex-direction:column; transform:translateX(100%); transition:transform 0.25s ease; overflow:hidden; }
.filter-drawer.open { transform:translateX(0); }
.filter-drawer-header { display:flex; align-items:center; justify-content:space-between; padding:1rem 1.25rem 0.75rem; border-bottom:1px solid var(--color-secondary); flex-shrink:0; }
.filter-drawer-header h3 { font-size:1rem; font-weight:700; color:var(--color-primary); margin:0; }
.filter-close-btn { background:none; border:none; color:var(--color-muted); font-size:1.3rem; cursor:pointer; padding:0.2rem 0.4rem; line-height:1; border-radius:4px; }
.filter-close-btn:hover { background:var(--color-secondary); color:var(--color-text); }
.filter-drawer-body { flex:1; overflow-y:auto; padding:1rem 1.25rem; display:flex; flex-direction:column; gap:1.25rem; }
.filter-drawer-footer { padding:0.75rem 1.25rem; border-top:1px solid var(--color-secondary); display:flex; gap:0.5rem; flex-shrink:0; }
.apply-btn { flex:1; padding:0.65rem; font-size:0.9rem; }
.reset-btn { padding:0.65rem 1rem; font-size:0.9rem; background:var(--color-secondary); color:var(--color-muted); border:1px solid var(--color-secondary); border-radius:6px; cursor:pointer; }
.reset-btn:hover { color:var(--color-text); }
.filter-group { display:flex; flex-direction:column; gap:0.35rem; }
.filter-group > label { font-size:0.78rem; color:var(--color-muted); margin:0; display:flex; justify-content:space-between; }
.range-val { color:var(--color-text); }
.filter-group input[type="range"] { padding:0; background:none; border:none; accent-color:var(--color-primary); width:100%; }
.filter-badge { display:inline-flex; align-items:center; justify-content:center; background:var(--color-primary); color:#fff; border-radius:50%; width:1rem; height:1rem; font-size:0.62rem; font-weight:700; line-height:1; }
.search-bar { display:flex; gap:0.5rem; flex-wrap:wrap; align-items:flex-end; margin-bottom:1.25rem; }
.search-bar .input-wrap { flex:1; min-width:160px; position:relative; }
.search-bar input, .search-bar select { width:100%; box-sizing:border-box; }
.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; }
.loc-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(180px,1fr)); gap:1rem; }
.loc-card { background:var(--color-card); border:1px solid var(--color-secondary); border-radius:10px; overflow:hidden; cursor:pointer; transition:border-color 0.15s, box-shadow 0.15s; text-decoration:none; color:inherit; display:block; }
.loc-card:hover { border-color:var(--color-primary); box-shadow:0 2px 8px rgba(0,0,0,.15); }
.loc-card-img { width:100%; aspect-ratio:1; object-fit:cover; background:var(--color-secondary); display:flex; align-items:center; justify-content:center; font-size:2rem; }
.loc-card-img img { width:100%; height:100%; object-fit:cover; }
.loc-card-body { padding:0.6rem 0.75rem; }
.loc-card-name { font-weight:600; font-size:0.9rem; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.loc-card-dist { font-size:0.75rem; color:var(--color-muted); margin-top:0.1rem; }
.loc-card-skeleton { background:var(--color-secondary); border-radius:10px; aspect-ratio:1; animation:pulse 1.4s infinite; }
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.5} }
.empty-hint { color:var(--color-muted); font-size:0.9rem; margin-top:0.5rem; }
.sentinel { height:1px; }
/* 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; }
.disclaimer-box { background:rgba(var(--color-primary-rgb,180,0,60),.08); border:1px solid var(--color-primary); border-radius:8px; padding:0.85rem 1rem; font-size:0.88rem; margin:0.75rem 0; }
.disclaimer-box label { display:flex; gap:0.5rem; align-items:flex-start; cursor:pointer; margin:0; color:var(--color-text); font-size:0.88rem; }
.disclaimer-box input[type="checkbox"] { width:auto; padding:0; border:none; background:none; flex-shrink:0; margin-top:0.15rem; cursor:pointer; }
.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; }
.hours-grid span { white-space:nowrap; }
.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 class="app">
<!-- ── Autocomplete (außerhalb des transformierten Drawers) ── -->
<ul id="filterCitySuggestions" 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>
<!-- ── Filter-Drawer ── -->
<div class="filter-overlay-bg" id="filterBg"></div>
<div class="filter-drawer" id="filterDrawer">
<div class="filter-drawer-header">
<h3>Filter</h3>
<button class="filter-close-btn" id="filterCloseBtn" aria-label="Filter schließen"></button>
</div>
<div class="filter-drawer-body">
<div class="filter-group">
<label>Ort</label>
<div style="position:relative;" id="filterCityRow">
<input type="text" id="filterCity" placeholder="Stadt suchen und auswählen…" autocomplete="off"
style="padding-right:2rem;" oninput="onFilterCityInput()">
<button id="filterCityClear" onclick="clearFilterCity()" 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>
</div>
</div>
<div class="filter-group">
<label>Umkreis <span class="range-val" id="distVal">50 km</span></label>
<input type="range" id="filterRadius" min="5" max="250" step="5" value="50"
oninput="document.getElementById('distVal').textContent = this.value + ' km'">
</div>
</div>
<div class="filter-drawer-footer">
<button class="btn apply-btn" id="applyBtn">Anwenden</button>
<button class="reset-btn" id="resetBtn">Zurücksetzen</button>
</div>
</div>
<div class="main">
<div class="content">
<div class="tabs">
<button class="tab-btn active" onclick="switchTab('search',this)">Suchen</button>
<button class="tab-btn" onclick="switchTab('mine',this)">Meine Locations</button>
<div style="margin-left:auto;display:flex;align-items:center;gap:0.4rem;padding-bottom:1px;">
<button id="filterOpenBtn" title="Filter"
style="display:flex;align-items:center;justify-content:center;position:relative;width:2rem;height:2rem;border-radius:50%;background:var(--color-secondary);color:var(--color-muted);border:none;cursor:pointer;transition:background 0.15s,color 0.15s;padding:0;"
onmouseover="this.style.background='var(--color-primary)';this.style.color='#fff';"
onmouseout="this.style.background='var(--color-secondary)';this.style.color='var(--color-muted)';">
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
<line x1="4" y1="6" x2="20" y2="6"/><line x1="8" y1="12" x2="16" y2="12"/><line x1="11" y1="18" x2="13" y2="18"/>
</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>
<!-- ── Suche ────────────────────────────────────────────────────── -->
<div id="paneSearch" class="tab-panel active">
<div class="loc-grid" id="searchGrid"></div>
<p class="empty-hint" id="searchEmpty" style="display:none;">Keine Locations in diesem Umkreis gefunden.</p>
<p class="empty-hint" id="searchHint" style="display:none;">Wähle im Filter einen Ort, um Locations in deiner Nähe zu suchen.</p>
<div class="sentinel" id="searchSentinel"></div>
</div>
<!-- ── Meine Locations ──────────────────────────────────────────── -->
<div id="paneMine" class="tab-panel">
<div class="loc-grid" id="mineGrid"></div>
<p class="empty-hint" id="mineEmpty" style="display:none;">Du hast noch keine Locations angelegt.</p>
</div>
</div>
</div>
<!-- ── Erstellen-Modal ──────────────────────────────────────────────── -->
<div class="modal-overlay" id="createModal">
<div class="modal">
<h3>Location anlegen</h3>
<div class="img-row">
<div class="img-preview" id="createPicPreview">📍</div>
<div>
<label class="btn" style="display:inline-block;cursor:pointer;font-size:0.85rem;">
Profilbild wählen
<input type="file" id="createPicFile" accept="image/*" style="display:none;" onchange="onCreatePicChange(this)">
</label>
</div>
</div>
<label>Name *</label>
<input type="text" id="createName" maxlength="200" placeholder="Name der Location">
<label>Beschreibung <span style="color:var(--color-muted);font-size:0.8rem;">(max. 1000 Zeichen)</span></label>
<textarea id="createDesc" 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="createStadtRow">
<div style="position:relative;">
<input type="text" id="createCity" 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="onCreateCityInput()">
<button id="createCityClear" onclick="clearCreateCity()" 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="createCitySuggestions" 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="createLocMsg" 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 <span style="color:var(--color-muted);font-size:0.8rem;">(optional)</span></label>
<div class="hours-grid" id="createHoursGrid"></div>
<div class="disclaimer-box">
<label>
<input type="checkbox" id="createOwnership">
<span>Ich bestätige, dass ich Eigentümer*in oder autorisierte*r Vertreter*in dieser Location bin und berechtigt bin, sie hier einzutragen.</span>
</label>
</div>
<div class="modal-footer">
<button class="btn" style="background:var(--color-secondary);color:var(--color-text);" onclick="closeCreateModal()">Abbrechen</button>
<button class="btn" id="createSubmitBtn" onclick="submitCreate()">Anlegen</button>
</div>
</div>
</div>
<script src="/js/icons.js"></script>
<script src="/js/nav.js"></script>
<script>
// ── 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;
});
}
// ── Tabs ─────────────────────────────────────────────────────────────────────
function switchTab(name, btn) {
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
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 ─────────────────────────────────────────────────────────────
let savedFilterCity = null, savedFilterLat = null, savedFilterLon = null, savedFilterRadius = 50;
let _inputLat = null, _inputLon = null, _cityTimer = null;
const filterDrawer = document.getElementById('filterDrawer');
const filterBg = document.getElementById('filterBg');
function openFilter() { filterDrawer.classList.add('open'); filterBg.classList.add('open'); document.body.style.overflow = 'hidden'; }
function closeFilter() { filterDrawer.classList.remove('open'); filterBg.classList.remove('open'); document.body.style.overflow = ''; }
document.getElementById('filterOpenBtn').addEventListener('click', openFilter);
document.getElementById('filterCloseBtn').addEventListener('click', closeFilter);
filterBg.addEventListener('click', closeFilter);
document.addEventListener('keydown', e => { if (e.key === 'Escape' && filterDrawer.classList.contains('open')) closeFilter(); });
function onFilterCityInput() {
const q = document.getElementById('filterCity').value.trim();
_inputLat = null; _inputLon = null;
document.getElementById('filterCityClear').style.display = 'none';
clearTimeout(_cityTimer);
if (q.length < 2) { document.getElementById('filterCitySuggestions').style.display = 'none'; return; }
_cityTimer = setTimeout(() => fetchCitySuggestions(q), 300);
}
async function fetchCitySuggestions(q) {
try {
const url = `https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(q)}&format=json&addressdetails=1&limit=5&featuretype=city`;
const res = await fetch(url, { headers: { 'Accept-Language': 'de' } });
if (!res.ok) return;
const results = await res.json();
const ul = document.getElementById('filterCitySuggestions');
if (!results.length) { ul.style.display = 'none'; return; }
ul.innerHTML = results.map(r => {
const city = r.address.city || r.address.town || r.address.village || r.address.county || r.name;
const country = r.address.country || '';
const label = city + (country ? ', ' + country : '');
return `<li style="padding:0.5rem 0.75rem;cursor:pointer;font-size:0.9rem;border-bottom:1px solid var(--color-secondary);"
onmousedown="selectFilterCity(event,'${label.replace(/'/g,"\\'")}',${parseFloat(r.lat)},${parseFloat(r.lon)})">${label}</li>`;
}).join('');
const rect = document.getElementById('filterCity').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 selectFilterCity(e, label, lat, lon) {
e.preventDefault();
const inp = document.getElementById('filterCity');
inp.value = label; inp.readOnly = true;
_inputLat = lat; _inputLon = lon;
document.getElementById('filterCityClear').style.display = '';
document.getElementById('filterCitySuggestions').style.display = 'none';
}
function clearFilterCity() {
const inp = document.getElementById('filterCity');
inp.value = ''; inp.readOnly = false;
_inputLat = null; _inputLon = null;
document.getElementById('filterCityClear').style.display = 'none';
inp.focus();
}
document.addEventListener('click', e => {
const ul = document.getElementById('filterCitySuggestions');
if (!e.target.closest('#filterCityRow') && !ul.contains(e.target)) ul.style.display = 'none';
});
document.getElementById('applyBtn').addEventListener('click', () => {
closeFilter();
const city = document.getElementById('filterCity').value.trim();
const radius = parseInt(document.getElementById('filterRadius').value);
savedFilterCity = city || null;
savedFilterLat = _inputLat;
savedFilterLon = _inputLon;
savedFilterRadius = radius;
saveFilterToDb(savedFilterCity, savedFilterLat, savedFilterLon, savedFilterRadius);
updateFilterBadge();
runSearch();
});
document.getElementById('resetBtn').addEventListener('click', () => {
clearFilterCity();
document.getElementById('filterRadius').value = 50;
document.getElementById('distVal').textContent = '50 km';
});
function saveFilterToDb(city, lat, lon, radius) {
fetch('/user/me/location-filter', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filterCity: city, filterLat: lat, filterLon: lon, filterMaxDistKm: radius })
}).catch(() => {});
}
function updateFilterBadge() {
const active = savedFilterCity != null;
const badge = document.getElementById('filterBadge');
badge.textContent = active ? '1' : '';
badge.style.display = active ? 'inline-flex' : 'none';
}
// ── Suche ─────────────────────────────────────────────────────────────────────
const searchState = { allIds: null, loaded: 0, loading: false, lat: 0, lon: 0 };
const BATCH = 20;
async function runSearch() {
if (savedFilterLat === null) {
document.getElementById('searchGrid').innerHTML = '';
document.getElementById('searchEmpty').style.display = 'none';
document.getElementById('searchHint').style.display = '';
return;
}
document.getElementById('searchGrid').innerHTML = '';
document.getElementById('searchEmpty').style.display = 'none';
document.getElementById('searchHint').style.display = 'none';
const res = await fetch(`/locations/ids?lat=${savedFilterLat}&lon=${savedFilterLon}&maxDistanceKm=${savedFilterRadius}`);
if (!res.ok) return;
const data = await res.json();
searchState.allIds = data.ids;
searchState.loaded = 0;
searchState.loading = false;
searchState.lat = savedFilterLat;
searchState.lon = savedFilterLon;
if (data.ids.length === 0) { document.getElementById('searchEmpty').style.display = ''; return; }
loadNextBatch();
}
async function loadNextBatch() {
if (searchState.loading || searchState.allIds === null) return;
if (searchState.loaded >= searchState.allIds.length) return;
searchState.loading = true;
const slice = searchState.allIds.slice(searchState.loaded, searchState.loaded + BATCH);
const res = await fetch('/locations/batch', {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({ ids: slice, lat: searchState.lat, lon: searchState.lon })
});
if (res.ok) {
const previews = await res.json();
const grid = document.getElementById('searchGrid');
previews.forEach(p => {
const a = document.createElement('a');
a.className = 'loc-card';
a.href = `/community/location-detail.html?id=${p.locationId}`;
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>
<div class="loc-card-body">
<div class="loc-card-name">${escHtml(p.name)}</div>
${p.distanzKm >= 0 ? `<div class="loc-card-dist">${p.distanzKm} km</div>` : ''}
</div>`;
grid.appendChild(a);
});
searchState.loaded += slice.length;
}
searchState.loading = false;
}
const observer = new IntersectionObserver(entries => {
if (entries[0].isIntersecting) loadNextBatch();
}, { rootMargin: '300px' });
observer.observe(document.getElementById('searchSentinel'));
// ── Meine Locations ───────────────────────────────────────────────────────────
let mineIds = [];
async function loadMine() {
const res = await fetch('/locations/mine');
if (!res.ok) return;
const data = await res.json();
mineIds = data.ids;
document.getElementById('mineEmpty').style.display = mineIds.length === 0 ? '' : 'none';
if (mineIds.length === 0) return;
const batchRes = await fetch('/locations/batch', {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({ ids: mineIds, lat: 0, lon: 0 })
});
if (!batchRes.ok) return;
const previews = await batchRes.json();
const grid = document.getElementById('mineGrid');
grid.innerHTML = '';
previews.forEach(p => {
const a = document.createElement('a');
a.className = 'loc-card';
a.href = `/community/location-detail.html?id=${p.locationId}`;
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>
<div class="loc-card-body">
<div class="loc-card-name">${escHtml(p.name)}</div>
</div>`;
grid.appendChild(a);
});
}
function escHtml(s) {
return (s || '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
// ── Create Modal ──────────────────────────────────────────────────────────────
let _createLq = null, _createHq = null;
let _createLat = null, _createLon = null;
let _createStreet = null, _createCity = null;
let _createCityTimer = null;
const DAY_NAMES = ['Montag','Dienstag','Mittwoch','Donnerstag','Freitag','Samstag','Sonntag'];
function buildHoursGrid(gridId) {
const grid = document.getElementById(gridId);
grid.innerHTML = '';
DAY_NAMES.forEach((name, i) => {
const day = i + 1;
grid.insertAdjacentHTML('beforeend', `
<span>${name}</span>
<input type="time" id="open_${day}" placeholder="--:--" style="font-size:0.82rem;padding:0.25rem 0.4rem;">
<input type="time" id="close_${day}" placeholder="--:--" 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}"> Geschlossen</label>
`);
});
}
function openCreateModal() {
_createLq = null; _createHq = null;
_createLat = null; _createLon = null; _createStreet = null; _createCity = null;
document.getElementById('createName').value = '';
document.getElementById('createDesc').value = '';
document.getElementById('createCity').value = '';
document.getElementById('createCity').readOnly = false;
document.getElementById('createCityClear').style.display = 'none';
document.getElementById('createLocMsg').textContent = '';
document.getElementById('createPicPreview').innerHTML = '📍';
document.getElementById('createOwnership').checked = false;
buildHoursGrid('createHoursGrid');
document.getElementById('createModal').classList.add('open');
}
function closeCreateModal() { document.getElementById('createModal').classList.remove('open'); }
async function onCreatePicChange(input) {
const file = input.files[0];
if (!file) return;
_createLq = await resizeImage(file, 120, 0.75);
_createHq = await resizeImage(file, 1024, 0.88);
const preview = document.getElementById('createPicPreview');
preview.innerHTML = `<img src="data:image/jpeg;base64,${_createHq}" alt="Vorschau">`;
}
function onCreateCityInput() {
const q = document.getElementById('createCity').value.trim();
_createLat = null; _createLon = null; _createStreet = null; _createCity = null;
document.getElementById('createCityClear').style.display = 'none';
clearTimeout(_createCityTimer);
if (q.length < 2) { document.getElementById('createCitySuggestions').style.display = 'none'; return; }
_createCityTimer = 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('createCitySuggestions');
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="selectCreateAddress(event,'${esc(label)}','${esc(street)}','${esc(city)}',${parseFloat(r.lat)},${parseFloat(r.lon)})">${label}</li>`;
}).join('');
ul.style.display = '';
} catch (_) {}
}
function selectCreateAddress(e, label, street, city, lat, lon) {
e.preventDefault();
const inp = document.getElementById('createCity');
inp.value = label; inp.readOnly = true;
_createStreet = street || null;
_createCity = city || null;
_createLat = lat; _createLon = lon;
document.getElementById('createCityClear').style.display = '';
document.getElementById('createCitySuggestions').style.display = 'none';
document.getElementById('createLocMsg').textContent = '';
}
function clearCreateCity() {
const inp = document.getElementById('createCity');
inp.value = ''; inp.readOnly = false;
_createLat = null; _createLon = null; _createStreet = null; _createCity = null;
document.getElementById('createCityClear').style.display = 'none';
document.getElementById('createLocMsg').textContent = '';
inp.focus();
}
document.addEventListener('click', e => {
if (!e.target.closest('#createStadtRow')) document.getElementById('createCitySuggestions').style.display = 'none';
});
function collectHours() {
const result = [];
for (let d = 1; d <= 7; d++) {
const open = document.getElementById(`open_${d}`)?.value;
const close = document.getElementById(`close_${d}`)?.value;
const closed = document.getElementById(`closed_${d}`)?.checked;
if (open || close || closed) {
result.push({ dayOfWeek: d, openTime: open || null, closeTime: close || null, closed: !!closed });
}
}
return result;
}
async function submitCreate() {
const name = document.getElementById('createName').value.trim();
const desc = document.getElementById('createDesc').value.trim();
const confirmed = document.getElementById('createOwnership').checked;
if (!name) { alert('Bitte gib einen Namen ein.'); return; }
if (!_createLat || !_createLon) {
document.getElementById('createLocMsg').textContent = 'Bitte eine Adresse aus der Vorschlagsliste auswählen oder per GPS ermitteln.';
document.getElementById('createCity').focus();
return;
}
if (!confirmed) { alert('Bitte bestätige, dass du Eigentümer*in der Location bist.'); return; }
const btn = document.getElementById('createSubmitBtn');
btn.disabled = true;
btn.textContent = 'Wird gespeichert…';
try {
const res = await fetch('/locations', {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({
name,
description: desc || null,
profilePictureLq: _createLq,
profilePictureHq: _createHq,
lat: _createLat,
lon: _createLon,
street: _createStreet || null,
city: _createCity || null,
ownershipConfirmed: true
})
});
if (!res.ok) throw new Error('Fehler beim Speichern');
const loc = await res.json();
// Öffnungszeiten setzen
const hours = collectHours();
if (hours.length > 0) {
await fetch(`/locations/${loc.locationId}/opening-hours`, {
method: 'PUT',
headers: {'Content-Type':'application/json'},
body: JSON.stringify(hours)
});
}
closeCreateModal();
window.location.href = `/community/location-detail.html?id=${loc.locationId}`;
} catch (err) {
alert('Fehler beim Anlegen: ' + err.message);
} finally {
btn.disabled = false;
btn.textContent = 'Anlegen';
}
}
// ── Init ──────────────────────────────────────────────────────────────────────
fetch('/login/me').then(r => r.ok ? r.json() : null).then(user => {
if (!user) return;
if (user.filterCity) {
savedFilterCity = user.filterCity;
savedFilterLat = user.filterLat;
savedFilterLon = user.filterLon;
savedFilterRadius = user.filterMaxDistKm || 50;
document.getElementById('filterCity').value = savedFilterCity;
document.getElementById('filterCity').readOnly = true;
document.getElementById('filterCityClear').style.display = '';
document.getElementById('filterRadius').value = savedFilterRadius;
document.getElementById('distVal').textContent = savedFilterRadius + ' km';
_inputLat = savedFilterLat;
_inputLon = savedFilterLon;
updateFilterBadge();
runSearch();
} else {
document.getElementById('searchHint').style.display = '';
}
}).catch(() => { document.getElementById('searchHint').style.display = ''; });
loadMine();
</script>
</body>
</html>