reworked site
This commit is contained in:
@@ -1,8 +1,10 @@
|
|||||||
FROM nginx:alpine
|
FROM nginx:alpine
|
||||||
|
|
||||||
COPY index.html /usr/share/nginx/html/
|
COPY index.html /usr/share/nginx/html/
|
||||||
|
COPY posts.html /usr/share/nginx/html/
|
||||||
COPY dashboard.html /usr/share/nginx/html/
|
COPY dashboard.html /usr/share/nginx/html/
|
||||||
COPY settings.html /usr/share/nginx/html/
|
COPY settings.html /usr/share/nginx/html/
|
||||||
|
COPY bookmarks.html /usr/share/nginx/html/
|
||||||
COPY style.css /usr/share/nginx/html/
|
COPY style.css /usr/share/nginx/html/
|
||||||
COPY dashboard.css /usr/share/nginx/html/
|
COPY dashboard.css /usr/share/nginx/html/
|
||||||
COPY settings.css /usr/share/nginx/html/
|
COPY settings.css /usr/share/nginx/html/
|
||||||
|
|||||||
191
web/app.js
191
web/app.js
@@ -145,6 +145,9 @@ function scheduleUpdatesReconnect() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function startUpdatesStream() {
|
function startUpdatesStream() {
|
||||||
|
if (isBookmarksPage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (typeof EventSource === 'undefined') {
|
if (typeof EventSource === 'undefined') {
|
||||||
console.warn('EventSource wird von diesem Browser nicht unterstützt. Fallback auf Polling.');
|
console.warn('EventSource wird von diesem Browser nicht unterstützt. Fallback auf Polling.');
|
||||||
return;
|
return;
|
||||||
@@ -246,6 +249,11 @@ const bookmarkForm = document.getElementById('bookmarkForm');
|
|||||||
const bookmarkNameInput = document.getElementById('bookmarkName');
|
const bookmarkNameInput = document.getElementById('bookmarkName');
|
||||||
const bookmarkQueryInput = document.getElementById('bookmarkQuery');
|
const bookmarkQueryInput = document.getElementById('bookmarkQuery');
|
||||||
const bookmarkCancelBtn = document.getElementById('bookmarkCancelBtn');
|
const bookmarkCancelBtn = document.getElementById('bookmarkCancelBtn');
|
||||||
|
const bookmarkSearchInput = document.getElementById('bookmarkSearchInput');
|
||||||
|
const bookmarkSortSelect = document.getElementById('bookmarkSortSelect');
|
||||||
|
const bookmarkSortDirectionToggle = document.getElementById('bookmarkSortDirectionToggle');
|
||||||
|
const profileSelectElement = document.getElementById('profileSelect');
|
||||||
|
const isBookmarksPage = document.body.classList.contains('bookmarks-page');
|
||||||
|
|
||||||
const REFRESH_SETTINGS_KEY = 'trackerRefreshSettings';
|
const REFRESH_SETTINGS_KEY = 'trackerRefreshSettings';
|
||||||
const SORT_SETTINGS_KEY = 'trackerSortSettings';
|
const SORT_SETTINGS_KEY = 'trackerSortSettings';
|
||||||
@@ -296,6 +304,9 @@ let manualPostModalPreviousOverflow = '';
|
|||||||
let activeDeadlinePicker = null;
|
let activeDeadlinePicker = null;
|
||||||
let bookmarkPanelVisible = false;
|
let bookmarkPanelVisible = false;
|
||||||
let bookmarkOutsideHandler = null;
|
let bookmarkOutsideHandler = null;
|
||||||
|
let bookmarkSearchTerm = '';
|
||||||
|
let bookmarkSortMode = 'recent';
|
||||||
|
let bookmarkSortDirection = 'desc';
|
||||||
|
|
||||||
const INITIAL_POST_LIMIT = 10;
|
const INITIAL_POST_LIMIT = 10;
|
||||||
const POST_LOAD_INCREMENT = 10;
|
const POST_LOAD_INCREMENT = 10;
|
||||||
@@ -501,6 +512,83 @@ function sortBookmarksByRecency(list) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getBookmarkLabelForComparison(bookmark = {}) {
|
||||||
|
const label = typeof bookmark.label === 'string' ? bookmark.label.trim() : '';
|
||||||
|
const query = typeof bookmark.query === 'string' ? bookmark.query.trim() : '';
|
||||||
|
return label || query || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBookmarkClickTimestamp(bookmark = {}) {
|
||||||
|
if (bookmark.last_clicked_at) {
|
||||||
|
const ts = new Date(bookmark.last_clicked_at).getTime();
|
||||||
|
if (!Number.isNaN(ts)) {
|
||||||
|
return ts;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortBookmarksForDisplay(list) {
|
||||||
|
const items = [...list];
|
||||||
|
if (bookmarkSortMode === 'label') {
|
||||||
|
items.sort((a, b) => {
|
||||||
|
const labelA = getBookmarkLabelForComparison(a);
|
||||||
|
const labelB = getBookmarkLabelForComparison(b);
|
||||||
|
const result = labelA.localeCompare(labelB, 'de', { sensitivity: 'base' });
|
||||||
|
return bookmarkSortDirection === 'desc' ? -result : result;
|
||||||
|
});
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
items.sort((a, b) => {
|
||||||
|
const diff = getBookmarkClickTimestamp(b) - getBookmarkClickTimestamp(a);
|
||||||
|
if (diff !== 0) {
|
||||||
|
return bookmarkSortDirection === 'desc' ? diff : -diff;
|
||||||
|
}
|
||||||
|
const fallback = getBookmarkLabelForComparison(a).localeCompare(
|
||||||
|
getBookmarkLabelForComparison(b),
|
||||||
|
'de',
|
||||||
|
{ sensitivity: 'base' }
|
||||||
|
);
|
||||||
|
return bookmarkSortDirection === 'desc' ? fallback : -fallback;
|
||||||
|
});
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterBookmarksBySearch(list) {
|
||||||
|
if (!bookmarkSearchTerm) {
|
||||||
|
return [...list];
|
||||||
|
}
|
||||||
|
const term = bookmarkSearchTerm.toLowerCase();
|
||||||
|
return list.filter((bookmark) => {
|
||||||
|
const label = (bookmark.label || bookmark.query || '').toLowerCase();
|
||||||
|
const query = (bookmark.query || '').toLowerCase();
|
||||||
|
return label.includes(term) || query.includes(term);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRecentBookmarks(list) {
|
||||||
|
const recent = sortBookmarksByRecency(list);
|
||||||
|
const RECENT_LIMIT = 5;
|
||||||
|
return recent.filter((bookmark) => bookmark.last_clicked_at).slice(0, RECENT_LIMIT);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateBookmarkSortDirectionUI() {
|
||||||
|
if (!bookmarkSortDirectionToggle) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const isAsc = bookmarkSortDirection === 'asc';
|
||||||
|
bookmarkSortDirectionToggle.setAttribute('aria-pressed', isAsc ? 'true' : 'false');
|
||||||
|
bookmarkSortDirectionToggle.setAttribute('title', isAsc ? 'Älteste zuerst' : 'Neueste zuerst');
|
||||||
|
const icon = bookmarkSortDirectionToggle.querySelector('.bookmark-sort__direction-icon');
|
||||||
|
if (icon) {
|
||||||
|
icon.textContent = isAsc ? '▲' : '▼';
|
||||||
|
} else {
|
||||||
|
bookmarkSortDirectionToggle.textContent = isAsc ? '▲' : '▼';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const DEFAULT_BOOKMARK_LAST_CLICK_KEY = 'trackerDefaultBookmarkLastClickedAt';
|
const DEFAULT_BOOKMARK_LAST_CLICK_KEY = 'trackerDefaultBookmarkLastClickedAt';
|
||||||
|
|
||||||
const bookmarkState = {
|
const bookmarkState = {
|
||||||
@@ -954,18 +1042,9 @@ function renderBookmarks() {
|
|||||||
isDefault: true
|
isDefault: true
|
||||||
};
|
};
|
||||||
|
|
||||||
const sorted = sortBookmarksByRecency(dynamicBookmarks);
|
const filteredBookmarks = filterBookmarksBySearch(dynamicBookmarks);
|
||||||
const recent = [];
|
const sortedForAll = sortBookmarksForDisplay(filteredBookmarks);
|
||||||
const RECENT_LIMIT = 5;
|
const recent = bookmarkSearchTerm ? [] : getRecentBookmarks(filteredBookmarks);
|
||||||
|
|
||||||
sorted.forEach((bookmark) => {
|
|
||||||
if (bookmark.last_clicked_at && recent.length < RECENT_LIMIT) {
|
|
||||||
recent.push(bookmark);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const alphabeticalAll = [...dynamicBookmarks]
|
|
||||||
.sort((a, b) => a.label.localeCompare(b.label, 'de', { sensitivity: 'base' }));
|
|
||||||
|
|
||||||
const sections = [];
|
const sections = [];
|
||||||
|
|
||||||
@@ -977,17 +1056,25 @@ function renderBookmarks() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const allItems = bookmarkSearchTerm ? sortedForAll : [staticDefault, ...sortedForAll];
|
||||||
|
const allTitle = bookmarkSearchTerm
|
||||||
|
? `Suchergebnisse${filteredBookmarks.length ? ` (${filteredBookmarks.length})` : ''}`
|
||||||
|
: 'Alle Bookmarks';
|
||||||
|
|
||||||
sections.push({
|
sections.push({
|
||||||
id: 'all',
|
id: bookmarkSearchTerm ? 'search' : 'all',
|
||||||
title: 'Alle Bookmarks',
|
title: allTitle,
|
||||||
items: [staticDefault, ...alphabeticalAll]
|
items: allItems
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let renderedAnySection = false;
|
||||||
|
|
||||||
sections.forEach((section) => {
|
sections.forEach((section) => {
|
||||||
if (!section.items.length) {
|
if (!section.items.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderedAnySection = true;
|
||||||
const sectionElement = document.createElement('section');
|
const sectionElement = document.createElement('section');
|
||||||
sectionElement.className = 'bookmark-section';
|
sectionElement.className = 'bookmark-section';
|
||||||
sectionElement.dataset.section = section.id;
|
sectionElement.dataset.section = section.id;
|
||||||
@@ -1012,6 +1099,17 @@ function renderBookmarks() {
|
|||||||
sectionElement.appendChild(list);
|
sectionElement.appendChild(list);
|
||||||
bookmarksList.appendChild(sectionElement);
|
bookmarksList.appendChild(sectionElement);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!renderedAnySection) {
|
||||||
|
const empty = document.createElement('div');
|
||||||
|
empty.className = 'bookmark-empty';
|
||||||
|
if (bookmarkSearchTerm) {
|
||||||
|
empty.textContent = `Keine Bookmarks gefunden für „${bookmarkSearchTerm}“.`;
|
||||||
|
} else {
|
||||||
|
empty.textContent = 'Noch keine Bookmarks gespeichert.';
|
||||||
|
}
|
||||||
|
bookmarksList.appendChild(empty);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetBookmarkForm() {
|
function resetBookmarkForm() {
|
||||||
@@ -1197,6 +1295,34 @@ function initializeBookmarks() {
|
|||||||
if (bookmarkForm) {
|
if (bookmarkForm) {
|
||||||
bookmarkForm.addEventListener('submit', handleBookmarkSubmit);
|
bookmarkForm.addEventListener('submit', handleBookmarkSubmit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (bookmarkSearchInput) {
|
||||||
|
bookmarkSearchInput.addEventListener('input', () => {
|
||||||
|
bookmarkSearchTerm = typeof bookmarkSearchInput.value === 'string'
|
||||||
|
? bookmarkSearchInput.value.trim()
|
||||||
|
: '';
|
||||||
|
renderBookmarks();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bookmarkSortSelect) {
|
||||||
|
bookmarkSortSelect.addEventListener('change', () => {
|
||||||
|
const value = bookmarkSortSelect.value;
|
||||||
|
if (value === 'label' || value === 'recent') {
|
||||||
|
bookmarkSortMode = value;
|
||||||
|
renderBookmarks();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bookmarkSortDirectionToggle) {
|
||||||
|
bookmarkSortDirectionToggle.addEventListener('click', () => {
|
||||||
|
bookmarkSortDirection = bookmarkSortDirection === 'desc' ? 'asc' : 'desc';
|
||||||
|
updateBookmarkSortDirectionUI();
|
||||||
|
renderBookmarks();
|
||||||
|
});
|
||||||
|
updateBookmarkSortDirectionUI();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSortSettingsPageKey() {
|
function getSortSettingsPageKey() {
|
||||||
@@ -2288,6 +2414,10 @@ function applyAutoRefreshSettings() {
|
|||||||
: '';
|
: '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isBookmarksPage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!autoRefreshSettings.enabled) {
|
if (!autoRefreshSettings.enabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -2582,7 +2712,9 @@ function applyProfileNumber(profileNumber, { fromBackend = false } = {}) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('profileSelect').value = String(profileNumber);
|
if (profileSelectElement) {
|
||||||
|
profileSelectElement.value = String(profileNumber);
|
||||||
|
}
|
||||||
|
|
||||||
if (currentProfile === profileNumber) {
|
if (currentProfile === profileNumber) {
|
||||||
if (!fromBackend) {
|
if (!fromBackend) {
|
||||||
@@ -2598,8 +2730,10 @@ function applyProfileNumber(profileNumber, { fromBackend = false } = {}) {
|
|||||||
pushProfileState(currentProfile);
|
pushProfileState(currentProfile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isBookmarksPage) {
|
||||||
resetVisibleCount();
|
resetVisibleCount();
|
||||||
renderPosts();
|
renderPosts();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load profile from localStorage
|
// Load profile from localStorage
|
||||||
@@ -2637,9 +2771,11 @@ function startProfilePolling() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Profile selector change handler
|
// Profile selector change handler
|
||||||
document.getElementById('profileSelect').addEventListener('change', (e) => {
|
if (profileSelectElement) {
|
||||||
|
profileSelectElement.addEventListener('change', (e) => {
|
||||||
saveProfile(parseInt(e.target.value, 10));
|
saveProfile(parseInt(e.target.value, 10));
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Tab switching
|
// Tab switching
|
||||||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||||||
@@ -2673,6 +2809,9 @@ if (autoRefreshToggle) {
|
|||||||
autoRefreshSettings.enabled = !!autoRefreshToggle.checked;
|
autoRefreshSettings.enabled = !!autoRefreshToggle.checked;
|
||||||
saveAutoRefreshSettings();
|
saveAutoRefreshSettings();
|
||||||
applyAutoRefreshSettings();
|
applyAutoRefreshSettings();
|
||||||
|
if (isBookmarksPage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (autoRefreshSettings.enabled && updatesStreamHealthy) {
|
if (autoRefreshSettings.enabled && updatesStreamHealthy) {
|
||||||
console.info('Live-Updates sind aktiv; automatisches Refresh bleibt pausiert.');
|
console.info('Live-Updates sind aktiv; automatisches Refresh bleibt pausiert.');
|
||||||
}
|
}
|
||||||
@@ -3980,7 +4119,9 @@ function checkAutoCheck() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Konnte check-Parameter nicht entfernen:', error);
|
console.warn('Konnte check-Parameter nicht entfernen:', error);
|
||||||
}
|
}
|
||||||
|
if (!isBookmarksPage) {
|
||||||
fetchPosts({ showLoader: false });
|
fetchPosts({ showLoader: false });
|
||||||
|
}
|
||||||
}).catch(console.error);
|
}).catch(console.error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4049,10 +4190,12 @@ loadAutoRefreshSettings();
|
|||||||
initializeFocusParams();
|
initializeFocusParams();
|
||||||
initializeTabFromUrl();
|
initializeTabFromUrl();
|
||||||
loadSortMode();
|
loadSortMode();
|
||||||
resetManualPostForm();
|
if (!isBookmarksPage) {
|
||||||
loadProfile();
|
resetManualPostForm();
|
||||||
startProfilePolling();
|
loadProfile();
|
||||||
fetchPosts();
|
startProfilePolling();
|
||||||
checkAutoCheck();
|
fetchPosts();
|
||||||
startUpdatesStream();
|
checkAutoCheck();
|
||||||
|
startUpdatesStream();
|
||||||
|
}
|
||||||
applyAutoRefreshSettings();
|
applyAutoRefreshSettings();
|
||||||
|
|||||||
62
web/bookmarks.html
Normal file
62
web/bookmarks.html
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Post Tracker – Bookmarks</title>
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="assets/app-icon-64.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="192x192" href="assets/app-icon-192.png">
|
||||||
|
<link rel="apple-touch-icon" href="assets/app-icon-192.png">
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
</head>
|
||||||
|
<body class="bookmarks-page">
|
||||||
|
<div class="container">
|
||||||
|
<main class="bookmark-page">
|
||||||
|
<section class="bookmark-page__panel">
|
||||||
|
<div class="bookmark-panel__header">
|
||||||
|
<h2 class="bookmark-panel__title">🔖 Bookmarks</h2>
|
||||||
|
<a href="index.html" class="btn btn-secondary">Zurück zum Dashboard</a>
|
||||||
|
</div>
|
||||||
|
<p class="bookmark-page__lead">Über die Bookmarks kannst du auf einen Schlag mehrere relevante Suchanfragen öffnen.</p>
|
||||||
|
<div class="bookmark-panel__toolbar">
|
||||||
|
<label class="bookmark-panel__search">
|
||||||
|
<span>Suche</span>
|
||||||
|
<input type="search" id="bookmarkSearchInput" placeholder="Keyword oder Titel durchsuchen">
|
||||||
|
</label>
|
||||||
|
<div class="bookmark-panel__sort">
|
||||||
|
<label>
|
||||||
|
<span>Sortierung</span>
|
||||||
|
<select id="bookmarkSortSelect">
|
||||||
|
<option value="recent">Zuletzt verwendet</option>
|
||||||
|
<option value="label">Alphabetisch</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<button type="button" class="bookmark-sort__direction" id="bookmarkSortDirectionToggle" aria-pressed="false" title="Sortierreihenfolge umkehren">
|
||||||
|
<span class="bookmark-sort__direction-icon" aria-hidden="true">▼</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="bookmarksList" class="bookmark-list" role="list" aria-live="polite"></div>
|
||||||
|
<form id="bookmarkForm" class="bookmark-form" autocomplete="off">
|
||||||
|
<div class="bookmark-form__fields">
|
||||||
|
<label class="bookmark-form__field">
|
||||||
|
<span>Titel</span>
|
||||||
|
<input type="text" id="bookmarkName" maxlength="40" placeholder="Optionaler Titel">
|
||||||
|
</label>
|
||||||
|
<label class="bookmark-form__field">
|
||||||
|
<span>Keyword *</span>
|
||||||
|
<input type="text" id="bookmarkQuery" required placeholder="z.B. gewinnspiel">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="bookmark-form__actions">
|
||||||
|
<button type="submit" class="btn btn-primary">Speichern</button>
|
||||||
|
<button type="button" class="btn btn-secondary" id="bookmarkCancelBtn">Zurücksetzen</button>
|
||||||
|
</div>
|
||||||
|
<p class="bookmark-form__hint">Öffnet drei Varianten (… Gewinnspiel / … gewinnen / … verlosen) mit Filter auf die letzten vier Wochen.</p>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
<script src="app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -12,10 +12,9 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<header>
|
<div class="page-toolbar page-toolbar--dashboard">
|
||||||
<div class="header-main">
|
<div class="page-toolbar__title">
|
||||||
<h1>📊 Dashboard</h1>
|
<h1>📊 Dashboard</h1>
|
||||||
<a href="?view=posts" class="btn btn-secondary">Zurück zu Beiträgen</a>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="header-controls">
|
<div class="header-controls">
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
@@ -43,7 +42,7 @@
|
|||||||
🔄 Aktualisieren
|
🔄 Aktualisieren
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</div>
|
||||||
|
|
||||||
<div id="loading" class="loading">Lade Statistiken...</div>
|
<div id="loading" class="loading">Lade Statistiken...</div>
|
||||||
<div id="error" class="error" style="display: none;"></div>
|
<div id="error" class="error" style="display: none;"></div>
|
||||||
|
|||||||
280
web/index.html
280
web/index.html
@@ -3,161 +3,159 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Post Tracker - Web Interface</title>
|
<title>Post Tracker</title>
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="assets/app-icon-64.png">
|
<link rel="icon" type="image/png" sizes="32x32" href="assets/app-icon-64.png">
|
||||||
<link rel="icon" type="image/png" sizes="192x192" href="assets/app-icon-192.png">
|
<link rel="icon" type="image/png" sizes="192x192" href="assets/app-icon-192.png">
|
||||||
<link rel="apple-touch-icon" href="assets/app-icon-192.png">
|
<link rel="apple-touch-icon" href="assets/app-icon-192.png">
|
||||||
<link rel="stylesheet" href="style.css">
|
<link rel="stylesheet" href="style.css">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||||
|
background: #f4f5f7;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #f4f5f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-header {
|
||||||
|
background: #ffffff;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
padding: 16px 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 20px;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-header__brand h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-header__nav {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-header__nav-btn {
|
||||||
|
border: 1px solid transparent;
|
||||||
|
background: transparent;
|
||||||
|
padding: 10px 18px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111827;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-header__nav-btn:hover {
|
||||||
|
background: #eef2ff;
|
||||||
|
border-color: #c7d2fe;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-header__nav-btn.active {
|
||||||
|
background: #111827;
|
||||||
|
color: #ffffff;
|
||||||
|
border-color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell-main {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#appFrame {
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100vh - 72px);
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.site-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
#appFrame {
|
||||||
|
height: calc(100vh - 140px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="shell">
|
||||||
<header>
|
<header class="site-header">
|
||||||
<div class="header-main">
|
<div class="site-header__brand">
|
||||||
<h1>📋 Post Tracker</h1>
|
<h1>📋 Post Tracker</h1>
|
||||||
<div class="header-links">
|
<p style="margin: 0; font-size: 13px; color: #6b7280;">Alle Bereiche über diese Navigation laden</p>
|
||||||
<a href="?view=dashboard" class="btn btn-secondary">Dashboard</a>
|
|
||||||
<a href="settings.html" class="btn btn-secondary">⚙️ Einstellungen</a>
|
|
||||||
<div class="bookmark-inline">
|
|
||||||
<button type="button" class="btn btn-secondary bookmark-inline__toggle" id="bookmarkPanelToggle" aria-expanded="false" aria-controls="bookmarkPanel">🔖 Bookmarks</button>
|
|
||||||
<div id="bookmarkPanel" class="bookmark-panel" role="dialog" aria-modal="false" hidden>
|
|
||||||
<div class="bookmark-panel__header">
|
|
||||||
<h2 class="bookmark-panel__title">🔖 Bookmarks</h2>
|
|
||||||
<button type="button" class="bookmark-panel__close" id="bookmarkPanelClose" aria-label="Schließen">×</button>
|
|
||||||
</div>
|
|
||||||
<div id="bookmarksList" class="bookmark-list" role="list" aria-live="polite"></div>
|
|
||||||
<form id="bookmarkForm" class="bookmark-form" autocomplete="off">
|
|
||||||
<div class="bookmark-form__fields">
|
|
||||||
<label class="bookmark-form__field">
|
|
||||||
<span>Titel</span>
|
|
||||||
<input type="text" id="bookmarkName" maxlength="40" placeholder="Optionaler Titel">
|
|
||||||
</label>
|
|
||||||
<label class="bookmark-form__field">
|
|
||||||
<span>Keyword *</span>
|
|
||||||
<input type="text" id="bookmarkQuery" required placeholder="z.B. gewinnspiel">
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="bookmark-form__actions">
|
|
||||||
<button type="submit" class="btn btn-primary">Speichern</button>
|
|
||||||
<button type="button" class="btn btn-secondary" id="bookmarkCancelBtn">Zurücksetzen</button>
|
|
||||||
</div>
|
|
||||||
<p class="bookmark-form__hint">Öffnet für das Keyword drei Suchen (… Gewinnspiel / … gewinnen / … verlosen) mit Filter auf die letzten 4 Wochen.</p>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button type="button" class="btn btn-primary" id="openManualPostModalBtn">Beitrag hinzufügen</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="header-controls">
|
|
||||||
<div class="control-group">
|
|
||||||
<label for="profileSelect">Dein Profil:</label>
|
|
||||||
<select id="profileSelect" class="control-select">
|
|
||||||
<option value="1">Profil 1</option>
|
|
||||||
<option value="2">Profil 2</option>
|
|
||||||
<option value="3">Profil 3</option>
|
|
||||||
<option value="4">Profil 4</option>
|
|
||||||
<option value="5">Profil 5</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="control-group">
|
|
||||||
<label class="switch">
|
|
||||||
<input type="checkbox" id="autoRefreshToggle" checked>
|
|
||||||
<span>Auto-Refresh</span>
|
|
||||||
</label>
|
|
||||||
<select id="autoRefreshInterval" class="control-select">
|
|
||||||
<option value="15000">15 s</option>
|
|
||||||
<option value="30000">30 s</option>
|
|
||||||
<option value="60000">1 min</option>
|
|
||||||
<option value="120000">2 min</option>
|
|
||||||
<option value="300000">5 min</option>
|
|
||||||
</select>
|
|
||||||
<button type="button" id="manualRefreshBtn" class="refresh-btn" aria-label="Aktualisieren" title="Aktualisieren">🔄</button>
|
|
||||||
</div>
|
|
||||||
<div class="control-group">
|
|
||||||
<label for="sortMode">Sortierung:</label>
|
|
||||||
<div class="sort-controls">
|
|
||||||
<select id="sortMode" class="control-select">
|
|
||||||
<option value="created">Erstelldatum</option>
|
|
||||||
<option value="deadline">Deadline</option>
|
|
||||||
<option value="lastCheck">Letzte Teilnahme</option>
|
|
||||||
<option value="lastChange">Letzte Änderung</option>
|
|
||||||
<option value="smart">Smart (Dringlichkeit)</option>
|
|
||||||
</select>
|
|
||||||
<button type="button" id="sortDirectionToggle" class="sort-direction-toggle" aria-label="Absteigend" aria-pressed="false" title="Absteigend">
|
|
||||||
<span class="sort-direction-toggle__icon" aria-hidden="true">▼</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<nav class="site-header__nav">
|
||||||
|
<button type="button" class="site-header__nav-btn" data-view-target="posts">Beiträge</button>
|
||||||
|
<button type="button" class="site-header__nav-btn" data-view-target="dashboard">Dashboard</button>
|
||||||
|
<button type="button" class="site-header__nav-btn" data-view-target="settings">⚙️ Einstellungen</button>
|
||||||
|
<button type="button" class="site-header__nav-btn" data-view-target="bookmarks">🔖 Bookmarks</button>
|
||||||
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
<main class="shell-main">
|
||||||
<div class="tabs-section">
|
<iframe id="appFrame" src="" title="Post Tracker Inhalt" loading="lazy"></iframe>
|
||||||
<div class="tabs">
|
</main>
|
||||||
<button class="tab-btn active" data-tab="pending">Offene Beiträge</button>
|
|
||||||
<button class="tab-btn" data-tab="expired">Abgelaufen/Abgeschlossen</button>
|
|
||||||
<button class="tab-btn" data-tab="all">Alle Beiträge</button>
|
|
||||||
</div>
|
|
||||||
<div class="search-container">
|
|
||||||
<input type="text" id="searchInput" class="search-input" placeholder="Beiträge durchsuchen...">
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="loading" class="loading">Lade Beiträge...</div>
|
<script>
|
||||||
<div id="error" class="error" style="display: none;"></div>
|
(function () {
|
||||||
|
const frame = document.getElementById('appFrame');
|
||||||
|
const buttons = Array.from(document.querySelectorAll('[data-view-target]'));
|
||||||
|
const viewUrls = {
|
||||||
|
posts: 'posts.html',
|
||||||
|
dashboard: 'dashboard.html',
|
||||||
|
settings: 'settings.html',
|
||||||
|
bookmarks: 'bookmarks.html'
|
||||||
|
};
|
||||||
|
|
||||||
<div id="postsContainer" class="posts-container"></div>
|
const queryParams = new URLSearchParams(window.location.search);
|
||||||
</div>
|
const initial = queryParams.get('view');
|
||||||
|
const defaultView = initial && viewUrls[initial] ? initial : 'posts';
|
||||||
|
|
||||||
<div id="screenshotModal" class="screenshot-modal" hidden>
|
function setView(view) {
|
||||||
<div id="screenshotModalBackdrop" class="screenshot-modal__backdrop" aria-hidden="true"></div>
|
buttons.forEach((button) => {
|
||||||
<div id="screenshotModalContent" class="screenshot-modal__content" role="dialog" aria-modal="true">
|
button.classList.toggle('active', button.dataset.viewTarget === view);
|
||||||
<button type="button" id="screenshotModalClose" class="screenshot-modal__close" aria-label="Schließen">×</button>
|
});
|
||||||
<img id="screenshotModalImage" alt="Screenshot zum Beitrag" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="manualPostModal" class="modal" hidden>
|
const targetUrl = viewUrls[view] || viewUrls.posts;
|
||||||
<div id="manualPostModalBackdrop" class="modal__backdrop" aria-hidden="true"></div>
|
if (frame.src !== targetUrl) {
|
||||||
<div id="manualPostModalContent" class="modal__content" role="dialog" aria-modal="true" aria-labelledby="manualPostModalTitle" tabindex="-1">
|
frame.src = targetUrl;
|
||||||
<button type="button" id="manualPostModalClose" class="modal__close" aria-label="Schließen">×</button>
|
}
|
||||||
<h2 id="manualPostModalTitle">Beitrag hinzufügen</h2>
|
|
||||||
<form id="manualPostForm" novalidate>
|
|
||||||
<div class="form-grid">
|
|
||||||
<label class="form-field">
|
|
||||||
<span>Direktlink *</span>
|
|
||||||
<input type="url" id="manualPostUrl" placeholder="https://www.facebook.com/..." required>
|
|
||||||
</label>
|
|
||||||
<label class="form-field">
|
|
||||||
<span>Titel</span>
|
|
||||||
<input type="text" id="manualPostTitle" placeholder="Kurzbeschreibung" maxlength="200">
|
|
||||||
</label>
|
|
||||||
<label class="form-field">
|
|
||||||
<span>Benötigte Profile *</span>
|
|
||||||
<select id="manualPostTarget" required>
|
|
||||||
<option value="1">1</option>
|
|
||||||
<option value="2">2</option>
|
|
||||||
<option value="3">3</option>
|
|
||||||
<option value="4">4</option>
|
|
||||||
<option value="5">5</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<label class="form-field">
|
|
||||||
<span>Erstellt von (Facebook-Name)</span>
|
|
||||||
<input type="text" id="manualPostCreatorName" placeholder="z.B. Max Mustermann">
|
|
||||||
</label>
|
|
||||||
<label class="form-field">
|
|
||||||
<span>Deadline</span>
|
|
||||||
<input type="datetime-local" id="manualPostDeadline">
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-actions">
|
|
||||||
<button type="submit" class="btn btn-primary" id="manualPostSubmitBtn">Speichern</button>
|
|
||||||
<button type="button" class="btn btn-secondary" id="manualPostReset">Zurücksetzen</button>
|
|
||||||
</div>
|
|
||||||
<div id="manualPostMessage" class="form-message" role="status" aria-live="polite"></div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="app.js"></script>
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
if (view === 'posts') {
|
||||||
|
params.delete('view');
|
||||||
|
} else {
|
||||||
|
params.set('view', view);
|
||||||
|
}
|
||||||
|
const url = params.toString() ? `?${params.toString()}` : window.location.pathname;
|
||||||
|
window.history.replaceState({}, document.title, url);
|
||||||
|
}
|
||||||
|
|
||||||
|
buttons.forEach((button) => {
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
setView(button.dataset.viewTarget);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
setView(defaultView);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
132
web/posts.html
Normal file
132
web/posts.html
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Post Tracker - Web Interface</title>
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="assets/app-icon-64.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="192x192" href="assets/app-icon-192.png">
|
||||||
|
<link rel="apple-touch-icon" href="assets/app-icon-192.png">
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="page-toolbar">
|
||||||
|
<div class="page-toolbar__title">
|
||||||
|
<h1>📋 Post Tracker</h1>
|
||||||
|
<button type="button" class="btn btn-primary" id="openManualPostModalBtn">Beitrag hinzufügen</button>
|
||||||
|
</div>
|
||||||
|
<div class="header-controls">
|
||||||
|
<div class="control-group">
|
||||||
|
<label for="profileSelect">Dein Profil:</label>
|
||||||
|
<select id="profileSelect" class="control-select">
|
||||||
|
<option value="1">Profil 1</option>
|
||||||
|
<option value="2">Profil 2</option>
|
||||||
|
<option value="3">Profil 3</option>
|
||||||
|
<option value="4">Profil 4</option>
|
||||||
|
<option value="5">Profil 5</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="control-group">
|
||||||
|
<label class="switch">
|
||||||
|
<input type="checkbox" id="autoRefreshToggle" checked>
|
||||||
|
<span>Auto-Refresh</span>
|
||||||
|
</label>
|
||||||
|
<select id="autoRefreshInterval" class="control-select">
|
||||||
|
<option value="15000">15 s</option>
|
||||||
|
<option value="30000">30 s</option>
|
||||||
|
<option value="60000">1 min</option>
|
||||||
|
<option value="120000">2 min</option>
|
||||||
|
<option value="300000">5 min</option>
|
||||||
|
</select>
|
||||||
|
<button type="button" id="manualRefreshBtn" class="refresh-btn" aria-label="Aktualisieren" title="Aktualisieren">🔄</button>
|
||||||
|
</div>
|
||||||
|
<div class="control-group">
|
||||||
|
<label for="sortMode">Sortierung:</label>
|
||||||
|
<div class="sort-controls">
|
||||||
|
<select id="sortMode" class="control-select">
|
||||||
|
<option value="created">Erstelldatum</option>
|
||||||
|
<option value="deadline">Deadline</option>
|
||||||
|
<option value="lastCheck">Letzte Teilnahme</option>
|
||||||
|
<option value="lastChange">Letzte Änderung</option>
|
||||||
|
<option value="smart">Smart (Dringlichkeit)</option>
|
||||||
|
</select>
|
||||||
|
<button type="button" id="sortDirectionToggle" class="sort-direction-toggle" aria-label="Absteigend" aria-pressed="false" title="Absteigend">
|
||||||
|
<span class="sort-direction-toggle__icon" aria-hidden="true">▼</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tabs-section">
|
||||||
|
<div class="tabs">
|
||||||
|
<button class="tab-btn active" data-tab="pending">Offene Beiträge</button>
|
||||||
|
<button class="tab-btn" data-tab="expired">Abgelaufen/Abgeschlossen</button>
|
||||||
|
<button class="tab-btn" data-tab="all">Alle Beiträge</button>
|
||||||
|
</div>
|
||||||
|
<div class="search-container">
|
||||||
|
<input type="text" id="searchInput" class="search-input" placeholder="Beiträge durchsuchen...">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="loading" class="loading">Lade Beiträge...</div>
|
||||||
|
<div id="error" class="error" style="display: none;"></div>
|
||||||
|
|
||||||
|
<div id="postsContainer" class="posts-container"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="screenshotModal" class="screenshot-modal" hidden>
|
||||||
|
<div id="screenshotModalBackdrop" class="screenshot-modal__backdrop" aria-hidden="true"></div>
|
||||||
|
<div id="screenshotModalContent" class="screenshot-modal__content" role="dialog" aria-modal="true">
|
||||||
|
<button type="button" id="screenshotModalClose" class="screenshot-modal__close" aria-label="Schließen">×</button>
|
||||||
|
<img id="screenshotModalImage" alt="Screenshot zum Beitrag" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="manualPostModal" class="modal" hidden>
|
||||||
|
<div id="manualPostModalBackdrop" class="modal__backdrop" aria-hidden="true"></div>
|
||||||
|
<div id="manualPostModalContent" class="modal__content" role="dialog" aria-modal="true" aria-labelledby="manualPostModalTitle" tabindex="-1">
|
||||||
|
<button type="button" id="manualPostModalClose" class="modal__close" aria-label="Schließen">×</button>
|
||||||
|
<h2 id="manualPostModalTitle">Beitrag hinzufügen</h2>
|
||||||
|
<form id="manualPostForm" novalidate>
|
||||||
|
<div class="form-grid">
|
||||||
|
<label class="form-field">
|
||||||
|
<span>Direktlink *</span>
|
||||||
|
<input type="url" id="manualPostUrl" placeholder="https://www.facebook.com/..." required>
|
||||||
|
</label>
|
||||||
|
<label class="form-field">
|
||||||
|
<span>Titel</span>
|
||||||
|
<input type="text" id="manualPostTitle" placeholder="Kurzbeschreibung" maxlength="200">
|
||||||
|
</label>
|
||||||
|
<label class="form-field">
|
||||||
|
<span>Benötigte Profile *</span>
|
||||||
|
<select id="manualPostTarget" required>
|
||||||
|
<option value="1">1</option>
|
||||||
|
<option value="2">2</option>
|
||||||
|
<option value="3">3</option>
|
||||||
|
<option value="4">4</option>
|
||||||
|
<option value="5">5</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="form-field">
|
||||||
|
<span>Erstellt von (Facebook-Name)</span>
|
||||||
|
<input type="text" id="manualPostCreatorName" placeholder="z.B. Max Mustermann">
|
||||||
|
</label>
|
||||||
|
<label class="form-field">
|
||||||
|
<span>Deadline</span>
|
||||||
|
<input type="datetime-local" id="manualPostDeadline">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="submit" class="btn btn-primary" id="manualPostSubmitBtn">Speichern</button>
|
||||||
|
<button type="button" class="btn btn-secondary" id="manualPostReset">Zurücksetzen</button>
|
||||||
|
</div>
|
||||||
|
<div id="manualPostMessage" class="form-message" role="status" aria-live="polite"></div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -12,12 +12,6 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<header>
|
|
||||||
<div class="header-main">
|
|
||||||
<h1>⚙️ Einstellungen</h1>
|
|
||||||
<a href="index.html" class="btn btn-secondary">Zurück zu Beiträgen</a>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div id="loading" class="loading" style="display: none;">Lade Einstellungen...</div>
|
<div id="loading" class="loading" style="display: none;">Lade Einstellungen...</div>
|
||||||
<div id="error" class="error" style="display: none;"></div>
|
<div id="error" class="error" style="display: none;"></div>
|
||||||
|
|||||||
117
web/style.css
117
web/style.css
@@ -28,6 +28,24 @@ header {
|
|||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page-toolbar {
|
||||||
|
background: white;
|
||||||
|
padding: 16px 18px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.08);
|
||||||
|
margin-bottom: 18px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-toolbar__title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.header-main {
|
.header-main {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -1333,6 +1351,105 @@ h1 {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bookmarks-page {
|
||||||
|
padding-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookmarks-page__intro {
|
||||||
|
margin-top: 12px;
|
||||||
|
color: #4b5563;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookmark-page {
|
||||||
|
margin-top: 32px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookmark-page__panel {
|
||||||
|
width: min(960px, 100%);
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 20px;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
padding: 24px 28px;
|
||||||
|
box-shadow: 0 24px 60px rgba(15, 23, 42, 0.12);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookmark-page__panel .bookmark-list {
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookmark-page__lead {
|
||||||
|
margin: 0;
|
||||||
|
color: #4b5563;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookmark-panel__toolbar {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: flex-end;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookmark-panel__search {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
flex: 1 1 220px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookmark-panel__search input {
|
||||||
|
border: 1px solid #d0d3d9;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookmark-panel__sort {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookmark-panel__sort label {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookmark-panel__sort select {
|
||||||
|
border: 1px solid #d0d3d9;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
min-width: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookmark-sort__direction {
|
||||||
|
border: 1px solid #d0d3d9;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookmark-sort__direction:hover {
|
||||||
|
border-color: #a5b4fc;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
.bookmark-panel {
|
.bookmark-panel {
|
||||||
width: min(480px, 94vw);
|
width: min(480px, 94vw);
|
||||||
|
|||||||
Reference in New Issue
Block a user