An den Verantaltungen und 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:
647
src/main/resources/static/community/locations.html
Normal file
647
src/main/resources/static/community/locations.html
Normal file
@@ -0,0 +1,647 @@
|
||||
<!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>
|
||||
</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 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>
|
||||
|
||||
</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/sidebar.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');
|
||||
}
|
||||
|
||||
// ── 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.profilePictureLq
|
||||
? `<img src="data:image/jpeg;base64,${p.profilePictureLq}" 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.profilePictureLq
|
||||
? `<img src="data:image/jpeg;base64,${p.profilePictureLq}" 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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
|
||||
// ── 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>
|
||||
Reference in New Issue
Block a user