reworked site

This commit is contained in:
2025-11-16 13:02:05 +01:00
parent 339f03e38e
commit f9d8fc5b82
8 changed files with 628 additions and 181 deletions

View File

@@ -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/

View File

@@ -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,9 +2730,11 @@ function applyProfileNumber(profileNumber, { fromBackend = false } = {}) {
pushProfileState(currentProfile); pushProfileState(currentProfile);
} }
if (!isBookmarksPage) {
resetVisibleCount(); resetVisibleCount();
renderPosts(); renderPosts();
} }
}
// Load profile from localStorage // Load profile from localStorage
function loadProfile() { function loadProfile() {
@@ -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();
if (!isBookmarksPage) {
resetManualPostForm(); resetManualPostForm();
loadProfile(); loadProfile();
startProfilePolling(); startProfilePolling();
fetchPosts(); fetchPosts();
checkAutoCheck(); checkAutoCheck();
startUpdatesStream(); startUpdatesStream();
}
applyAutoRefreshSettings(); applyAutoRefreshSettings();

62
web/bookmarks.html Normal file
View 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>

View File

@@ -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>

View File

@@ -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
View 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>

View File

@@ -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>

View File

@@ -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);