Files
xxx-sphere-web/bin/main/static/community/gruppen.html
Mario e2a71ab096
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
Hashtags eingeführt
2026-04-11 01:14:33 +02:00

302 lines
16 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>Gruppen xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<style>
.tabs { display:flex; gap:0; margin-bottom:1.5rem; 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.25rem; font-size:0.95rem; font-weight:600; color:var(--color-muted); cursor:pointer; margin-bottom:-1px; transition:color 0.15s,border-color 0.15s; }
.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; }
.gruppe-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(220px,1fr)); gap:1rem; margin-top:0.5rem; }
.gruppe-card { background:var(--color-card); border:1px solid var(--color-secondary); border-radius:10px; overflow:hidden; display:flex; flex-direction:column; }
.gruppe-card-img { width:100%; height:110px; object-fit:cover; background:var(--color-secondary); display:flex; align-items:center; justify-content:center; font-size:2.5rem; }
.gruppe-card-img img { width:100%; height:100%; object-fit:cover; }
.gruppe-card-body { padding:0.85rem; flex:1; display:flex; flex-direction:column; gap:0.4rem; }
.gruppe-card-name { font-weight:700; font-size:1rem; }
.gruppe-card-meta { font-size:0.78rem; color:var(--color-muted); }
.role-badge { font-size:0.7rem; font-weight:700; padding:0.1rem 0.4rem; border-radius:4px; background:var(--color-primary); color:#fff; display:inline-block; }
.role-badge.mitglied { background:var(--color-secondary); color:var(--color-text); }
.anfrage-list { list-style:none; margin:0; padding:0; }
.anfrage-item { display:flex; align-items:center; justify-content:space-between; gap:1rem; padding:0.75rem 0; border-bottom:1px solid var(--color-secondary); }
.anfrage-item:last-child { border-bottom:none; }
.anfrage-name { font-weight:600; }
.anfrage-status { font-size:0.8rem; color:var(--color-muted); }
.empty-hint { color:var(--color-muted); font-size:0.9rem; margin-top:0.5rem; }
/* Dialog */
.dialog-backdrop { display:none; position:fixed; inset:0; background:rgba(0,0,0,0.6); z-index:200; align-items:center; justify-content:center; }
.dialog-backdrop.visible { display:flex; }
.dialog { background:var(--color-card); border:1px solid var(--color-secondary); border-radius:12px; padding:1.75rem; width:100%; max-width:420px; box-shadow:0 8px 32px rgba(0,0,0,0.6); max-height:90vh; overflow-y:auto; }
.dialog h3 { color:var(--color-primary); font-size:1.1rem; margin-bottom:1.25rem; }
.dialog label { display:block; font-size:0.8rem; color:#aaa; margin-bottom:0.3rem; margin-top:1rem; }
.dialog input, .dialog textarea { width:100%; box-sizing:border-box; }
.dialog textarea { width:100%; padding:0.6rem 0.85rem; border:1px solid var(--color-secondary); border-radius:6px; background:var(--color-secondary); color:var(--color-text); font-size:0.95rem; outline:none; transition:border-color 0.2s; resize:vertical; min-height:80px; box-sizing:border-box; }
.dialog textarea:focus { border-color:var(--color-primary); }
.dialog-actions { display:flex; justify-content:flex-end; gap:0.75rem; margin-top:1.5rem; }
.dialog-actions button { flex:none; margin:0; padding:0.55rem 1.1rem; font-size:0.9rem; width:auto; }
.toggle-row { display:flex; align-items:center; gap:0.75rem; margin-top:0.75rem; }
.toggle-row label { margin:0; font-size:0.9rem; color:var(--color-text); }
.img-preview { width:100%; max-height:140px; object-fit:cover; border-radius:6px; margin-top:0.5rem; display:none; }
.card-notif { font-size:0.75rem; font-weight:700; color:#fff; background:#e67e22; border-radius:4px; padding:0.15rem 0.45rem; display:none; width:fit-content; }
</style>
</head>
<body class="app">
<div class="main">
<div class="content">
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:1.25rem;">
<h1 style="margin:0;">Gruppen</h1>
<div style="display:flex; gap:0.5rem;">
<button onclick="location.href='/search.html?tab=gruppen'" style="width:auto; padding:0.4rem 1rem; font-size:0.9rem; margin:0;">🔍 Suchen</button>
<button onclick="openCreateDialog()" style="width:auto; padding:0.4rem 1rem; font-size:0.9rem; margin:0;">+ Erstellen</button>
</div>
</div>
<div class="tabs">
<button class="tab-btn active" data-tab="mine" onclick="switchTab('mine', this)">Meine Gruppen</button>
<button class="tab-btn" data-tab="requests" onclick="switchTab('requests', this)">Meine Anfragen</button>
</div>
<!-- Meine Gruppen -->
<div class="tab-panel active" id="tab-mine">
<div class="gruppe-grid" id="mineGrid"></div>
<p class="empty-hint" id="mineEmpty" style="display:none;">Du bist noch in keiner Gruppe.</p>
</div>
<!-- Meine Anfragen -->
<div class="tab-panel" id="tab-requests">
<ul class="anfrage-list" id="requestsList"></ul>
<p class="empty-hint" id="requestsEmpty" style="display:none;">Keine ausstehenden Anfragen.</p>
</div>
</div>
</div>
<!-- Gruppe erstellen Dialog -->
<div class="dialog-backdrop" id="createDialog">
<div class="dialog">
<h3>Gruppe erstellen</h3>
<label>Name *</label>
<input type="text" id="createName" maxlength="100" placeholder="Gruppenname">
<label>Beschreibung</label>
<textarea id="createDesc" maxlength="1000" placeholder="Worum geht es in dieser Gruppe?"></textarea>
<label>Bild (optional)</label>
<input type="file" id="createBildFile" accept="image/*" onchange="previewBild(this,'createBildPreview','createBildData')">
<img id="createBildPreview" class="img-preview" alt="">
<input type="hidden" id="createBildData">
<div class="toggle-row">
<input type="checkbox" id="createPrivate">
<label for="createPrivate">Private Gruppe (Beitritt nur per Anfrage)</label>
</div>
<p class="message error" id="createError" style="display:none; margin-top:0.75rem;"></p>
<div class="dialog-actions">
<button class="secondary" onclick="closeCreateDialog()">Abbrechen</button>
<button onclick="createGruppe()">Erstellen</button>
</div>
</div>
</div>
<script src="/js/icons.js"></script>
<script src="/js/nav.js"></script>
<script src="/js/social-sidebar.js"></script>
<script>
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('tab-' + name).classList.add('active');
localStorage.setItem('tab_gruppen', name);
if (name === 'mine') loadMine();
if (name === 'requests') loadRequests();
}
const _savedGruppenTab = localStorage.getItem('tab_gruppen');
if (_savedGruppenTab) {
const _btn = document.querySelector(`.tab-btn[data-tab="${_savedGruppenTab}"]`);
if (_btn) switchTab(_savedGruppenTab, _btn);
}
function esc(s) { const d = document.createElement('div'); d.textContent = s ?? ''; return d.innerHTML; }
function gruppeCard(g) {
const img = g.bild
? `<div class="gruppe-card-img"><img src="data:image/jpeg;base64,${g.bild}" alt=""></div>`
: `<div class="gruppe-card-img">👥</div>`;
const roleBadge = g.myRole
? `<span class="role-badge ${g.myRole === 'ADMIN' ? '' : 'mitglied'}">${g.myRole === 'ADMIN' ? 'Admin' : 'Mitglied'}</span>`
: '';
const privBadge = g.isPrivate ? ' 🔒' : '';
return `
<div class="gruppe-card" id="gc-${g.gruppeId}" onclick="location.href='/community/gruppe.html?gruppeId=${g.gruppeId}'" style="cursor:pointer;">
${img}
<div class="gruppe-card-body">
<div class="gruppe-card-name">${esc(g.name)}${privBadge} ${roleBadge}</div>
<div class="gruppe-card-meta">${g.memberCount} Mitglied${g.memberCount !== 1 ? 'er' : ''} · ${g.postCount} Beiträge</div>
${g.beschreibung ? `<div style="font-size:0.82rem;color:var(--color-muted);">${esc(g.beschreibung.substring(0,80))}${g.beschreibung.length>80?'…':''}</div>` : ''}
<div class="card-notif" id="notif-${g.gruppeId}"></div>
</div>
</div>`;
}
async function loadMine() {
try {
const res = await fetch('/gruppen/mine');
if (!res.ok) return;
const data = await res.json();
const grid = document.getElementById('mineGrid');
grid.innerHTML = '';
if (data.length === 0) { document.getElementById('mineEmpty').style.display = ''; return; }
document.getElementById('mineEmpty').style.display = 'none';
data.forEach(g => grid.insertAdjacentHTML('beforeend', gruppeCard(g)));
loadAdminBadges(data);
} catch(e) { console.error(e); }
}
async function loadAdminBadges(groups) {
const adminGroups = groups.filter(g => g.myRole === 'ADMIN');
await Promise.all(adminGroups.map(async g => {
const [reqRes, repRes] = await Promise.all([
fetch('/gruppen/' + g.gruppeId + '/requests'),
fetch('/gruppen/' + g.gruppeId + '/reports')
]);
const reqs = reqRes.ok ? await reqRes.json() : [];
const reps = repRes.ok ? await repRes.json() : [];
const total = reqs.length + reps.length;
if (total === 0) return;
const el = document.getElementById('notif-' + g.gruppeId);
if (!el) return;
const parts = [];
if (reqs.length > 0) parts.push(reqs.length + ' Anfrage' + (reqs.length !== 1 ? 'n' : ''));
if (reps.length > 0) parts.push(reps.length + ' Meldung' + (reps.length !== 1 ? 'en' : ''));
el.textContent = '🔔 ' + parts.join(' · ');
el.style.display = '';
}));
}
async function loadRequests() {
try {
const reqRes = await fetch('/gruppen/requests/mine');
if (!reqRes.ok) { document.getElementById('requestsEmpty').style.display = ''; return; }
const data = await reqRes.json();
const list = document.getElementById('requestsList');
list.innerHTML = '';
if (data.length === 0) { document.getElementById('requestsEmpty').style.display = ''; return; }
document.getElementById('requestsEmpty').style.display = 'none';
data.forEach(r => {
list.insertAdjacentHTML('beforeend', `
<li class="anfrage-item" id="req-${r.anfrageId}">
<div>
<div class="anfrage-name">${esc(r.gruppeName)}</div>
<div class="anfrage-status">Ausstehend seit ${fmtDate(r.angefragtAt)}</div>
${r.nachricht ? `<div style="font-size:0.82rem;color:var(--color-muted);margin-top:0.2rem;">"${esc(r.nachricht)}"</div>` : ''}
</div>
<button class="secondary" onclick="withdrawRequest('${r.gruppeId}', '${r.anfrageId}', this)">Zurückziehen</button>
</li>`);
});
} catch(e) { console.error(e); }
}
async function withdrawRequest(gruppeId, anfrageId, btn) {
btn.disabled = true;
try {
await fetch('/gruppen/' + gruppeId + '/requests/mine', { method: 'DELETE' });
document.getElementById('req-' + anfrageId)?.remove();
if (document.getElementById('requestsList').children.length === 0)
document.getElementById('requestsEmpty').style.display = '';
} catch(e) { btn.disabled = false; }
}
function fmtDate(iso) {
if (!iso) return '';
return new Date(iso).toLocaleDateString('de-DE', { day:'2-digit', month:'2-digit', year:'numeric' });
}
// ── Create dialog ──
function openCreateDialog() {
document.getElementById('createName').value = '';
document.getElementById('createDesc').value = '';
document.getElementById('createPrivate').checked = false;
document.getElementById('createBildData').value = '';
document.getElementById('createBildPreview').style.display = 'none';
document.getElementById('createBildFile').value = '';
document.getElementById('createDialog').classList.add('visible');
}
function closeCreateDialog() { document.getElementById('createDialog').classList.remove('visible'); document.getElementById('createError').style.display = 'none'; }
function showCreateError(text) {
const el = document.getElementById('createError');
el.textContent = text;
el.style.display = 'block';
}
async function createGruppe() {
const name = document.getElementById('createName').value.trim();
if (!name) { showCreateError('Bitte einen Namen eingeben.'); return; }
document.getElementById('createError').style.display = 'none';
const body = {
name,
beschreibung: document.getElementById('createDesc').value.trim() || null,
bild: document.getElementById('createBildData').value || null,
isPrivate: document.getElementById('createPrivate').checked
};
try {
const res = await fetch('/gruppen', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body) });
if (res.ok || res.status === 201) {
const g = await res.json();
closeCreateDialog();
window.location.href = '/community/gruppe.html?gruppeId=' + g.gruppeId;
} else {
showCreateError('Fehler beim Erstellen der Gruppe.');
}
} catch(e) { showCreateError('Fehler: ' + e.message); }
}
// ── Image preview ──
function previewBild(input, previewId, dataId) {
const file = input.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = e => {
const original = new Image();
original.onload = () => {
const MAX = 256;
let w = original.width, h = original.height;
if (w > MAX || h > MAX) {
if (w > h) { h = Math.round(h * MAX / w); w = MAX; }
else { w = Math.round(w * MAX / h); h = MAX; }
}
const canvas = document.createElement('canvas');
canvas.width = w; canvas.height = h;
canvas.getContext('2d').drawImage(original, 0, 0, w, h);
const scaled = canvas.toDataURL('image/jpeg', 0.85);
const img = document.getElementById(previewId);
img.src = scaled;
img.style.display = 'block';
document.getElementById(dataId).value = scaled.split(',')[1];
};
original.src = e.target.result;
};
reader.readAsDataURL(file);
}
document.getElementById('createDialog').addEventListener('click', e => {
if (e.target === document.getElementById('createDialog')) closeCreateDialog();
});
loadMine();
</script>
</body>
</html>