Verschiebung nach anderem RePo - nun pro Projekt getrennt
This commit is contained in:
1323
bin/main/static/konto/einstellungen.html
Normal file
1323
bin/main/static/konto/einstellungen.html
Normal file
File diff suppressed because it is too large
Load Diff
766
bin/main/static/konto/profile.html
Normal file
766
bin/main/static/konto/profile.html
Normal 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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user