All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 13s
240 lines
9.1 KiB
JavaScript
240 lines
9.1 KiB
JavaScript
const favoriteIcons = {
|
|
star: '<polygon points="12 2 15 8.5 22 9.3 16.8 14 18.2 21 12 17.4 5.8 21 7.2 14 2 9.3 9 8.5 12 2"></polygon>',
|
|
link: '<path d="M10 13a5 5 0 0 0 7.1 0l2.8-2.8a5 5 0 0 0-7.1-7.1l-1.6 1.6"></path><path d="M14 11a5 5 0 0 0-7.1 0l-2.8 2.8a5 5 0 0 0 7.1 7.1l1.6-1.6"></path>',
|
|
play: '<circle cx="12" cy="12" r="10"></circle><path d="m10 8 6 4-6 4z"></path>',
|
|
note: '<path d="M4 3h11l5 5v13H4z"></path><path d="M14 3v6h6"></path><path d="M8 13h8"></path><path d="M8 17h6"></path>',
|
|
book: '<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path>',
|
|
code: '<path d="m16 18 6-6-6-6"></path><path d="m8 6-6 6 6 6"></path>',
|
|
mail: '<rect x="3" y="5" width="18" height="14" rx="2"></rect><path d="m3 7 9 6 9-6"></path>',
|
|
calendar: '<rect x="3" y="4" width="18" height="18" rx="2"></rect><path d="M16 2v4"></path><path d="M8 2v4"></path><path d="M3 10h18"></path>',
|
|
image: '<rect x="3" y="3" width="18" height="18" rx="2"></rect><circle cx="8.5" cy="8.5" r="1.5"></circle><path d="m21 15-5-5L5 21"></path>',
|
|
tool: '<path d="M14.7 6.3a4 4 0 0 0-5 5L3 18v3h3l6.7-6.7a4 4 0 0 0 5-5l-2.8 2.8-2-2z"></path>'
|
|
};
|
|
|
|
const iconLabels = {
|
|
star: 'Stern',
|
|
link: 'Link',
|
|
play: 'Video',
|
|
note: 'Notiz',
|
|
book: 'Buch',
|
|
code: 'Code',
|
|
mail: 'Mail',
|
|
calendar: 'Kalender',
|
|
image: 'Bild',
|
|
tool: 'Tool'
|
|
};
|
|
|
|
const loginButton = document.querySelector('#favoritesLoginButton');
|
|
const logoutButton = document.querySelector('#favoritesLogoutButton');
|
|
const loginPanel = document.querySelector('#favoritesLoginPanel');
|
|
const panel = document.querySelector('#favoritesPanel');
|
|
const message = document.querySelector('#favoritesMessage');
|
|
const userBox = document.querySelector('#favoritesUser');
|
|
const editor = document.querySelector('#favoriteEditor');
|
|
const preview = document.querySelector('#favoritePreview');
|
|
const addButton = document.querySelector('#addFavoriteButton');
|
|
const saveButton = document.querySelector('#saveFavoritesButton');
|
|
const reloadButton = document.querySelector('#reloadFavoritesButton');
|
|
|
|
let favorites = [];
|
|
|
|
function escapeHtml(value) {
|
|
return String(value || '')
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
|
|
function createDefaultFavorite() {
|
|
return {
|
|
id: crypto.randomUUID(),
|
|
name: 'Neuer Link',
|
|
url: 'https://',
|
|
group: 'Favoriten',
|
|
description: '',
|
|
color: '#67e8f9',
|
|
icon: 'star',
|
|
logoUrl: '',
|
|
order: favorites.length
|
|
};
|
|
}
|
|
|
|
function iconOptions(selected) {
|
|
return Object.keys(favoriteIcons).map((icon) => (
|
|
`<option value="${icon}" ${icon === selected ? 'selected' : ''}>${iconLabels[icon]}</option>`
|
|
)).join('');
|
|
}
|
|
|
|
function syncFromInputs() {
|
|
editor.querySelectorAll('.favorite-edit-card').forEach((row) => {
|
|
const favorite = favorites[Number(row.dataset.index)];
|
|
if (!favorite) return;
|
|
row.querySelectorAll('[data-field]').forEach((input) => {
|
|
favorite[input.dataset.field] = input.value;
|
|
});
|
|
});
|
|
}
|
|
|
|
function moveFavorite(index, direction) {
|
|
const next = index + direction;
|
|
if (next < 0 || next >= favorites.length) return;
|
|
syncFromInputs();
|
|
[favorites[index], favorites[next]] = [favorites[next], favorites[index]];
|
|
favorites = favorites.map((favorite, order) => ({ ...favorite, order }));
|
|
render();
|
|
}
|
|
|
|
function removeFavorite(index) {
|
|
syncFromInputs();
|
|
favorites.splice(index, 1);
|
|
favorites = favorites.map((favorite, order) => ({ ...favorite, order }));
|
|
render();
|
|
}
|
|
|
|
function renderPreviewCard(favorite) {
|
|
const color = /^#[0-9a-fA-F]{6}$/.test(favorite.color) ? favorite.color : '#67e8f9';
|
|
const iconSvg = favoriteIcons[favorite.icon] || favoriteIcons.star;
|
|
let host = '';
|
|
try {
|
|
host = new URL(favorite.url).hostname;
|
|
} catch {
|
|
host = 'ungueltige-url';
|
|
}
|
|
|
|
return `
|
|
<article class="card favorite-card" style="--accent: ${color};">
|
|
<div class="card-icon favorite-icon" style="--accent: ${color};">
|
|
${favorite.logoUrl
|
|
? `<img src="${escapeHtml(favorite.logoUrl)}" alt="" loading="lazy">`
|
|
: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${iconSvg}</svg>`}
|
|
</div>
|
|
<div class="card-content">
|
|
<span class="card-category">${escapeHtml(favorite.group || 'Favorit')}</span>
|
|
<h3>${escapeHtml(favorite.name)}</h3>
|
|
<p>${escapeHtml(favorite.description || 'Eigener Weblink')}</p>
|
|
</div>
|
|
<span class="card-domain">${escapeHtml(host)}</span>
|
|
</article>
|
|
`;
|
|
}
|
|
|
|
function renderPreview() {
|
|
preview.innerHTML = favorites.map(renderPreviewCard).join('') || '<div class="favorites-empty">Die Vorschau erscheint hier.</div>';
|
|
}
|
|
|
|
function render() {
|
|
editor.innerHTML = favorites.map((favorite, index) => `
|
|
<article class="favorite-edit-card" data-index="${index}">
|
|
<div class="favorite-edit-head">
|
|
<strong>${escapeHtml(favorite.name || 'Neuer Link')}</strong>
|
|
<div class="favorite-order-actions">
|
|
<button class="ghost-button mini" data-action="up" type="button" ${index === 0 ? 'disabled' : ''}>Hoch</button>
|
|
<button class="ghost-button mini" data-action="down" type="button" ${index === favorites.length - 1 ? 'disabled' : ''}>Runter</button>
|
|
<button class="ghost-button mini danger-button" data-action="remove" type="button">Entfernen</button>
|
|
</div>
|
|
</div>
|
|
<div class="favorite-form-grid">
|
|
<label>Beschriftung<input data-field="name" value="${escapeHtml(favorite.name)}" placeholder="YouTube"></label>
|
|
<label>Link<input data-field="url" value="${escapeHtml(favorite.url)}" placeholder="https://youtube.com"></label>
|
|
<label>Gruppe<input data-field="group" value="${escapeHtml(favorite.group)}" placeholder="Privat"></label>
|
|
<label>Beschreibung<input data-field="description" value="${escapeHtml(favorite.description)}" placeholder="Optionaler Untertitel"></label>
|
|
<label>Farbe<input data-field="color" type="color" value="${escapeHtml(favorite.color || '#67e8f9')}"></label>
|
|
<label>Icon<select data-field="icon">${iconOptions(favorite.icon || 'star')}</select></label>
|
|
<label class="favorite-form-wide">Logo URL<input data-field="logoUrl" value="${escapeHtml(favorite.logoUrl)}" placeholder="https://.../logo.png"></label>
|
|
</div>
|
|
</article>
|
|
`).join('') || '<div class="favorites-empty">Noch keine Favoriten angelegt.</div>';
|
|
|
|
renderPreview();
|
|
|
|
editor.querySelectorAll('[data-field]').forEach((input) => {
|
|
input.addEventListener('input', () => {
|
|
syncFromInputs();
|
|
renderPreview();
|
|
});
|
|
});
|
|
|
|
editor.querySelectorAll('[data-action]').forEach((button) => {
|
|
button.addEventListener('click', () => {
|
|
const index = Number(button.closest('.favorite-edit-card').dataset.index);
|
|
if (button.dataset.action === 'up') moveFavorite(index, -1);
|
|
if (button.dataset.action === 'down') moveFavorite(index, 1);
|
|
if (button.dataset.action === 'remove') removeFavorite(index);
|
|
});
|
|
});
|
|
}
|
|
|
|
async function loadFavorites() {
|
|
const response = await fetch('/api/favorites', {
|
|
cache: 'no-store',
|
|
headers: MischLabsAuth.authHeader()
|
|
});
|
|
const payload = await response.json().catch(() => ({}));
|
|
if (!response.ok) throw new Error(payload.error || `HTTP ${response.status}`);
|
|
favorites = Array.isArray(payload.favorites) ? payload.favorites : [];
|
|
userBox.textContent = `Angemeldet als ${payload.user?.name || MischLabsAuth.displayName()}`;
|
|
render();
|
|
}
|
|
|
|
async function saveFavorites() {
|
|
syncFromInputs();
|
|
const response = await fetch('/api/favorites', {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
...MischLabsAuth.authHeader()
|
|
},
|
|
body: JSON.stringify({ favorites })
|
|
});
|
|
const payload = await response.json().catch(() => ({}));
|
|
if (!response.ok) throw new Error(payload.error || `HTTP ${response.status}`);
|
|
favorites = payload.favorites || [];
|
|
render();
|
|
userBox.textContent = 'Favoriten gespeichert.';
|
|
}
|
|
|
|
function updateAuthUi() {
|
|
const authenticated = MischLabsAuth.isAuthenticated();
|
|
loginButton.hidden = authenticated;
|
|
logoutButton.hidden = !authenticated;
|
|
loginPanel.hidden = authenticated;
|
|
panel.hidden = !authenticated;
|
|
}
|
|
|
|
async function boot() {
|
|
try {
|
|
await MischLabsAuth.handleCallback();
|
|
} catch (error) {
|
|
message.textContent = error.message;
|
|
}
|
|
|
|
updateAuthUi();
|
|
if (MischLabsAuth.isAuthenticated()) {
|
|
await loadFavorites();
|
|
}
|
|
}
|
|
|
|
loginButton.addEventListener('click', () => MischLabsAuth.login());
|
|
logoutButton.addEventListener('click', () => MischLabsAuth.logout());
|
|
addButton.addEventListener('click', () => {
|
|
syncFromInputs();
|
|
favorites.push(createDefaultFavorite());
|
|
render();
|
|
});
|
|
saveButton.addEventListener('click', () => {
|
|
saveFavorites().catch((error) => {
|
|
userBox.textContent = `Speichern fehlgeschlagen: ${error.message}`;
|
|
});
|
|
});
|
|
reloadButton.addEventListener('click', () => {
|
|
loadFavorites().catch((error) => {
|
|
userBox.textContent = `Laden fehlgeschlagen: ${error.message}`;
|
|
});
|
|
});
|
|
|
|
boot().catch((error) => {
|
|
message.textContent = `Favoritenseite konnte nicht starten: ${error.message}`;
|
|
});
|