Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
644 lines
25 KiB
HTML
644 lines
25 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<link rel="icon" href="/img/icon.png" type="image/png">
|
||
<meta http-equiv="refresh" content="0;url=/games/aufgaben/toys.html">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Toys – xXx Sphere</title>
|
||
<link rel="stylesheet" href="/css/variables.css">
|
||
<link rel="stylesheet" href="/css/style.css">
|
||
<style>
|
||
/* ── Section ── */
|
||
.section + .section { margin-top: 2.5rem; }
|
||
|
||
.section-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 1rem;
|
||
padding-bottom: 0.5rem;
|
||
border-bottom: 1px solid var(--color-secondary);
|
||
}
|
||
.section-title {
|
||
font-size: 1.1rem;
|
||
font-weight: 600;
|
||
color: var(--color-primary);
|
||
margin: 0;
|
||
}
|
||
.btn-add {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.4rem;
|
||
background: var(--color-primary);
|
||
color: #fff;
|
||
border: none;
|
||
border-radius: 6px;
|
||
padding: 0.4rem 0.85rem;
|
||
font-size: 0.85rem;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: background 0.15s;
|
||
}
|
||
.btn-add:hover { background: #c73652; }
|
||
|
||
/* ── Toy grid ── */
|
||
.toy-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(270px, 1fr));
|
||
gap: 0.85rem;
|
||
}
|
||
|
||
/* ── Toy card ── */
|
||
.toy-card {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.85rem;
|
||
background: var(--color-card);
|
||
border: 1px solid var(--color-secondary);
|
||
border-radius: 10px;
|
||
padding: 0.8rem 0.9rem;
|
||
transition: border-color 0.15s;
|
||
position: relative;
|
||
}
|
||
.toy-card { cursor: pointer; }
|
||
.toy-card:hover { border-color: var(--color-primary); }
|
||
.toy-card.selected {
|
||
border-color: var(--color-primary);
|
||
background: rgba(233,69,96,0.06);
|
||
}
|
||
|
||
.toy-img {
|
||
width: 52px; height: 52px;
|
||
border-radius: 7px;
|
||
object-fit: cover;
|
||
flex-shrink: 0;
|
||
}
|
||
.toy-img-placeholder {
|
||
width: 52px; height: 52px;
|
||
border-radius: 7px;
|
||
background: var(--color-secondary);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 1.4rem;
|
||
flex-shrink: 0;
|
||
color: var(--color-muted);
|
||
}
|
||
.toy-info { flex: 1; min-width: 0; }
|
||
.toy-name {
|
||
font-size: 0.9rem;
|
||
font-weight: 600;
|
||
color: var(--color-text);
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
.toy-desc {
|
||
font-size: 0.78rem;
|
||
color: var(--color-muted);
|
||
margin-top: 0.2rem;
|
||
display: -webkit-box;
|
||
-webkit-line-clamp: 2;
|
||
-webkit-box-orient: vertical;
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* ── Section action buttons ── */
|
||
.section-actions { display: flex; align-items: center; gap: 0.5rem; }
|
||
.btn-action {
|
||
background: var(--color-secondary);
|
||
color: var(--color-text);
|
||
border: none;
|
||
border-radius: 6px;
|
||
padding: 0.4rem 0.85rem;
|
||
font-size: 0.85rem;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: background 0.15s, color 0.15s, opacity 0.15s;
|
||
}
|
||
.btn-action:disabled { opacity: 0.35; cursor: default; }
|
||
.btn-action:not(:disabled):hover { background: var(--color-primary); color: #fff; }
|
||
.btn-action-danger:not(:disabled):hover { background: rgba(233,69,96,0.18); color: var(--color-primary); }
|
||
.action-error {
|
||
font-size: 0.82rem;
|
||
color: var(--color-primary);
|
||
min-height: 1.1em;
|
||
margin-bottom: 0.4rem;
|
||
}
|
||
|
||
/* ── Empty / Loading ── */
|
||
.empty, .loading { color: var(--color-muted); font-size: 0.9rem; padding: 0.75rem 0; }
|
||
|
||
/* ── Inline-Fehler im Grid ── */
|
||
.grid-error {
|
||
font-size: 0.85rem;
|
||
color: var(--color-primary);
|
||
padding: 0.5rem 0;
|
||
}
|
||
|
||
/* ── 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.open { display: flex; }
|
||
.modal {
|
||
background: var(--color-card);
|
||
border: 1px solid var(--color-secondary);
|
||
border-radius: 12px;
|
||
padding: 2rem;
|
||
width: 100%;
|
||
max-width: 420px;
|
||
box-shadow: 0 12px 40px rgba(0,0,0,0.6);
|
||
}
|
||
.modal h2 {
|
||
color: var(--color-primary);
|
||
font-size: 1.1rem;
|
||
margin-bottom: 1.25rem;
|
||
}
|
||
.modal label {
|
||
display: block;
|
||
font-size: 0.8rem;
|
||
color: #aaa;
|
||
margin-top: 1rem;
|
||
margin-bottom: 0.3rem;
|
||
}
|
||
.modal input[type="text"],
|
||
.modal 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;
|
||
}
|
||
.modal input[type="text"]:focus,
|
||
.modal textarea:focus { border-color: var(--color-primary); }
|
||
.modal input[type="file"] {
|
||
font-size: 0.85rem;
|
||
color: var(--color-muted);
|
||
margin-top: 0.25rem;
|
||
}
|
||
.modal-actions {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 0.75rem;
|
||
margin-top: 1.5rem;
|
||
}
|
||
.modal-actions .btn-cancel {
|
||
background: var(--color-secondary);
|
||
color: var(--color-text);
|
||
border: none;
|
||
border-radius: 6px;
|
||
padding: 0.55rem 1.1rem;
|
||
font-size: 0.9rem;
|
||
cursor: pointer;
|
||
transition: background 0.15s;
|
||
}
|
||
.modal-actions .btn-cancel:hover { background: #1a4a8a; }
|
||
.modal-actions .btn-save {
|
||
background: var(--color-primary);
|
||
color: #fff;
|
||
border: none;
|
||
border-radius: 6px;
|
||
padding: 0.55rem 1.1rem;
|
||
font-size: 0.9rem;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: background 0.15s;
|
||
}
|
||
.modal-actions .btn-save:hover { background: #c73652; }
|
||
.modal-actions .btn-save:disabled { opacity: 0.5; cursor: default; }
|
||
.modal-error {
|
||
color: var(--color-primary);
|
||
font-size: 0.82rem;
|
||
margin-top: 0.75rem;
|
||
display: none;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.toy-grid { grid-template-columns: 1fr; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body class="app">
|
||
|
||
<!-- Erstell-/Bearbeitungs-Modal -->
|
||
<div class="modal-backdrop" id="createModal">
|
||
<div class="modal">
|
||
<h2 id="modalTitle">Neues Toy</h2>
|
||
<label for="toyName">Name *</label>
|
||
<input type="text" id="toyName" placeholder="z.B. Vibrator" maxlength="100">
|
||
<label for="toyDesc">Beschreibung</label>
|
||
<textarea id="toyDesc" rows="3" placeholder="Kurze Beschreibung…" maxlength="500"></textarea>
|
||
<label>Bild (optional)</label>
|
||
<div id="currentImageWrap" style="display:none; align-items:center; gap:0.5rem; margin-bottom:0.4rem;">
|
||
<img id="currentImage" style="max-width:64px; max-height:64px; border-radius:6px;" src="" alt="">
|
||
<span style="font-size:0.78rem; color:var(--color-muted);">Aktuelles Bild – neues Bild wählen zum Ersetzen</span>
|
||
</div>
|
||
<input type="file" id="toyBild" accept="image/*">
|
||
<div class="modal-error" id="modalError"></div>
|
||
<div class="modal-actions">
|
||
<button class="btn-cancel" id="cancelBtn">Abbrechen</button>
|
||
<button class="btn-save" id="saveBtn">Speichern</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="main">
|
||
<div class="content">
|
||
|
||
<!-- Meine Toys -->
|
||
<div class="section">
|
||
<div class="section-header">
|
||
<h2 class="section-title">Meine Toys</h2>
|
||
<div class="section-actions">
|
||
<button class="btn-action" id="editBtn" disabled>✎ Bearbeiten</button>
|
||
<button class="btn-action btn-action-danger" id="deleteBtn" disabled>✕ Löschen</button>
|
||
<button class="btn-add" id="openCreateBtn">+ Neu</button>
|
||
</div>
|
||
</div>
|
||
<div class="action-error" id="actionError"></div>
|
||
<div class="toy-grid" id="userGrid"></div>
|
||
<div id="userLoading" class="loading" style="display:none;"></div>
|
||
<div id="userSentinel"></div>
|
||
</div>
|
||
|
||
<!-- System-Toys -->
|
||
<div class="section">
|
||
<div class="section-header">
|
||
<h2 class="section-title">System-Toys</h2>
|
||
<div class="section-actions">
|
||
<button class="btn-action" id="copyBtn" disabled>⊕ In meine Toys kopieren</button>
|
||
</div>
|
||
</div>
|
||
<div class="action-error" id="systemActionError"></div>
|
||
<div class="toy-grid" id="systemGrid"></div>
|
||
<div id="systemLoading" class="loading" style="display:none;"></div>
|
||
<div id="systemSentinel"></div>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<script src="/js/icons.js"></script>
|
||
<script src="/js/nav.js"></script>
|
||
<script>
|
||
const PAGE_SIZE = 12;
|
||
let userPage = 0, userTotalPages = 1, userLoading = false;
|
||
let systemPage = 0, systemTotalPages = 1, systemLoading = false;
|
||
|
||
// ── Infinite-scroll observers ──
|
||
const userObserver = new IntersectionObserver(entries => {
|
||
if (entries[0].isIntersecting) loadUserPage();
|
||
}, { rootMargin: '200px' });
|
||
|
||
const systemObserver = new IntersectionObserver(entries => {
|
||
if (entries[0].isIntersecting) loadSystemPage();
|
||
}, { rootMargin: '200px' });
|
||
|
||
// ── Auth + initial load ──
|
||
fetch('/login/me')
|
||
.then(r => { if (r.status === 401) { window.location.href = '/login.html'; return null; } return r.ok ? r.json() : null; })
|
||
.then(user => {
|
||
if (!user) return;
|
||
userObserver.observe(document.getElementById('userSentinel'));
|
||
systemObserver.observe(document.getElementById('systemSentinel'));
|
||
})
|
||
.catch(() => { window.location.href = '/login.html'; });
|
||
|
||
// ── Load user toys (append, füllt Viewport automatisch auf) ──
|
||
async function loadUserPage() {
|
||
if (userLoading || userPage >= userTotalPages) return;
|
||
userLoading = true;
|
||
const loadEl = document.getElementById('userLoading');
|
||
try {
|
||
do {
|
||
loadEl.textContent = 'Wird geladen…';
|
||
loadEl.style.display = 'block';
|
||
const r = await fetch(`/toy/list/user?page=${userPage}&size=${PAGE_SIZE}`);
|
||
const data = await r.json();
|
||
userTotalPages = data.totalPages || 1;
|
||
appendGrid('userGrid', data.content, 'selectToy');
|
||
userPage++;
|
||
loadEl.style.display = 'none';
|
||
} while (userPage < userTotalPages && sentinelVisible('userSentinel'));
|
||
} catch (_) {
|
||
loadEl.textContent = 'Fehler beim Laden.';
|
||
} finally {
|
||
userLoading = false;
|
||
}
|
||
}
|
||
|
||
function reloadUserToys() {
|
||
userPage = 0;
|
||
userTotalPages = 1;
|
||
resetSelection();
|
||
document.getElementById('userGrid').innerHTML = '';
|
||
loadUserPage();
|
||
}
|
||
|
||
// ── Load system toys (append, füllt Viewport automatisch auf) ──
|
||
async function loadSystemPage() {
|
||
if (systemLoading || systemPage >= systemTotalPages) return;
|
||
systemLoading = true;
|
||
const loadEl = document.getElementById('systemLoading');
|
||
try {
|
||
do {
|
||
loadEl.textContent = 'Wird geladen…';
|
||
loadEl.style.display = 'block';
|
||
const r = await fetch(`/toy/list/system?page=${systemPage}&size=${PAGE_SIZE}`);
|
||
const data = await r.json();
|
||
systemTotalPages = data.totalPages || 1;
|
||
appendGrid('systemGrid', data.content, 'selectSystemToy');
|
||
systemPage++;
|
||
loadEl.style.display = 'none';
|
||
} while (systemPage < systemTotalPages && sentinelVisible('systemSentinel'));
|
||
} catch (_) {
|
||
loadEl.textContent = 'Fehler beim Laden.';
|
||
} finally {
|
||
systemLoading = false;
|
||
}
|
||
}
|
||
|
||
function reloadSystemToys() {
|
||
systemPage = 0;
|
||
systemTotalPages = 1;
|
||
resetSystemSelection();
|
||
document.getElementById('systemGrid').innerHTML = '';
|
||
loadSystemPage();
|
||
}
|
||
|
||
// ── Prüft ob ein Sentinel noch im (erweiterten) Viewport liegt ──
|
||
function sentinelVisible(id) {
|
||
const el = document.getElementById(id);
|
||
return el ? el.getBoundingClientRect().top <= window.innerHeight + 200 : false;
|
||
}
|
||
|
||
// ── Append items to a grid ──
|
||
function appendGrid(gridId, toys, selectFn) {
|
||
const grid = document.getElementById(gridId);
|
||
if (!toys || toys.length === 0) {
|
||
if (!grid.querySelector('.toy-card')) {
|
||
grid.innerHTML = '<p class="empty">Keine Einträge vorhanden.</p>';
|
||
}
|
||
return;
|
||
}
|
||
const emptyEl = grid.querySelector('.empty');
|
||
if (emptyEl) emptyEl.remove();
|
||
grid.insertAdjacentHTML('beforeend', toys.map(toy => `
|
||
<div class="toy-card" data-id="${esc(toy.toyId)}"
|
||
${selectFn ? `onclick="${selectFn}('${esc(toy.toyId)}')"` : ''}>
|
||
${toy.bild
|
||
? `<img class="toy-img" src="data:image/png;base64,${toy.bild}" alt="${esc(toy.name)}">`
|
||
: `<div class="toy-img-placeholder">◈</div>`}
|
||
<div class="toy-info">
|
||
<div class="toy-name">${esc(toy.name)}</div>
|
||
${toy.beschreibung ? `<div class="toy-desc">${esc(toy.beschreibung)}</div>` : ''}
|
||
</div>
|
||
</div>
|
||
`).join(''));
|
||
}
|
||
|
||
// ── Selection ──
|
||
let selectedUserToyId = null;
|
||
|
||
function selectToy(toyId) {
|
||
const prev = document.querySelector('#userGrid .toy-card.selected');
|
||
if (prev) prev.classList.remove('selected');
|
||
if (selectedUserToyId === toyId) {
|
||
selectedUserToyId = null;
|
||
} else {
|
||
selectedUserToyId = toyId;
|
||
document.querySelector(`#userGrid .toy-card[data-id="${toyId}"]`).classList.add('selected');
|
||
}
|
||
const has = selectedUserToyId != null;
|
||
document.getElementById('editBtn').disabled = !has;
|
||
document.getElementById('deleteBtn').disabled = !has;
|
||
document.getElementById('actionError').textContent = '';
|
||
}
|
||
|
||
function resetSelection() {
|
||
selectedUserToyId = null;
|
||
document.getElementById('editBtn').disabled = true;
|
||
document.getElementById('deleteBtn').disabled = true;
|
||
document.getElementById('actionError').textContent = '';
|
||
}
|
||
|
||
// ── System-Toy selection ──
|
||
let selectedSystemToyId = null;
|
||
|
||
function selectSystemToy(toyId) {
|
||
const prev = document.querySelector('#systemGrid .toy-card.selected');
|
||
if (prev) prev.classList.remove('selected');
|
||
if (selectedSystemToyId === toyId) {
|
||
selectedSystemToyId = null;
|
||
} else {
|
||
selectedSystemToyId = toyId;
|
||
document.querySelector(`#systemGrid .toy-card[data-id="${toyId}"]`).classList.add('selected');
|
||
}
|
||
document.getElementById('copyBtn').disabled = selectedSystemToyId == null;
|
||
document.getElementById('systemActionError').textContent = '';
|
||
}
|
||
|
||
function resetSystemSelection() {
|
||
selectedSystemToyId = null;
|
||
document.getElementById('copyBtn').disabled = true;
|
||
document.getElementById('systemActionError').textContent = '';
|
||
}
|
||
|
||
// ── Copy system toy ──
|
||
document.getElementById('copyBtn').addEventListener('click', () => {
|
||
if (!selectedSystemToyId) return;
|
||
const btn = document.getElementById('copyBtn');
|
||
btn.disabled = true;
|
||
fetch(`/toy/copy/${selectedSystemToyId}`, { method: 'POST' })
|
||
.then(r => {
|
||
if (r.ok || r.status === 201) {
|
||
reloadUserToys();
|
||
document.getElementById('systemActionError').textContent = '';
|
||
} else if (r.status === 409 && r.headers.get('X-Error') === 'duplicate-name') {
|
||
document.getElementById('systemActionError').textContent =
|
||
'Du hast bereits ein Toy mit diesem Namen.';
|
||
btn.disabled = false;
|
||
} else {
|
||
document.getElementById('systemActionError').textContent =
|
||
'Fehler beim Kopieren (HTTP ' + r.status + ').';
|
||
btn.disabled = false;
|
||
}
|
||
})
|
||
.catch(() => {
|
||
document.getElementById('systemActionError').textContent = 'Verbindungsfehler.';
|
||
btn.disabled = false;
|
||
});
|
||
});
|
||
|
||
// ── Header action buttons ──
|
||
document.getElementById('editBtn').addEventListener('click', () => {
|
||
if (selectedUserToyId) openModal(selectedUserToyId);
|
||
});
|
||
|
||
document.getElementById('deleteBtn').addEventListener('click', () => {
|
||
if (!selectedUserToyId) return;
|
||
if (!confirm('Toy wirklich löschen?')) return;
|
||
const btn = document.getElementById('deleteBtn');
|
||
btn.disabled = true;
|
||
const toyId = selectedUserToyId;
|
||
fetch(`/toy/${toyId}`, { method: 'DELETE' })
|
||
.then(r => {
|
||
if (r.status === 409) {
|
||
showActionError('Wird in Aufgaben verwendet – nicht löschbar.');
|
||
btn.disabled = false;
|
||
} else if (r.status === 403) {
|
||
showActionError('Keine Berechtigung.');
|
||
btn.disabled = false;
|
||
} else if (r.ok || r.status === 202) {
|
||
reloadUserToys();
|
||
} else {
|
||
showActionError('Fehler beim Löschen.');
|
||
btn.disabled = false;
|
||
}
|
||
})
|
||
.catch(() => { showActionError('Verbindungsfehler.'); btn.disabled = false; });
|
||
});
|
||
|
||
function showActionError(msg) {
|
||
const el = document.getElementById('actionError');
|
||
el.textContent = msg;
|
||
setTimeout(() => { if (el.textContent === msg) el.textContent = ''; }, 4000);
|
||
}
|
||
|
||
// ── Create / Edit modal ──
|
||
const modal = document.getElementById('createModal');
|
||
const saveBtn = document.getElementById('saveBtn');
|
||
let currentEditId = null;
|
||
|
||
function openModal(editId) {
|
||
currentEditId = editId || null;
|
||
document.getElementById('modalError').style.display = 'none';
|
||
document.getElementById('toyBild').value = '';
|
||
if (currentEditId) {
|
||
fetch(`/toy/${currentEditId}`)
|
||
.then(r => r.ok ? r.json() : null)
|
||
.then(toy => {
|
||
if (!toy) return;
|
||
document.getElementById('modalTitle').textContent = 'Toy bearbeiten';
|
||
document.getElementById('toyName').value = toy.name || '';
|
||
document.getElementById('toyDesc').value = toy.beschreibung || '';
|
||
const imgWrap = document.getElementById('currentImageWrap');
|
||
if (toy.bild) {
|
||
document.getElementById('currentImage').src = 'data:image/png;base64,' + toy.bild;
|
||
imgWrap.style.display = 'flex';
|
||
} else {
|
||
imgWrap.style.display = 'none';
|
||
}
|
||
modal.classList.add('open');
|
||
document.getElementById('toyName').focus();
|
||
})
|
||
.catch(() => alert('Fehler beim Laden des Toys.'));
|
||
} else {
|
||
document.getElementById('modalTitle').textContent = 'Neues Toy';
|
||
document.getElementById('toyName').value = '';
|
||
document.getElementById('toyDesc').value = '';
|
||
document.getElementById('currentImageWrap').style.display = 'none';
|
||
modal.classList.add('open');
|
||
document.getElementById('toyName').focus();
|
||
}
|
||
}
|
||
|
||
document.getElementById('openCreateBtn').addEventListener('click', () => openModal(null));
|
||
document.getElementById('cancelBtn').addEventListener('click', closeModal);
|
||
modal.addEventListener('click', e => { if (e.target === modal) closeModal(); });
|
||
|
||
function closeModal() { modal.classList.remove('open'); }
|
||
|
||
function editToy(toyId) { openModal(toyId); }
|
||
|
||
saveBtn.addEventListener('click', async () => {
|
||
const name = document.getElementById('toyName').value.trim();
|
||
if (!name) {
|
||
showModalError('Bitte einen Namen eingeben.');
|
||
return;
|
||
}
|
||
saveBtn.disabled = true;
|
||
saveBtn.textContent = 'Speichert…';
|
||
|
||
let bildBase64 = null;
|
||
const fileInput = document.getElementById('toyBild');
|
||
if (fileInput.files.length > 0) {
|
||
bildBase64 = await toBase64(fileInput.files[0]);
|
||
}
|
||
|
||
const payload = {
|
||
name,
|
||
beschreibung: document.getElementById('toyDesc').value.trim() || null,
|
||
bild: bildBase64
|
||
};
|
||
|
||
const isEdit = currentEditId != null;
|
||
fetch(isEdit ? `/toy/${currentEditId}` : '/toy', {
|
||
method: isEdit ? 'PUT' : 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(payload)
|
||
})
|
||
.then(r => {
|
||
if (r.ok || r.status === 201) {
|
||
closeModal();
|
||
reloadUserToys();
|
||
} else if (r.status === 409 && r.headers.get('X-Error') === 'duplicate-name') {
|
||
showModalError('Ein Toy mit diesem Namen existiert bereits.');
|
||
} else {
|
||
showModalError('Fehler beim Speichern (HTTP ' + r.status + ').');
|
||
}
|
||
})
|
||
.catch(() => showModalError('Verbindungsfehler.'))
|
||
.finally(() => { saveBtn.disabled = false; saveBtn.textContent = 'Speichern'; });
|
||
});
|
||
|
||
function showModalError(msg) {
|
||
const el = document.getElementById('modalError');
|
||
el.textContent = msg;
|
||
el.style.display = 'block';
|
||
}
|
||
|
||
function toBase64(file) {
|
||
const MAX = 128;
|
||
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;
|
||
});
|
||
}
|
||
|
||
// ── XSS-Schutz ──
|
||
function esc(str) {
|
||
if (str == null) return '';
|
||
return String(str).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||
}
|
||
|
||
</script>
|
||
</body>
|
||
</html>
|