Files
xxx-sphere-web/bin/main/static/konto/profile.html

767 lines
30 KiB
HTML
Raw Permalink 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>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>