Files
xxx-sphere-web/bin/main/static/dating/besucher.html
Mario a13b8e894f
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
Alles mögliche
2026-04-08 00:53:49 +02:00

148 lines
8.1 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Profilbesucher xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<style>
.profiles-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 1rem; }
@media (max-width: 500px) { .profiles-grid { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); } }
.profile-card { background: var(--color-card); border: 1px solid var(--color-secondary); border-radius: 12px; overflow: hidden; transition: border-color 0.15s, box-shadow 0.15s; text-decoration: none; color: var(--color-text); display: flex; flex-direction: column; }
.profile-card:hover { border-color: var(--color-primary); box-shadow: 0 4px 18px rgba(0,0,0,0.35); }
.profile-card-img-wrap { position: relative; width: 100%; aspect-ratio: 1; flex-shrink: 0; overflow: hidden; background: var(--color-secondary); display: flex; align-items: center; justify-content: center; font-size: 3rem; }
.profile-card-img-wrap img { width: 100%; height: 100%; object-fit: cover; }
.profile-card-body { padding: 0.75rem; display: flex; flex-direction: column; gap: 0.3rem; flex: 1; }
.profile-card-name { font-weight: 700; font-size: 1rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.profile-card-meta { display: flex; flex-wrap: wrap; gap: 0.3rem; }
.meta-chip { padding: 0.1rem 0.45rem; border-radius: 20px; background: var(--color-secondary); font-size: 0.73rem; color: var(--color-muted); }
.meta-chip.time { color: var(--color-primary); }
.profile-card-desc { font-size: 0.78rem; color: var(--color-muted); line-height: 1.4; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; margin-top: 0.15rem; }
.profile-card-skeleton { background: var(--color-card); border: 1px solid var(--color-secondary); border-radius: 12px; overflow: hidden; display: flex; flex-direction: column; }
.skeleton-img { width: 100%; aspect-ratio: 1; background: linear-gradient(90deg, var(--color-secondary) 25%, var(--color-card) 50%, var(--color-secondary) 75%); background-size: 200% 100%; animation: shimmer 1.2s infinite; }
.skeleton-body { padding: 0.75rem; display: flex; flex-direction: column; gap: 0.5rem; }
.skeleton-line { height: 0.75rem; border-radius: 4px; background: linear-gradient(90deg, var(--color-secondary) 25%, var(--color-card) 50%, var(--color-secondary) 75%); background-size: 200% 100%; animation: shimmer 1.2s infinite; }
@keyframes shimmer { to { background-position: -200% 0; } }
.empty-state { text-align: center; padding: 3rem 1rem; color: var(--color-muted); grid-column: 1 / -1; }
.empty-state .icon { font-size: 2.5rem; margin-bottom: 0.75rem; }
#sentinel { height: 1px; margin-top: 1rem; }
.results-count { font-size: 0.85rem; color: var(--color-muted); display: block; margin-bottom: 0.75rem; }
</style>
</head>
<body class="app">
<div class="main">
<div class="content">
<h1 style="margin:0 0 0.15rem;">Profilbesucher</h1>
<span class="results-count" id="resultsCount"></span>
<div class="profiles-grid" id="profilesGrid"></div>
<div id="sentinel"></div>
</div>
</div>
<script src="/js/icons.js"></script>
<script src="/js/sidebar.js"></script>
<script>
const BATCH = 12;
let allEntries = [];
let loadedCount = 0;
let loading = false;
function relativeTime(iso) {
const diff = Date.now() - new Date(iso).getTime();
const m = Math.floor(diff / 60000);
if (m < 1) return 'gerade eben';
if (m < 60) return `vor ${m} Min.`;
const h = Math.floor(m / 60);
if (h < 24) return `vor ${h} Std.`;
const d = Math.floor(h / 24);
return `vor ${d} Tag${d !== 1 ? 'en' : ''}`;
}
function esc(s) { return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
function appendProfiles(profiles) {
const grid = document.getElementById('profilesGrid');
profiles.forEach(p => {
const entry = allEntries.find(e => e.userId === p.userId);
const pic = p.profilePictureHq || p.profilePicture;
const img = pic ? `<img src="data:image/png;base64,${pic}" alt="${esc(p.name)}" loading="lazy">` : '👤';
const chips = [
p.alter ? `<span class="meta-chip">${p.alter} J.</span>` : '',
p.geschlecht ? `<span class="meta-chip">${esc(p.geschlecht)}</span>` : '',
p.datingStadt ? `<span class="meta-chip">${esc(p.datingStadt)}</span>` : '',
entry?.visitedAt ? `<span class="meta-chip time">${relativeTime(entry.visitedAt)}</span>` : '',
].filter(Boolean).join('');
const desc = p.beschreibung ? `<div class="profile-card-desc">${esc(p.beschreibung)}</div>` : '';
const card = document.createElement('a');
card.className = 'profile-card';
card.href = `/community/benutzer.html?userId=${p.userId}`;
card.innerHTML = `
<div class="profile-card-img-wrap">${img}</div>
<div class="profile-card-body">
<div class="profile-card-name">${esc(p.name)}</div>
<div class="profile-card-meta">${chips}</div>
${desc}
</div>`;
grid.appendChild(card);
});
}
async function loadNextBatch() {
if (loading || loadedCount >= allEntries.length) return;
loading = true;
const batch = allEntries.slice(loadedCount, loadedCount + BATCH);
const grid = document.getElementById('profilesGrid');
const skelDiv = document.createElement('div');
skelDiv.style.display = 'contents';
skelDiv.innerHTML = Array.from({length: batch.length}, () => `
<div class="profile-card-skeleton">
<div class="skeleton-img"></div>
<div class="skeleton-body">
<div class="skeleton-line" style="width:70%"></div>
<div class="skeleton-line" style="width:50%"></div>
</div>
</div>`).join('');
grid.appendChild(skelDiv);
try {
const res = await fetch('/dating/profiles/batch', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(batch.map(e => e.userId))
});
grid.removeChild(skelDiv);
if (res.ok) appendProfiles(await res.json());
} catch { grid.removeChild(skelDiv); }
loadedCount += batch.length;
loading = false;
}
(async () => {
try {
const res = await fetch('/social/profile-visits/my-visitors');
if (!res.ok) throw new Error();
const visitors = await res.json();
allEntries = visitors.map(v => ({ userId: v.userId, visitedAt: v.visitedAt }));
document.getElementById('resultsCount').textContent =
allEntries.length === 0 ? 'Noch keine Besucher'
: allEntries.length + (allEntries.length === 1 ? ' Besucher' : ' Besucher');
if (!allEntries.length) {
document.getElementById('profilesGrid').innerHTML =
'<div class="empty-state"><div class="icon">👀</div><p>Noch niemand hat dein Profil besucht.</p></div>';
return;
}
loadNextBatch();
} catch {
document.getElementById('profilesGrid').innerHTML =
'<div class="empty-state"><div class="icon">⚠️</div><p>Fehler beim Laden.</p></div>';
}
})();
new IntersectionObserver(
e => { if (e[0].isIntersecting) loadNextBatch(); },
{ rootMargin: '300px' }
).observe(document.getElementById('sentinel'));
</script>
</body>
</html>