Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
302 lines
16 KiB
HTML
302 lines
16 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>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>
|