Verschiebung nach anderem RePo - nun pro Projekt getrennt

This commit is contained in:
2026-04-01 10:41:19 +02:00
commit 7b9eda1d62
1048 changed files with 93351 additions and 0 deletions

View File

@@ -0,0 +1,766 @@
<!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>Profil xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<style>
.profile-picture-wrap {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
margin-bottom: 2rem;
}
.profile-picture {
width: 120px;
height: 120px;
border-radius: 50%;
object-fit: cover;
border: 3px solid var(--color-secondary);
background: var(--color-secondary);
display: flex;
align-items: center;
justify-content: center;
font-size: 3rem;
color: var(--color-muted);
flex-shrink: 0;
}
.profile-picture img {
width: 120px;
height: 120px;
border-radius: 50%;
object-fit: cover;
}
.profile-field {
margin-bottom: 1.25rem;
}
.profile-field label {
margin-top: 0;
}
.profile-field-row {
display: flex;
align-items: stretch;
gap: 0.5rem;
}
.profile-field-row p {
flex: 1;
padding: 0.65rem 0.9rem;
border: 1px solid var(--color-secondary);
border-radius: 6px;
background: var(--color-secondary);
font-size: 1rem;
color: var(--color-text);
margin: 0;
}
.profile-field-row button {
flex-shrink: 0;
width: 175px;
padding: 0.65rem 0.75rem;
margin-top: 0;
white-space: nowrap;
}
input[type="file"] {
font-size: 0.85rem;
color: var(--color-muted);
}
/* ── Gallery ── */
.gallery-section-label {
font-size: 0.8rem;
font-weight: 600;
color: var(--color-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
margin: 1.75rem 0 0.75rem;
border-top: 1px solid var(--color-secondary);
padding-top: 1.5rem;
}
.gallery-upload-row {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1rem;
}
/* ── Vorlieben Tabs ── */
.vl-tabs { display: flex; gap: 0; flex-wrap: wrap;
border-bottom: 1px solid var(--color-secondary); margin-bottom: 1rem; }
.vl-tab-btn { background: none; border: none; border-bottom: 3px solid transparent;
border-radius: 0; padding: 0.5rem 1rem; font-size: 0.85rem; font-weight: 600;
color: var(--color-muted); cursor: pointer; margin-bottom: -1px;
transition: color .15s, border-color .15s; white-space: nowrap; }
.vl-tab-btn:hover { color: var(--color-text); background: none; }
.vl-tab-btn.active { color: var(--color-primary); border-bottom-color: var(--color-primary); }
.vl-tab-panel { display: none; }
.vl-tab-panel.active { display: block; }
/* ── Vorlieben Items ── */
.vorliebe-row {
display: flex; align-items: center; justify-content: space-between;
gap: 0.75rem; padding: 0.55rem 0.75rem; border-radius: 8px;
background: var(--color-card); border: 1px solid var(--color-secondary);
margin-bottom: 0.45rem;
}
.vorliebe-row-name { font-size: 0.9rem; flex: 1; min-width: 0; }
.vorliebe-smileys { display: flex; gap: 0.25rem; flex-shrink: 0; }
.vl-smiley {
display: inline-flex; align-items: center; cursor: pointer;
border: 2px solid transparent; border-radius: 6px;
padding: 0.1rem 0.15rem;
transition: border-color .15s, transform .1s;
user-select: none;
}
.vl-smiley img { height: 1.6rem; width: auto; display: block; }
.vl-smiley:hover { transform: scale(1.15); }
.vl-smiley.active { border-color: var(--vl-color); }
.vorlieben-hint { font-size: 0.82rem; color: var(--color-muted); margin-bottom: 1rem; }
#ownGallery {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.4rem;
margin-bottom: 1.5rem;
}
.own-thumb {
aspect-ratio: 1;
overflow: hidden;
border-radius: 6px;
position: relative;
background: var(--color-secondary);
cursor: pointer;
}
.own-thumb img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.own-thumb-delete {
position: absolute;
top: 4px;
right: 4px;
background: rgba(0,0,0,0.6);
border: none;
color: #fff;
border-radius: 50%;
width: 1.5rem;
height: 1.5rem;
font-size: 0.75rem;
line-height: 1;
cursor: pointer;
display: none;
align-items: center;
justify-content: center;
padding: 0;
margin: 0;
transition: background 0.15s;
}
.own-thumb:hover .own-thumb-delete { display: flex; }
.own-thumb-delete:hover { background: rgba(180,30,30,0.85); }
.btn-delete-row {
display: flex;
justify-content: flex-end;
margin-bottom: 1.25rem;
}
.btn-delete {
width: 175px;
padding: 0.65rem 0.75rem;
margin-top: 0;
background: transparent;
border: 1px solid var(--color-secondary);
border-radius: 6px;
color: var(--color-muted);
font-size: 0.85rem;
font-weight: normal;
cursor: pointer;
white-space: nowrap;
transition: background 0.15s, border-color 0.15s, color 0.15s;
}
.btn-delete:hover {
background: #3d0f1a;
border-color: var(--color-primary);
color: var(--color-primary);
}
/* ── Profile extras ── */
.profile-extras-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.75rem 1rem;
margin-bottom: 1rem;
}
.profile-extras-grid .full-col {
grid-column: 1 / -1;
}
select {
width: 100%;
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;
transition: border-color 0.2s;
appearance: none;
-webkit-appearance: none;
}
select:focus { border-color: var(--color-primary); }
textarea {
width: 100%;
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;
resize: vertical;
min-height: 100px;
transition: border-color 0.2s;
font-family: inherit;
}
textarea:focus { border-color: var(--color-primary); }
.char-count {
font-size: 0.75rem;
color: var(--color-muted);
text-align: right;
margin-top: 0.25rem;
}
.char-count.over { color: var(--color-primary); }
/* ── Modal ── */
.modal-backdrop {
display: none;
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.6);
z-index: 200;
align-items: center;
justify-content: center;
}
.modal-backdrop.visible {
display: flex;
}
.modal {
background: var(--color-card);
border: 1px solid var(--color-secondary);
border-radius: 12px;
padding: 2rem;
width: 100%;
max-width: 360px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6);
}
.modal h2 {
color: var(--color-primary);
font-size: 1.2rem;
margin-bottom: 1.25rem;
}
.modal-actions {
display: flex;
gap: 0.75rem;
margin-top: 1.25rem;
}
.modal-actions button {
flex: 1;
margin-top: 0;
}
</style>
</head>
<body class="app">
<div class="main">
<div class="content">
<div class="profile-picture-wrap">
<div class="profile-picture" id="profilePicDisplay"></div>
<input type="file" id="picFile" accept="image/*">
</div>
<!-- Freiwillige Profilangaben -->
<div class="gallery-section-label" style="margin-top:1.25rem;">Freiwillige Angaben</div>
<div class="profile-extras-grid">
<div>
<label>Alter</label>
<input type="text" id="profileAlter" readonly style="background:transparent;cursor:default;color:var(--color-muted);" placeholder="—">
</div>
<div>
<label>Größe (cm)</label>
<input type="number" id="profileGroesse" min="100" max="250" placeholder="—">
</div>
<div>
<label>Gewicht (kg)</label>
<input type="number" id="profileGewicht" min="30" max="300" placeholder="—">
</div>
<div>
<label>Geschlecht</label>
<select id="profileGeschlecht">
<option value="">— keine Angabe —</option>
<option value="WEIBLICH">weiblich</option>
<option value="DIVERS">divers</option>
<option value="MAENNLICH">männlich</option>
</select>
</div>
<div class="full-col">
<label>Neigung</label>
<select id="profileNeigung">
<option value="">— keine Angabe —</option>
<option value="DEVOT">devot</option>
<option value="EHER_DEVOT">eher devot</option>
<option value="SWITCHER">Switcher</option>
<option value="EHER_DOMINANT">eher dominant</option>
<option value="DOMINANT">dominant</option>
<option value="KEINES">keines</option>
</select>
</div>
<div class="full-col">
<label>Beziehungsstatus</label>
<select id="profileBeziehungsstatus">
<option value="">— keine Angabe —</option>
<option value="SINGLE">single</option>
<option value="IN_EINER_BEZIEHUNG">in einer Beziehung</option>
<option value="VERHEIRATET">verheiratet</option>
<option value="IN_EINER_OFFENEN_BEZIEHUNG">in einer offenen Beziehung</option>
<option value="IN_EINER_OFFENEN_EHE">in einer offenen Ehe</option>
</select>
</div>
<div class="full-col">
<label>Über mich</label>
<textarea id="profileBeschreibung" maxlength="600" placeholder="Erzähl etwas über dich…" oninput="updateCharCount()"></textarea>
<div class="char-count" id="charCount">0 / 600</div>
</div>
</div>
<div class="message" id="message"></div>
<button class="full-width" id="saveBtn" onclick="saveProfile()">Profil speichern</button>
<div class="gallery-section-label" style="margin-top:1.5rem;">Vorlieben</div>
<p class="vorlieben-hint">Wähle für jede Vorliebe aus, wie du dazu stehst. Nicht ausgefüllte Einträge werden nicht angezeigt.</p>
<div id="vorliebenSection"><p style="color:var(--color-muted);font-size:0.85rem;">Wird geladen…</p></div>
<div class="message" id="vorliebenMessage" style="margin-top:0.75rem;display:none;"></div>
<button class="full-width" id="saveVorliebenBtn" onclick="saveVorlieben()" style="margin-bottom:1.5rem;">Vorlieben speichern</button>
<div class="gallery-section-label">Meine Bilder</div>
<div class="gallery-upload-row">
<input type="file" id="galleryFile" accept="image/*" multiple style="display:none;" onchange="handleGalleryUpload(this.files)">
<button onclick="document.getElementById('galleryFile').click()">+ Bilder hochladen</button>
<span id="galleryUploadStatus" style="font-size:0.85rem;color:var(--color-muted);"></span>
</div>
<div id="ownGallery"></div>
</div>
</div>
<script src="/js/shared.js"></script>
<script src="/js/image-viewer.js"></script>
<script src="/js/icons.js"></script>
<script src="/js/sidebar.js"></script>
<script>
let currentPicture = null;
let currentPictureHq = null;
let pictureDirty = false;
let allGalleryImages = [];
fetch('/login/me')
.then(r => {
if (r.status === 401) { window.location.href = '/login.html'; return null; }
return r.json();
})
.then(user => {
if (!user) return;
if (user.profilePicture) {
currentPicture = user.profilePicture;
renderPicture(currentPicture);
}
// Fill optional profile fields
if (user.geburtsdatum) {
const birth = new Date(user.geburtsdatum);
const today = new Date();
const age = today.getFullYear() - birth.getFullYear()
- (today < new Date(today.getFullYear(), birth.getMonth(), birth.getDate()) ? 1 : 0);
document.getElementById('profileAlter').value = age + ' Jahre';
}
if (user.groesse) document.getElementById('profileGroesse').value = user.groesse;
if (user.gewicht) document.getElementById('profileGewicht').value = user.gewicht;
if (user.geschlecht) document.getElementById('profileGeschlecht').value = user.geschlecht;
if (user.neigung) document.getElementById('profileNeigung').value = user.neigung;
if (user.beziehungsstatus) document.getElementById('profileBeziehungsstatus').value = user.beziehungsstatus;
if (user.beschreibung) {
document.getElementById('profileBeschreibung').value = user.beschreibung;
updateCharCount();
}
myUserId = user.userId;
loadOwnGallery();
loadVorliebenEdit();
})
.catch(() => { window.location.href = '/login.html'; });
// Check if redirected back after email change
if (new URLSearchParams(window.location.search).get('emailChanged')) {
// Already handled by login.html redirect — nothing to do here
}
document.getElementById('picFile').addEventListener('change', async e => {
const file = e.target.files[0];
if (!file) return;
[currentPicture, currentPictureHq] = await Promise.all([toBase64(file, 96), toBase64(file, 1024)]);
pictureDirty = true;
renderPicture(currentPicture);
});
function renderPicture(base64) {
const el = document.getElementById('profilePicDisplay');
el.innerHTML = `<img src="data:image/png;base64,${base64}" alt="Profilbild">`;
}
function updateCharCount() {
const ta = document.getElementById('profileBeschreibung');
const el = document.getElementById('charCount');
const len = ta.value.length;
el.textContent = len + ' / 600';
el.classList.toggle('over', len > 600);
}
async function saveProfile() {
const btn = document.getElementById('saveBtn');
btn.disabled = true;
btn.textContent = 'Wird gespeichert…';
hideMessage();
const beschreibung = document.getElementById('profileBeschreibung').value.trim();
if (beschreibung.length > 600) {
showMessage('Die Beschreibung darf maximal 600 Zeichen lang sein.', 'error');
btn.disabled = false;
btn.textContent = 'Profil speichern';
return;
}
const toNullOrInt = id => {
const v = document.getElementById(id).value;
return v ? parseInt(v, 10) : null;
};
const toNullOrStr = id => {
const v = document.getElementById(id).value;
return v || null;
};
try {
const [picRes, profileRes] = await Promise.all([
pictureDirty ? fetch('/user/me/picture', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ picture: currentPicture, pictureHq: currentPictureHq })
}) : Promise.resolve({ ok: true }),
fetch('/user/me/profile', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
groesse: toNullOrInt('profileGroesse'),
gewicht: toNullOrInt('profileGewicht'),
geschlecht: toNullOrStr('profileGeschlecht'),
neigung: toNullOrStr('profileNeigung'),
beziehungsstatus: toNullOrStr('profileBeziehungsstatus'),
beschreibung: beschreibung || null
})
})
]);
if (picRes.ok && profileRes.ok) {
showMessage('Gespeichert!', 'success');
} else {
showMessage(`Fehler beim Speichern.`, 'error');
}
} catch (err) {
showMessage('Server nicht erreichbar.', 'error');
console.error(err);
} finally {
btn.disabled = false;
btn.textContent = 'Profil speichern';
}
}
// ── Gallery ──
let myUserId = null;
async function loadOwnGallery() {
if (!myUserId) return;
const res = await fetch('/social/profile-images?userId=' + myUserId);
if (!res.ok) return;
const images = await res.json();
renderOwnGallery(images);
}
function renderOwnGallery(images) {
allGalleryImages = images;
const grid = document.getElementById('ownGallery');
if (images.length === 0) {
grid.innerHTML = '<p style="color:var(--color-muted);font-size:0.85rem;grid-column:1/-1;">Noch keine Bilder hochgeladen.</p>';
return;
}
grid.innerHTML = images.map((img, i) => `
<div class="own-thumb" onclick="openGalleryViewer(${i})" style="cursor:pointer;">
<img src="data:image/jpeg;base64,${img.imageData}" alt="Galerie-Bild">
<button class="own-thumb-delete" onclick="deleteGalleryImage('${img.imageId}', event)" title="Bild löschen">✕</button>
</div>
`).join('');
}
function openGalleryViewer(index) {
imageViewer.open({
images: allGalleryImages.map(img => ({
src: 'data:image/jpeg;base64,' + img.imageData,
id: img.imageId,
likedByMe: false,
likeCount: 0
})),
index,
showLike: false,
showComments: false
});
}
async function deleteGalleryImage(imageId, event) {
event.stopPropagation();
const res = await fetch('/social/profile-images/' + imageId, { method: 'DELETE' });
if (res.ok || res.status === 204) loadOwnGallery();
}
async function handleGalleryUpload(files) {
if (!files || files.length === 0) return;
const status = document.getElementById('galleryUploadStatus');
status.textContent = '0 / ' + files.length + ' hochgeladen…';
let done = 0;
for (const file of Array.from(files)) {
try {
const base64 = await toJpeg(file, 1024);
const res = await fetch('/social/profile-images', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ imageData: base64 })
});
if (res.status === 422) {
status.textContent = 'Limit von 20 Bildern erreicht.';
break;
}
} catch (e) {
console.error('Upload-Fehler:', e);
}
done++;
status.textContent = done + ' / ' + files.length + ' hochgeladen…';
}
status.textContent = done + ' Bild' + (done !== 1 ? 'er' : '') + ' hochgeladen.';
document.getElementById('galleryFile').value = '';
loadOwnGallery();
}
function toJpeg(file, max) {
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 > max || h > max) {
if (w >= h) { h = Math.max(1, Math.round(max * h / w)); w = max; }
else { w = Math.max(1, Math.round(max * w / h)); h = max; }
}
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', 0.85).split(',')[1]);
};
img.onerror = reject;
img.src = url;
});
}
function toBase64(file, max) {
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 > max || h > max) {
if (w >= h) { h = Math.max(1, Math.round(max * h / w)); w = max; }
else { w = Math.max(1, Math.round(max * w / h)); h = max; }
}
const canvas = document.createElement('canvas');
canvas.width = w; canvas.height = h;
canvas.getContext('2d').drawImage(img, 0, 0, w, h);
resolve(canvas.toDataURL('image/png').split(',')[1]);
};
img.onerror = reject;
img.src = url;
});
}
function showMessage(text, type) {
const el = document.getElementById('message');
el.textContent = text;
el.className = `message ${type}`;
el.style.display = 'block';
}
function hideMessage() {
document.getElementById('message').style.display = 'none';
}
// ── Vorlieben ──────────────────────────────────────────────────────────────
const VL_SMILEYS = [
{ value: 'GEHT_GAR_NICHT', img: '/img/vorlieben/verynegative.png', color: '#e53935', title: 'Geht gar nicht' },
{ value: 'EHER_NICHT', img: '/img/vorlieben/negative.png', color: '#fb8c00', title: 'Eher nicht' },
{ value: 'NEUTRAL', img: '/img/vorlieben/neutral.png', color: '#fdd835', title: 'Neutral' },
{ value: 'MAG_ICH', img: '/img/vorlieben/positiv.png', color: '#81c784', title: 'Mag ich' },
{ value: 'UNBEDINGT', img: '/img/vorlieben/verypositiv.png', color: '#2e7d32', title: 'Unbedingt' },
{ value: 'WILL_AUSPROBIEREN', img: '/img/vorlieben/dunno.png', color: '#1e88e5', title: 'Will ich ausprobieren' },
];
let vorliebenRatings = {};
async function loadVorliebenEdit() {
const container = document.getElementById('vorliebenSection');
try {
const [itemsRes, meRes] = await Promise.all([
fetch('/vorlieben/items'),
fetch('/vorlieben/me'),
]);
if (!itemsRes.ok) {
container.innerHTML = '<p style="color:var(--color-muted);font-size:0.85rem;">Keine Vorlieben konfiguriert.</p>';
return;
}
const kategorien = await itemsRes.json();
vorliebenRatings = meRes.ok ? await meRes.json() : {};
if (!kategorien.length) {
container.innerHTML = '<p style="color:var(--color-muted);font-size:0.85rem;">Keine Vorlieben konfiguriert.</p>';
return;
}
const smileysHtml = VL_SMILEYS.map(s =>
`<span class="vl-smiley" data-val="${s.value}" title="${s.title}" style="--vl-color:${s.color}"><img src="${s.img}" alt="${s.title}"></span>`
).join('');
// Tab buttons
const tabBtns = kategorien.map((kat, i) =>
`<button class="vl-tab-btn${i === 0 ? ' active' : ''}" onclick="switchVlTab('vlkat-${kat.kategorieId}', this)">${escapeHtml(kat.name)}</button>`
).join('');
// Tab panels
const tabPanels = kategorien.map((kat, i) => `
<div class="vl-tab-panel${i === 0 ? ' active' : ''}" id="vlkat-${kat.kategorieId}">
${kat.items.map(item => `
<div class="vorliebe-row">
<span class="vorliebe-row-name">${escapeHtml(item.name)}</span>
<div class="vorliebe-smileys" data-item-id="${item.itemId}">
${smileysHtml}
</div>
</div>`).join('')}
</div>`).join('');
container.innerHTML = `<div class="vl-tabs">${tabBtns}</div>${tabPanels}`;
// Mark saved values
container.querySelectorAll('.vorliebe-smileys[data-item-id]').forEach(group => {
const saved = vorliebenRatings[group.dataset.itemId];
if (saved) {
const btn = group.querySelector(`.vl-smiley[data-val="${saved}"]`);
if (btn) btn.classList.add('active');
}
});
// Click handler
container.addEventListener('click', e => {
const btn = e.target.closest('.vl-smiley');
if (!btn) return;
const group = btn.closest('.vorliebe-smileys');
const itemId = group.dataset.itemId;
const isActive = btn.classList.contains('active');
// Deselect all in this group
group.querySelectorAll('.vl-smiley').forEach(s => s.classList.remove('active'));
if (!isActive) {
btn.classList.add('active');
vorliebenRatings[itemId] = btn.dataset.val;
} else {
delete vorliebenRatings[itemId];
}
});
} catch (e) {
container.innerHTML = '<p style="color:var(--color-muted);font-size:0.85rem;">Fehler beim Laden.</p>';
}
}
function switchVlTab(panelId, btn) {
document.querySelectorAll('.vl-tab-btn').forEach(b => b.classList.remove('active'));
document.querySelectorAll('.vl-tab-panel').forEach(p => p.classList.remove('active'));
btn.classList.add('active');
document.getElementById(panelId).classList.add('active');
}
async function saveVorlieben() {
const btn = document.getElementById('saveVorliebenBtn');
const msgEl = document.getElementById('vorliebenMessage');
btn.disabled = true; btn.textContent = 'Wird gespeichert…';
// Collect: all known items → current rating or null
const ratings = {};
document.querySelectorAll('#vorliebenSection .vorliebe-smileys[data-item-id]').forEach(group => {
const active = group.querySelector('.vl-smiley.active');
ratings[group.dataset.itemId] = active ? active.dataset.val : null;
});
try {
const res = await fetch('/vorlieben/me', {
method: 'PUT', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(ratings),
});
msgEl.textContent = res.ok ? 'Vorlieben gespeichert.' : 'Fehler beim Speichern.';
msgEl.className = `message ${res.ok ? 'success' : 'error'}`;
msgEl.style.display = 'block';
setTimeout(() => { msgEl.style.display = 'none'; }, 3000);
} catch (e) {
msgEl.textContent = 'Fehler beim Speichern.'; msgEl.className = 'message error'; msgEl.style.display = 'block';
} finally {
btn.disabled = false; btn.textContent = 'Vorlieben speichern';
}
}
function escapeHtml(str) {
return str.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
</script>
</body>
</html>