bookmarks restyling
This commit is contained in:
517
web/app.js
517
web/app.js
@@ -101,10 +101,8 @@ const SORT_SETTINGS_LEGACY_KEY = 'trackerSortMode';
|
||||
const FACEBOOK_TRACKING_PARAMS = ['__cft__', '__tn__', '__eep__', 'mibextid', 'set', 'comment_id', 'hoisted_section_header_type'];
|
||||
const VALID_SORT_MODES = new Set(['created', 'deadline', 'smart', 'lastCheck', 'lastChange']);
|
||||
const DEFAULT_SORT_SETTINGS = { mode: 'created', direction: 'desc' };
|
||||
const BOOKMARKS_STORAGE_KEY = 'trackerSearchBookmarks';
|
||||
const BOOKMARKS_BASE_URL = 'https://www.facebook.com/search/top';
|
||||
const BOOKMARK_WINDOW_DAYS = 28;
|
||||
const DEFAULT_BOOKMARKS = [];
|
||||
const BOOKMARK_SUFFIXES = ['Gewinnspiel', 'gewinnen', 'verlosen'];
|
||||
|
||||
function initializeFocusParams() {
|
||||
@@ -279,53 +277,202 @@ function persistSortStorage(storage) {
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeCustomBookmark(entry) {
|
||||
function normalizeServerBookmark(entry) {
|
||||
if (!entry || typeof entry !== 'object') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const id = typeof entry.id === 'string' ? entry.id : null;
|
||||
const query = typeof entry.query === 'string' ? entry.query.trim() : '';
|
||||
if (!query) {
|
||||
if (!id || !query) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const label = typeof entry.label === 'string' && entry.label.trim() ? entry.label.trim() : query;
|
||||
const id = typeof entry.id === 'string' && entry.id ? entry.id : `custom-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
|
||||
|
||||
const normalizeDate = (value) => {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return null;
|
||||
}
|
||||
return date.toISOString();
|
||||
};
|
||||
|
||||
return {
|
||||
id,
|
||||
label,
|
||||
query,
|
||||
type: 'custom'
|
||||
created_at: normalizeDate(entry.created_at),
|
||||
updated_at: normalizeDate(entry.updated_at),
|
||||
last_clicked_at: normalizeDate(entry.last_clicked_at),
|
||||
deletable: true
|
||||
};
|
||||
}
|
||||
|
||||
function loadCustomBookmarks() {
|
||||
try {
|
||||
const raw = localStorage.getItem(BOOKMARKS_STORAGE_KEY);
|
||||
if (!raw) {
|
||||
return [];
|
||||
function deduplicateBookmarks(list) {
|
||||
const seen = new Set();
|
||||
const deduped = [];
|
||||
|
||||
list.forEach((bookmark) => {
|
||||
if (!bookmark || !bookmark.query) {
|
||||
return;
|
||||
}
|
||||
const parsed = JSON.parse(raw);
|
||||
if (!Array.isArray(parsed)) {
|
||||
return [];
|
||||
const key = bookmark.query.toLowerCase();
|
||||
if (seen.has(key)) {
|
||||
return;
|
||||
}
|
||||
return parsed.map(normalizeCustomBookmark).filter(Boolean);
|
||||
} catch (error) {
|
||||
console.warn('Konnte Bookmarks nicht laden:', error);
|
||||
return [];
|
||||
}
|
||||
seen.add(key);
|
||||
deduped.push(bookmark);
|
||||
});
|
||||
|
||||
return deduped;
|
||||
}
|
||||
|
||||
function saveCustomBookmarks(bookmarks) {
|
||||
try {
|
||||
const sanitized = Array.isArray(bookmarks)
|
||||
? bookmarks.map(normalizeCustomBookmark).filter(Boolean)
|
||||
: [];
|
||||
localStorage.setItem(BOOKMARKS_STORAGE_KEY, JSON.stringify(sanitized));
|
||||
} catch (error) {
|
||||
console.warn('Konnte Bookmarks nicht speichern:', error);
|
||||
function sortBookmarksByRecency(list) {
|
||||
return [...list].sort((a, b) => {
|
||||
const aClick = a.last_clicked_at ? new Date(a.last_clicked_at).getTime() : -Infinity;
|
||||
const bClick = b.last_clicked_at ? new Date(b.last_clicked_at).getTime() : -Infinity;
|
||||
if (aClick !== bClick) {
|
||||
return bClick - aClick;
|
||||
}
|
||||
|
||||
const aCreated = a.created_at ? new Date(a.created_at).getTime() : -Infinity;
|
||||
const bCreated = b.created_at ? new Date(b.created_at).getTime() : -Infinity;
|
||||
if (aCreated !== bCreated) {
|
||||
return bCreated - aCreated;
|
||||
}
|
||||
|
||||
return a.label.localeCompare(b.label, 'de', { sensitivity: 'base' });
|
||||
});
|
||||
}
|
||||
|
||||
const bookmarkState = {
|
||||
items: [],
|
||||
loaded: false,
|
||||
loading: false,
|
||||
error: null
|
||||
};
|
||||
|
||||
let bookmarkFetchPromise = null;
|
||||
|
||||
function formatRelativeTimeFromNow(timestamp) {
|
||||
if (!timestamp) {
|
||||
return 'Noch nie geöffnet';
|
||||
}
|
||||
|
||||
const date = new Date(timestamp);
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return 'Zuletzt: unbekannt';
|
||||
}
|
||||
|
||||
const diffMs = Date.now() - date.getTime();
|
||||
if (diffMs < 0) {
|
||||
return 'gerade eben';
|
||||
}
|
||||
const diffSeconds = Math.floor(diffMs / 1000);
|
||||
if (diffSeconds < 45) {
|
||||
return 'vor wenigen Sekunden';
|
||||
}
|
||||
const diffMinutes = Math.floor(diffSeconds / 60);
|
||||
if (diffMinutes < 60) {
|
||||
return `vor ${diffMinutes} ${diffMinutes === 1 ? 'Minute' : 'Minuten'}`;
|
||||
}
|
||||
const diffHours = Math.floor(diffMinutes / 60);
|
||||
if (diffHours < 24) {
|
||||
return `vor ${diffHours} ${diffHours === 1 ? 'Stunde' : 'Stunden'}`;
|
||||
}
|
||||
const diffDays = Math.floor(diffHours / 24);
|
||||
if (diffDays < 31) {
|
||||
return `vor ${diffDays} ${diffDays === 1 ? 'Tag' : 'Tagen'}`;
|
||||
}
|
||||
const diffMonths = Math.floor(diffDays / 30);
|
||||
if (diffMonths < 12) {
|
||||
return `vor ${diffMonths} ${diffMonths === 1 ? 'Monat' : 'Monaten'}`;
|
||||
}
|
||||
const diffYears = Math.floor(diffMonths / 12);
|
||||
return `vor ${diffYears} ${diffYears === 1 ? 'Jahr' : 'Jahren'}`;
|
||||
}
|
||||
|
||||
function upsertBookmarkInState(bookmark) {
|
||||
const normalized = normalizeServerBookmark(bookmark);
|
||||
if (!normalized) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lowerQuery = normalized.query.toLowerCase();
|
||||
const existingIndex = bookmarkState.items.findIndex((item) => {
|
||||
if (!item || !item.query) {
|
||||
return false;
|
||||
}
|
||||
return item.id === normalized.id || item.query.toLowerCase() === lowerQuery;
|
||||
});
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
bookmarkState.items[existingIndex] = { ...bookmarkState.items[existingIndex], ...normalized };
|
||||
} else {
|
||||
bookmarkState.items.push(normalized);
|
||||
}
|
||||
|
||||
bookmarkState.items = deduplicateBookmarks(sortBookmarksByRecency(bookmarkState.items));
|
||||
}
|
||||
|
||||
function removeBookmarkFromState(bookmarkId) {
|
||||
if (!bookmarkId) {
|
||||
return;
|
||||
}
|
||||
bookmarkState.items = bookmarkState.items.filter((bookmark) => bookmark.id !== bookmarkId);
|
||||
}
|
||||
|
||||
async function refreshBookmarks(options = {}) {
|
||||
const { force = false } = options;
|
||||
if (bookmarkFetchPromise && !force) {
|
||||
return bookmarkFetchPromise;
|
||||
}
|
||||
|
||||
bookmarkFetchPromise = (async () => {
|
||||
bookmarkState.loading = true;
|
||||
if (!bookmarkState.loaded || force) {
|
||||
renderBookmarks();
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await apiFetch(`${API_URL}/bookmarks`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const normalized = Array.isArray(data)
|
||||
? data.map(normalizeServerBookmark).filter(Boolean)
|
||||
: [];
|
||||
|
||||
const finalList = deduplicateBookmarks(sortBookmarksByRecency(normalized));
|
||||
|
||||
bookmarkState.items = finalList;
|
||||
bookmarkState.loaded = true;
|
||||
bookmarkState.loading = false;
|
||||
bookmarkState.error = null;
|
||||
renderBookmarks();
|
||||
|
||||
return bookmarkState.items;
|
||||
} catch (error) {
|
||||
console.warn('Konnte Bookmarks nicht laden:', error);
|
||||
bookmarkState.error = 'Bookmarks konnten nicht geladen werden.';
|
||||
bookmarkState.loading = false;
|
||||
if (!bookmarkState.loaded) {
|
||||
bookmarkState.items = [];
|
||||
}
|
||||
renderBookmarks();
|
||||
throw error;
|
||||
} finally {
|
||||
bookmarkFetchPromise = null;
|
||||
}
|
||||
})();
|
||||
|
||||
return bookmarkFetchPromise;
|
||||
}
|
||||
|
||||
function formatFacebookDateParts(date) {
|
||||
@@ -457,6 +604,22 @@ function openBookmark(bookmark) {
|
||||
queries.push('');
|
||||
}
|
||||
|
||||
const stateBookmark = bookmarkState.items.find((item) => item.id === bookmark.id) || bookmark;
|
||||
|
||||
if (stateBookmark && stateBookmark.id && stateBookmark.deletable !== false) {
|
||||
const nowIso = new Date().toISOString();
|
||||
upsertBookmarkInState({
|
||||
id: stateBookmark.id,
|
||||
label: stateBookmark.label,
|
||||
query: stateBookmark.query,
|
||||
last_clicked_at: nowIso,
|
||||
created_at: stateBookmark.created_at || nowIso,
|
||||
updated_at: nowIso
|
||||
});
|
||||
renderBookmarks();
|
||||
markBookmarkClick(stateBookmark.id);
|
||||
}
|
||||
|
||||
queries.forEach((searchTerm) => {
|
||||
const url = buildBookmarkSearchUrl(searchTerm);
|
||||
if (url) {
|
||||
@@ -465,18 +628,114 @@ function openBookmark(bookmark) {
|
||||
});
|
||||
}
|
||||
|
||||
function removeBookmark(bookmarkId) {
|
||||
async function markBookmarkClick(bookmarkId) {
|
||||
if (!bookmarkId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const current = loadCustomBookmarks();
|
||||
const next = current.filter((bookmark) => bookmark.id !== bookmarkId);
|
||||
if (next.length === current.length) {
|
||||
try {
|
||||
const response = await apiFetch(`${API_URL}/bookmarks/${encodeURIComponent(bookmarkId)}/click`, {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updated = await response.json();
|
||||
upsertBookmarkInState(updated);
|
||||
renderBookmarks();
|
||||
} catch (error) {
|
||||
console.warn('Konnte Bookmark-Klick nicht speichern:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function removeBookmark(bookmarkId) {
|
||||
if (!bookmarkId) {
|
||||
return;
|
||||
}
|
||||
saveCustomBookmarks(next);
|
||||
renderBookmarks();
|
||||
|
||||
try {
|
||||
const response = await apiFetch(`${API_URL}/bookmarks/${encodeURIComponent(bookmarkId)}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (response.ok || response.status === 204 || response.status === 404) {
|
||||
removeBookmarkFromState(bookmarkId);
|
||||
bookmarkState.error = null;
|
||||
renderBookmarks();
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
} catch (error) {
|
||||
console.warn('Konnte Bookmark nicht löschen:', error);
|
||||
bookmarkState.error = 'Bookmark konnte nicht gelöscht werden.';
|
||||
renderBookmarks();
|
||||
}
|
||||
}
|
||||
|
||||
function createBookmarkRow(bookmark) {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'bookmark-row';
|
||||
row.dataset.query = bookmark.query || '';
|
||||
|
||||
if (!bookmark.last_clicked_at) {
|
||||
row.dataset.state = 'never-used';
|
||||
}
|
||||
if (bookmark.isDefault) {
|
||||
row.dataset.default = '1';
|
||||
}
|
||||
|
||||
const openButton = document.createElement('button');
|
||||
openButton.type = 'button';
|
||||
openButton.className = 'bookmark-row__open';
|
||||
|
||||
const searchVariants = buildBookmarkSearchQueries(bookmark.query);
|
||||
if (searchVariants.length) {
|
||||
openButton.title = searchVariants.map((variant) => `• ${variant}`).join('\n');
|
||||
}
|
||||
|
||||
openButton.addEventListener('click', () => openBookmark(bookmark));
|
||||
|
||||
const label = document.createElement('span');
|
||||
label.className = 'bookmark-row__label';
|
||||
label.textContent = bookmark.label || bookmark.query || 'Bookmark';
|
||||
openButton.appendChild(label);
|
||||
|
||||
const query = document.createElement('span');
|
||||
query.className = 'bookmark-row__query';
|
||||
query.textContent = bookmark.query ? `„${bookmark.query}“` : 'Standard-Keywords';
|
||||
openButton.appendChild(query);
|
||||
|
||||
row.appendChild(openButton);
|
||||
|
||||
const meta = document.createElement('span');
|
||||
meta.className = 'bookmark-row__meta';
|
||||
meta.textContent = formatRelativeTimeFromNow(bookmark.last_clicked_at);
|
||||
if (bookmark.last_clicked_at) {
|
||||
const date = new Date(bookmark.last_clicked_at);
|
||||
if (!Number.isNaN(date.getTime())) {
|
||||
meta.title = date.toLocaleString();
|
||||
}
|
||||
}
|
||||
row.appendChild(meta);
|
||||
|
||||
if (bookmark.deletable !== false) {
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.type = 'button';
|
||||
removeBtn.className = 'bookmark-row__remove';
|
||||
removeBtn.setAttribute('aria-label', `${bookmark.label || bookmark.query} entfernen`);
|
||||
removeBtn.textContent = '×';
|
||||
removeBtn.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
removeBookmark(bookmark.id);
|
||||
});
|
||||
row.appendChild(removeBtn);
|
||||
}
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
function renderBookmarks() {
|
||||
@@ -486,62 +745,97 @@ function renderBookmarks() {
|
||||
|
||||
bookmarksList.innerHTML = '';
|
||||
|
||||
const items = [...DEFAULT_BOOKMARKS, ...loadCustomBookmarks()];
|
||||
|
||||
const staticDefault = {
|
||||
id: 'default-empty',
|
||||
label: 'Gewinnspiel / gewinnen / verlosen',
|
||||
query: '',
|
||||
type: 'default'
|
||||
};
|
||||
|
||||
items.unshift(staticDefault);
|
||||
|
||||
if (!items.length) {
|
||||
const empty = document.createElement('div');
|
||||
empty.className = 'bookmark-empty';
|
||||
empty.textContent = 'Noch keine Bookmarks vorhanden.';
|
||||
empty.setAttribute('role', 'listitem');
|
||||
bookmarksList.appendChild(empty);
|
||||
if (bookmarkState.loading && !bookmarkState.loaded) {
|
||||
const loading = document.createElement('div');
|
||||
loading.className = 'bookmark-status bookmark-status--loading';
|
||||
loading.textContent = 'Lade Bookmarks...';
|
||||
bookmarksList.appendChild(loading);
|
||||
return;
|
||||
}
|
||||
|
||||
items.forEach((bookmark) => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'bookmark-item';
|
||||
item.setAttribute('role', 'listitem');
|
||||
if (bookmarkState.error && !bookmarkState.loaded) {
|
||||
const errorNode = document.createElement('div');
|
||||
errorNode.className = 'bookmark-status bookmark-status--error';
|
||||
errorNode.textContent = bookmarkState.error;
|
||||
bookmarksList.appendChild(errorNode);
|
||||
return;
|
||||
}
|
||||
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.className = 'bookmark-button';
|
||||
const label = bookmark.label || bookmark.query;
|
||||
button.textContent = label;
|
||||
if (bookmarkState.error && bookmarkState.loaded) {
|
||||
const warnNode = document.createElement('div');
|
||||
warnNode.className = 'bookmark-status bookmark-status--error';
|
||||
warnNode.textContent = bookmarkState.error;
|
||||
bookmarksList.appendChild(warnNode);
|
||||
}
|
||||
|
||||
const searchVariants = buildBookmarkSearchQueries(bookmark.query);
|
||||
if (searchVariants.length) {
|
||||
button.title = searchVariants.map((variant) => `• ${variant}`).join('\n');
|
||||
} else {
|
||||
button.title = `Suche nach "${bookmark.query}" (letzte 4 Wochen)`;
|
||||
const dynamicBookmarks = bookmarkState.items;
|
||||
|
||||
const staticDefault = {
|
||||
id: 'default-search',
|
||||
label: 'Gewinnspiel / gewinnen / verlosen',
|
||||
query: '',
|
||||
last_clicked_at: null,
|
||||
deletable: false,
|
||||
isDefault: true
|
||||
};
|
||||
|
||||
const sorted = sortBookmarksByRecency(dynamicBookmarks);
|
||||
const recent = [];
|
||||
const RECENT_LIMIT = 5;
|
||||
|
||||
sorted.forEach((bookmark) => {
|
||||
if (bookmark.last_clicked_at && recent.length < RECENT_LIMIT) {
|
||||
recent.push(bookmark);
|
||||
}
|
||||
button.addEventListener('click', () => openBookmark(bookmark));
|
||||
});
|
||||
|
||||
item.appendChild(button);
|
||||
const alphabeticalAll = [...dynamicBookmarks]
|
||||
.sort((a, b) => a.label.localeCompare(b.label, 'de', { sensitivity: 'base' }));
|
||||
|
||||
if (bookmark.type === 'custom') {
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.type = 'button';
|
||||
removeBtn.className = 'bookmark-remove-btn';
|
||||
removeBtn.setAttribute('aria-label', `${bookmark.label || bookmark.query} entfernen`);
|
||||
removeBtn.textContent = '×';
|
||||
removeBtn.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
removeBookmark(bookmark.id);
|
||||
});
|
||||
item.appendChild(removeBtn);
|
||||
const sections = [];
|
||||
|
||||
if (recent.length) {
|
||||
sections.push({
|
||||
id: 'recent',
|
||||
title: 'Zuletzt verwendet',
|
||||
items: recent
|
||||
});
|
||||
}
|
||||
|
||||
sections.push({
|
||||
id: 'all',
|
||||
title: 'Alle Bookmarks',
|
||||
items: [staticDefault, ...alphabeticalAll]
|
||||
});
|
||||
|
||||
sections.forEach((section) => {
|
||||
if (!section.items.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
bookmarksList.appendChild(item);
|
||||
const sectionElement = document.createElement('section');
|
||||
sectionElement.className = 'bookmark-section';
|
||||
sectionElement.dataset.section = section.id;
|
||||
|
||||
const header = document.createElement('header');
|
||||
header.className = 'bookmark-section__header';
|
||||
|
||||
const title = document.createElement('h3');
|
||||
title.className = 'bookmark-section__title';
|
||||
title.textContent = section.title;
|
||||
header.appendChild(title);
|
||||
|
||||
sectionElement.appendChild(header);
|
||||
|
||||
const list = document.createElement('div');
|
||||
list.className = 'bookmark-section__list';
|
||||
|
||||
section.items.forEach((bookmark) => {
|
||||
list.appendChild(createBookmarkRow(bookmark));
|
||||
});
|
||||
|
||||
sectionElement.appendChild(list);
|
||||
bookmarksList.appendChild(sectionElement);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -604,7 +898,13 @@ function toggleBookmarkPanel(forceVisible) {
|
||||
bookmarkPanelToggle.setAttribute('aria-expanded', bookmarkPanelVisible ? 'true' : 'false');
|
||||
|
||||
if (bookmarkPanelVisible) {
|
||||
renderBookmarks();
|
||||
if (!bookmarkState.loaded && !bookmarkState.loading) {
|
||||
bookmarkState.loading = true;
|
||||
renderBookmarks();
|
||||
refreshBookmarks().catch(() => {});
|
||||
} else {
|
||||
renderBookmarks();
|
||||
}
|
||||
resetBookmarkForm();
|
||||
if (bookmarkQueryInput) {
|
||||
window.requestAnimationFrame(() => {
|
||||
@@ -619,13 +919,14 @@ function toggleBookmarkPanel(forceVisible) {
|
||||
} else {
|
||||
resetBookmarkForm();
|
||||
removeBookmarkOutsideHandler();
|
||||
bookmarkState.error = null;
|
||||
if (bookmarkPanelToggle) {
|
||||
bookmarkPanelToggle.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleBookmarkSubmit(event) {
|
||||
async function handleBookmarkSubmit(event) {
|
||||
event.preventDefault();
|
||||
|
||||
if (!bookmarkForm) {
|
||||
@@ -643,26 +944,44 @@ function handleBookmarkSubmit(event) {
|
||||
return;
|
||||
}
|
||||
|
||||
const customBookmarks = loadCustomBookmarks();
|
||||
const normalizedQuery = query.toLowerCase();
|
||||
const existingIndex = customBookmarks.findIndex((bookmark) => bookmark.query.toLowerCase() === normalizedQuery);
|
||||
bookmarkState.error = null;
|
||||
|
||||
const nextBookmark = {
|
||||
id: existingIndex >= 0 ? customBookmarks[existingIndex].id : `custom-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`,
|
||||
label: name || query,
|
||||
query,
|
||||
type: 'custom'
|
||||
};
|
||||
try {
|
||||
const response = await apiFetch(`${API_URL}/bookmarks`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
label: name,
|
||||
query
|
||||
})
|
||||
});
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
customBookmarks[existingIndex] = nextBookmark;
|
||||
} else {
|
||||
customBookmarks.push(nextBookmark);
|
||||
if (response.status === 409) {
|
||||
bookmarkState.error = 'Bookmark existiert bereits.';
|
||||
renderBookmarks();
|
||||
resetBookmarkForm();
|
||||
if (bookmarkQueryInput) {
|
||||
bookmarkQueryInput.focus();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const created = await response.json();
|
||||
upsertBookmarkInState(created);
|
||||
bookmarkState.error = null;
|
||||
renderBookmarks();
|
||||
resetBookmarkForm();
|
||||
} catch (error) {
|
||||
console.warn('Konnte Bookmark nicht speichern:', error);
|
||||
bookmarkState.error = 'Bookmark konnte nicht gespeichert werden.';
|
||||
renderBookmarks();
|
||||
}
|
||||
|
||||
saveCustomBookmarks(customBookmarks);
|
||||
renderBookmarks();
|
||||
resetBookmarkForm();
|
||||
}
|
||||
|
||||
function initializeBookmarks() {
|
||||
@@ -670,7 +989,7 @@ function initializeBookmarks() {
|
||||
return;
|
||||
}
|
||||
|
||||
renderBookmarks();
|
||||
refreshBookmarks().catch(() => {});
|
||||
|
||||
if (bookmarkPanel) {
|
||||
bookmarkPanel.setAttribute('aria-hidden', 'true');
|
||||
|
||||
181
web/style.css
181
web/style.css
@@ -1109,13 +1109,16 @@ h1 {
|
||||
position: absolute;
|
||||
top: calc(100% + 8px);
|
||||
right: 0;
|
||||
width: min(420px, 90vw);
|
||||
width: min(540px, 92vw);
|
||||
max-height: 70vh;
|
||||
background: #ffffff;
|
||||
border-radius: 10px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 18px 45px rgba(15, 23, 42, 0.18);
|
||||
padding: 16px;
|
||||
z-index: 20;
|
||||
border: 1px solid rgba(229, 231, 235, 0.8);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.bookmark-panel__header {
|
||||
@@ -1124,11 +1127,13 @@ h1 {
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
border-bottom: 1px solid rgba(229, 231, 235, 0.8);
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.bookmark-panel__title {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@@ -1148,62 +1153,136 @@ h1 {
|
||||
}
|
||||
|
||||
.bookmark-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding-right: 6px;
|
||||
}
|
||||
|
||||
.bookmark-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.bookmark-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 10px;
|
||||
background: #f0f2f5;
|
||||
border-radius: 999px;
|
||||
.bookmark-section__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.bookmark-button {
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #1d2129;
|
||||
.bookmark-section__title {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.bookmark-section__hint {
|
||||
font-size: 11px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.bookmark-section__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.bookmark-button:hover,
|
||||
.bookmark-button:focus {
|
||||
text-decoration: underline;
|
||||
.bookmark-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
background: #f3f4f6;
|
||||
border-radius: 8px;
|
||||
padding: 7px 10px;
|
||||
}
|
||||
|
||||
.bookmark-button:focus-visible {
|
||||
outline: 2px solid #2563eb;
|
||||
outline-offset: 2px;
|
||||
.bookmark-row[data-state="never-used"] {
|
||||
background: #eef2ff;
|
||||
}
|
||||
|
||||
.bookmark-remove-btn {
|
||||
.bookmark-row__open {
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #7f8186;
|
||||
text-align: left;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
cursor: pointer;
|
||||
color: inherit;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.bookmark-row__open:focus-visible {
|
||||
outline: 2px solid #2563eb;
|
||||
outline-offset: 3px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.bookmark-row__label {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.bookmark-row__query {
|
||||
font-size: 11px;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.bookmark-row__meta {
|
||||
font-size: 11px;
|
||||
color: #6b7280;
|
||||
white-space: nowrap;
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
.bookmark-row__remove {
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #9ca3af;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
padding: 0 2px;
|
||||
padding: 0 6px;
|
||||
border-radius: 6px;
|
||||
transition: background-color 0.2s ease, color 0.2s ease;
|
||||
}
|
||||
|
||||
.bookmark-remove-btn:hover,
|
||||
.bookmark-remove-btn:focus {
|
||||
color: #c0392b;
|
||||
.bookmark-row__remove:hover,
|
||||
.bookmark-row__remove:focus-visible {
|
||||
color: #ef4444;
|
||||
background: rgba(239, 68, 68, 0.12);
|
||||
}
|
||||
|
||||
.bookmark-status {
|
||||
font-size: 13px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
background: #f3f4f6;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.bookmark-status--error {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
border: 1px solid rgba(248, 113, 113, 0.4);
|
||||
}
|
||||
|
||||
.bookmark-status--loading {
|
||||
background: #ede9fe;
|
||||
color: #4c1d95;
|
||||
}
|
||||
|
||||
.bookmark-form {
|
||||
margin-top: 12px;
|
||||
border-top: 1px solid #e4e6eb;
|
||||
padding-top: 12px;
|
||||
margin-top: 16px;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
padding-top: 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
@@ -1246,11 +1325,27 @@ h1 {
|
||||
}
|
||||
|
||||
.bookmark-empty {
|
||||
font-size: 14px;
|
||||
color: #65676b;
|
||||
background: #f0f2f5;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
font-size: 13px;
|
||||
color: #4b5563;
|
||||
background: #f3f4f6;
|
||||
border-radius: 10px;
|
||||
padding: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.bookmark-panel {
|
||||
width: min(480px, 94vw);
|
||||
max-height: 75vh;
|
||||
}
|
||||
|
||||
.bookmark-section__list {
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.bookmark-row {
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
}
|
||||
}
|
||||
|
||||
.screenshot-modal {
|
||||
@@ -1434,6 +1529,12 @@ h1 {
|
||||
position: static;
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
max-height: none;
|
||||
box-shadow: 0 12px 30px rgba(15, 23, 42, 0.12);
|
||||
}
|
||||
|
||||
.bookmark-list {
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
.bookmark-form__actions {
|
||||
|
||||
Reference in New Issue
Block a user