587 lines
39 KiB
HTML
587 lines
39 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>LanguageTool Website Checker</title>
|
||
<style>
|
||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||
:root {
|
||
--bg: #f8f9fa; --surface: #ffffff; --border: #e2e8f0;
|
||
--text: #1a202c; --text-secondary: #64748b;
|
||
--primary: #4f46e5; --primary-hover: #4338ca;
|
||
--spelling: #ef4444; --spelling-bg: #fef2f2;
|
||
--grammar: #f59e0b; --grammar-bg: #fffbeb;
|
||
--style: #3b82f6; --style-bg: #eff6ff;
|
||
--success: #22c55e; --success-bg: #f0fdf4;
|
||
--error-bg: #fef2f2;
|
||
--radius: 8px;
|
||
--shadow: 0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.06);
|
||
}
|
||
body { font-family: -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif; background: var(--bg); color: var(--text); line-height: 1.6; min-height: 100vh; }
|
||
.container { max-width: 1100px; margin: 0 auto; padding: 0 24px; }
|
||
|
||
/* Header */
|
||
.header { background: var(--surface); border-bottom: 1px solid var(--border); padding: 16px 0; position: sticky; top: 0; z-index: 100; }
|
||
.header-inner { display: flex; align-items: center; justify-content: space-between; }
|
||
.header h1 { font-size: 18px; font-weight: 700; color: var(--primary); }
|
||
.header-back { font-size: 14px; color: var(--primary); cursor: pointer; text-decoration: none; display: none; }
|
||
.header-back:hover { text-decoration: underline; }
|
||
.legend { display: flex; gap: 16px; font-size: 13px; color: var(--text-secondary); }
|
||
.legend-item { display: flex; align-items: center; gap: 4px; }
|
||
.legend-dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; }
|
||
.legend-dot.spelling { background: var(--spelling); }
|
||
.legend-dot.grammar { background: var(--grammar); }
|
||
.legend-dot.style { background: var(--style); }
|
||
|
||
/* Form */
|
||
.form-view { padding: 60px 0; }
|
||
.form-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 40px; max-width: 560px; margin: 0 auto; box-shadow: var(--shadow); }
|
||
.form-card h2 { font-size: 22px; margin-bottom: 4px; }
|
||
.form-card .subtitle { color: var(--text-secondary); font-size: 14px; margin-bottom: 28px; }
|
||
.form-group { margin-bottom: 18px; }
|
||
.form-group label { display: block; font-size: 13px; font-weight: 600; margin-bottom: 5px; }
|
||
.form-group input, .form-group select { width: 100%; padding: 10px 12px; border: 1px solid var(--border); border-radius: 6px; font-size: 14px; color: var(--text); background: var(--bg); }
|
||
.form-group input:focus, .form-group select:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px rgba(79,70,229,0.1); }
|
||
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
||
.btn-primary { width: 100%; padding: 12px; background: var(--primary); color: white; border: none; border-radius: 6px; font-size: 15px; font-weight: 600; cursor: pointer; margin-top: 8px; }
|
||
.btn-primary:hover { background: var(--primary-hover); }
|
||
.btn-primary:disabled { opacity: 0.6; cursor: not-allowed; }
|
||
|
||
/* Progress */
|
||
.progress-view { padding: 80px 0; text-align: center; display: none; }
|
||
.progress-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 48px 40px; max-width: 560px; margin: 0 auto; box-shadow: var(--shadow); }
|
||
.spinner { width: 40px; height: 40px; border: 3px solid var(--border); border-top-color: var(--primary); border-radius: 50%; animation: spin 0.8s linear infinite; margin: 0 auto 20px; }
|
||
@keyframes spin { to { transform: rotate(360deg); } }
|
||
.progress-text { font-size: 15px; font-weight: 600; margin-bottom: 6px; }
|
||
.progress-detail { font-size: 13px; color: var(--text-secondary); word-break: break-all; }
|
||
.progress-bar-outer { width: 100%; height: 6px; background: var(--border); border-radius: 3px; margin-top: 20px; overflow: hidden; }
|
||
.progress-bar-inner { height: 100%; background: var(--primary); border-radius: 3px; transition: width 0.3s; width: 0%; }
|
||
|
||
.error-banner { background: var(--error-bg); border: 1px solid var(--spelling); color: var(--spelling); padding: 12px 16px; border-radius: 6px; font-size: 14px; margin-top: 16px; display: none; }
|
||
|
||
/* Results Overview */
|
||
.results-view { padding: 32px 0; display: none; }
|
||
.stats-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 28px; }
|
||
.stat-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 18px; text-align: center; box-shadow: var(--shadow); }
|
||
.stat-number { font-size: 28px; font-weight: 700; line-height: 1.2; }
|
||
.stat-number.spelling { color: var(--spelling); }
|
||
.stat-number.grammar { color: var(--grammar); }
|
||
.stat-number.style { color: var(--style); }
|
||
.stat-label { font-size: 12px; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.5px; margin-top: 4px; }
|
||
.results-info { font-size: 13px; color: var(--text-secondary); margin-bottom: 16px; }
|
||
|
||
.search-bar { margin-bottom: 16px; }
|
||
.search-bar input { width: 100%; padding: 10px 14px; border: 1px solid var(--border); border-radius: 6px; font-size: 14px; background: var(--surface); }
|
||
.search-bar input:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px rgba(79,70,229,0.1); }
|
||
.search-results { margin-bottom: 24px; }
|
||
.search-result-item { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 12px 16px; margin-bottom: 6px; cursor: pointer; }
|
||
.search-result-item:hover { border-color: var(--primary); }
|
||
.search-result-url { font-size: 11px; color: var(--text-secondary); margin-bottom: 4px; word-break: break-all; }
|
||
.search-result-context { font-family: 'Cascadia Code','Fira Code','Consolas',monospace; font-size: 13px; }
|
||
.search-highlight { background: #fef08a; padding: 1px 2px; border-radius: 2px; }
|
||
|
||
.page-list { display: flex; flex-direction: column; gap: 6px; }
|
||
.page-item { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 14px 18px; display: flex; align-items: center; justify-content: space-between; cursor: pointer; }
|
||
.page-item:hover { border-color: var(--primary); box-shadow: 0 0 0 2px rgba(79,70,229,0.08); }
|
||
.page-item.skipped { opacity: 0.5; cursor: default; }
|
||
.page-item.dismissed { opacity: 0.45; }
|
||
.page-url { font-size: 14px; word-break: break-all; flex: 1; margin-right: 12px; }
|
||
.page-badges { display: flex; gap: 6px; flex-shrink: 0; align-items: center; }
|
||
.badge { display: inline-flex; align-items: center; padding: 3px 8px; border-radius: 12px; font-size: 12px; font-weight: 600; }
|
||
.badge.spelling { background: var(--spelling-bg); color: var(--spelling); }
|
||
.badge.grammar { background: var(--grammar-bg); color: var(--grammar); }
|
||
.badge.style { background: var(--style-bg); color: var(--style); }
|
||
.badge.success { background: var(--success-bg); color: var(--success); }
|
||
.badge.error { background: var(--error-bg); color: var(--spelling); }
|
||
.page-dismiss-icon { font-size: 16px; font-weight: 700; cursor: pointer; flex-shrink: 0; margin-left: 10px; line-height: 1; }
|
||
.page-dismiss-icon.pending { color: var(--spelling); }
|
||
.page-dismiss-icon.done { color: var(--success); }
|
||
|
||
/* Detail View */
|
||
.detail-view { padding: 32px 0; display: none; }
|
||
.detail-url { font-size: 14px; color: var(--primary); text-decoration: none; word-break: break-all; }
|
||
.detail-url:hover { text-decoration: underline; }
|
||
.detail-stats { display: flex; gap: 12px; margin: 16px 0 12px; }
|
||
.detail-toolbar { display: flex; gap: 8px; margin-bottom: 20px; flex-wrap: wrap; align-items: center; }
|
||
.filter-btn { padding: 6px 14px; border: 2px solid var(--border); border-radius: 20px; background: var(--surface); font-size: 13px; font-weight: 600; cursor: pointer; color: var(--text-secondary); }
|
||
.filter-btn:hover { border-color: var(--text-secondary); }
|
||
.filter-btn.active { color: white; }
|
||
.filter-btn.active[data-filter="all"] { background: var(--primary); border-color: var(--primary); }
|
||
.filter-btn.active[data-filter="spelling"] { background: var(--spelling); border-color: var(--spelling); }
|
||
.filter-btn.active[data-filter="grammar"] { background: var(--grammar); border-color: var(--grammar); }
|
||
.filter-btn.active[data-filter="style"] { background: var(--style); border-color: var(--style); }
|
||
.toolbar-spacer { flex: 1; }
|
||
.dismiss-all-btn { padding: 6px 14px; border: 1px solid var(--success); border-radius: 6px; background: var(--success-bg); color: var(--success); font-size: 13px; font-weight: 600; cursor: pointer; }
|
||
.dismiss-all-btn:hover { background: var(--success); color: white; }
|
||
|
||
.section-block { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); margin-bottom: 12px; overflow: hidden; }
|
||
.section-header { display: flex; align-items: flex-start; justify-content: space-between; padding: 10px 16px; background: var(--bg); border-bottom: 1px solid var(--border); font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-secondary); gap: 12px; }
|
||
.section-header-left { display: flex; flex-direction: column; gap: 4px; flex: 1; min-width: 0; }
|
||
.section-type-label { white-space: nowrap; }
|
||
.section-corrections { display: flex; flex-direction: column; gap: 2px; text-transform: none; letter-spacing: normal; font-weight: 400; }
|
||
.section-correction-item { display: flex; align-items: center; gap: 6px; font-size: 12px; line-height: 1.4; padding: 3px 6px; border-radius: 4px; cursor: pointer; user-select: none; }
|
||
.section-correction-item:hover { background: rgba(0,0,0,0.04); }
|
||
.section-correction-item.dismissed-item { opacity: 0.35; }
|
||
.section-correction-item.dismissed-item .section-corr-original,
|
||
.section-correction-item.dismissed-item .section-corr-arrow,
|
||
.section-correction-item.dismissed-item .section-corr-suggestion,
|
||
.section-correction-item.dismissed-item .section-corr-msg { text-decoration: line-through; }
|
||
.section-corr-marker { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
|
||
.section-corr-marker.spelling { background: var(--spelling); }
|
||
.section-corr-marker.grammar { background: var(--grammar); }
|
||
.section-corr-marker.style { background: var(--style); }
|
||
.section-corr-original { font-weight: 600; color: var(--text); }
|
||
.section-corr-arrow { color: var(--success); }
|
||
.section-corr-suggestion { color: var(--success); font-weight: 600; cursor: pointer; }
|
||
.section-corr-suggestion:hover { text-decoration: underline; }
|
||
.section-corr-msg { color: var(--text-secondary); margin-left: 4px; }
|
||
.section-corr-extra { color: var(--text-secondary); margin-left: 2px; }
|
||
.section-corr-status { flex-shrink: 0; margin-left: auto; font-size: 14px; font-weight: 700; line-height: 1; }
|
||
.section-corr-status.pending { color: var(--spelling); }
|
||
.section-corr-status.done { color: var(--success); }
|
||
.section-check { color: var(--success); font-size: 14px; flex-shrink: 0; }
|
||
.section-body { padding: 16px; font-family: 'Cascadia Code','Fira Code','Consolas',monospace; font-size: 14px; line-height: 1.8; white-space: pre-wrap; word-wrap: break-word; }
|
||
|
||
/* Error marks — clickable, copies first suggestion */
|
||
.error-mark { position: relative; cursor: pointer; padding: 1px 2px; border-radius: 2px; }
|
||
.error-mark.dismissed { opacity: 0.25; text-decoration: line-through; cursor: default; pointer-events: none; }
|
||
.error-mark.spelling { background: var(--spelling-bg); border-bottom: 2px solid var(--spelling); }
|
||
.error-mark.grammar { background: var(--grammar-bg); border-bottom: 2px solid var(--grammar); }
|
||
.error-mark.style { background: var(--style-bg); border-bottom: 2px solid var(--style); }
|
||
|
||
/* (inline checkbox removed — now at end of correction row in header) */
|
||
|
||
/* (error-list removed — corrections now shown in section header) */
|
||
|
||
/* Copied toast */
|
||
.copied-toast { position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%); background: var(--text); color: white; padding: 8px 20px; border-radius: 20px; font-size: 13px; font-weight: 600; z-index: 9999; opacity: 0; transition: opacity 0.2s; pointer-events: none; }
|
||
.copied-toast.show { opacity: 1; }
|
||
|
||
/* Preview */
|
||
.preview-view { padding: 32px 0; display: none; }
|
||
.preview-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 28px; max-width: 700px; margin: 0 auto; box-shadow: var(--shadow); }
|
||
.preview-card h2 { font-size: 18px; margin-bottom: 4px; }
|
||
.preview-info { font-size: 13px; color: var(--text-secondary); margin-bottom: 16px; }
|
||
.preview-actions { display: flex; gap: 8px; margin-bottom: 16px; align-items: center; }
|
||
.preview-actions button { padding: 5px 12px; border: 1px solid var(--border); border-radius: 6px; background: var(--surface); font-size: 12px; cursor: pointer; color: var(--text-secondary); }
|
||
.preview-actions button:hover { border-color: var(--primary); color: var(--primary); }
|
||
.preview-count { font-size: 13px; color: var(--text-secondary); margin-left: auto; }
|
||
.preview-list { max-height: 400px; overflow-y: auto; border: 1px solid var(--border); border-radius: 6px; margin-bottom: 16px; }
|
||
.preview-item { display: flex; align-items: center; gap: 10px; padding: 8px 12px; border-bottom: 1px solid var(--border); font-size: 13px; }
|
||
.preview-item:last-child { border-bottom: none; }
|
||
.preview-item input[type="checkbox"] { width: 16px; height: 16px; flex-shrink: 0; accent-color: var(--primary); }
|
||
.preview-item label { word-break: break-all; cursor: pointer; flex: 1; }
|
||
.preview-buttons { display: flex; gap: 10px; }
|
||
.btn-secondary { padding: 10px 20px; background: var(--surface); color: var(--text); border: 1px solid var(--border); border-radius: 6px; font-size: 14px; font-weight: 600; cursor: pointer; }
|
||
.btn-secondary:hover { border-color: var(--text-secondary); }
|
||
|
||
@media (max-width: 768px) { .stats-grid { grid-template-columns: repeat(2, 1fr); } .form-row { grid-template-columns: 1fr; } .legend { flex-wrap: wrap; gap: 8px; } }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<div class="header">
|
||
<div class="container header-inner">
|
||
<h1>LanguageTool Website Checker</h1>
|
||
<a class="header-back" id="backBtn" onclick="goBack()">← Zurück zur Übersicht</a>
|
||
<div class="legend">
|
||
<div class="legend-item"><span class="legend-dot spelling"></span> Rechtschreibung</div>
|
||
<div class="legend-item"><span class="legend-dot grammar"></span> Grammatik</div>
|
||
<div class="legend-item"><span class="legend-dot style"></span> Stil</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="copied-toast" id="copiedToast">In Zwischenablage kopiert</div>
|
||
|
||
<!-- Form -->
|
||
<div class="form-view" id="formView">
|
||
<div class="container"><div class="form-card">
|
||
<h2>Website prüfen</h2>
|
||
<p class="subtitle">Crawlt alle Seiten einer Website und prüft Texte mit LanguageTool.</p>
|
||
<div class="form-group"><label>Domain</label><input type="text" id="domain" placeholder="beispiel.de" autocomplete="off"></div>
|
||
<div class="form-group"><label>LanguageTool E-Mail / Username</label><input type="text" id="username" placeholder="mail@beispiel.de" autocomplete="off"></div>
|
||
<div class="form-group"><label>LanguageTool API Key</label><input type="password" id="apiKey" placeholder="API Key eingeben" autocomplete="off"></div>
|
||
<div class="form-row">
|
||
<div class="form-group"><label>Sprache</label><select id="language"><option value="de-DE">Deutsch (DE)</option><option value="de-AT">Deutsch (AT)</option><option value="de-CH">Deutsch (CH)</option><option value="en-US">English (US)</option><option value="en-GB">English (GB)</option></select></div>
|
||
<div class="form-group"><label>Max. Seiten</label><input type="number" id="maxPages" value="50" min="1" max="500"></div>
|
||
</div>
|
||
<button class="btn-primary" id="startBtn" onclick="startCheck()">Website prüfen</button>
|
||
<div class="error-banner" id="formError"></div>
|
||
</div></div>
|
||
</div>
|
||
|
||
<!-- Progress -->
|
||
<div class="progress-view" id="progressView">
|
||
<div class="container"><div class="progress-card">
|
||
<div class="spinner"></div>
|
||
<div class="progress-text" id="progressText">Starte...</div>
|
||
<div class="progress-detail" id="progressDetail"></div>
|
||
<div class="progress-bar-outer"><div class="progress-bar-inner" id="progressBar"></div></div>
|
||
</div></div>
|
||
</div>
|
||
|
||
<!-- Preview -->
|
||
<div class="preview-view" id="previewView">
|
||
<div class="container"><div class="preview-card">
|
||
<h2>Gefundene Seiten</h2>
|
||
<div class="preview-info" id="previewInfo"></div>
|
||
<div class="preview-actions">
|
||
<button onclick="toggleAllUrls(true)">Alle auswählen</button>
|
||
<button onclick="toggleAllUrls(false)">Keine auswählen</button>
|
||
<span class="preview-count" id="previewCount"></span>
|
||
</div>
|
||
<div class="preview-list" id="previewList"></div>
|
||
<div class="error-banner" id="previewError"></div>
|
||
<div class="preview-buttons">
|
||
<button class="btn-secondary" onclick="showView('form')">Zurück</button>
|
||
<button class="btn-primary" style="flex:1" id="checkSelectedBtn" onclick="startCheckSelected()">Ausgewählte prüfen</button>
|
||
</div>
|
||
</div></div>
|
||
</div>
|
||
|
||
<!-- Results Overview -->
|
||
<div class="results-view" id="resultsView">
|
||
<div class="container">
|
||
<div class="stats-grid" id="statsGrid"></div>
|
||
<div class="results-info" id="resultsInfo"></div>
|
||
<div class="search-bar"><input type="text" id="globalSearch" placeholder="Fehler durchsuchen... (z.B. "dass" oder "Komma")" oninput="onGlobalSearch()"></div>
|
||
<div class="search-results" id="searchResults"></div>
|
||
<div class="page-list" id="pageList"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Detail View -->
|
||
<div class="detail-view" id="detailView">
|
||
<div class="container">
|
||
<a class="detail-url" id="detailUrl" target="_blank" rel="noopener"></a>
|
||
<div class="detail-stats" id="detailStats"></div>
|
||
<div class="detail-toolbar" id="detailToolbar"></div>
|
||
<div id="detailSections"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// ==================== STATE ====================
|
||
let currentSessionId = null, currentResults = null, currentView = 'form';
|
||
let currentDetailIdx = null, currentFilter = 'all', crawledUrls = [];
|
||
let dismissedErrors = new Set(), dismissedPages = new Set();
|
||
|
||
const formView = document.getElementById('formView');
|
||
const previewView = document.getElementById('previewView');
|
||
const progressView = document.getElementById('progressView');
|
||
const resultsView = document.getElementById('resultsView');
|
||
const detailView = document.getElementById('detailView');
|
||
const backBtn = document.getElementById('backBtn');
|
||
|
||
function showView(name) {
|
||
currentView = name;
|
||
formView.style.display = name === 'form' ? 'block' : 'none';
|
||
previewView.style.display = name === 'preview' ? 'block' : 'none';
|
||
progressView.style.display = name === 'progress' ? 'block' : 'none';
|
||
resultsView.style.display = name === 'results' ? 'block' : 'none';
|
||
detailView.style.display = name === 'detail' ? 'block' : 'none';
|
||
backBtn.style.display = (name === 'detail' || name === 'results') ? 'inline' : 'none';
|
||
}
|
||
function goBack() {
|
||
if (currentView === 'detail') { renderOverview(); showView('results'); }
|
||
else if (currentView === 'results') showView('form');
|
||
}
|
||
|
||
// ==================== LOCALSTORAGE ====================
|
||
function saveFormCache() {
|
||
localStorage.setItem('wc_form', JSON.stringify({
|
||
domain: document.getElementById('domain').value,
|
||
username: document.getElementById('username').value,
|
||
language: document.getElementById('language').value,
|
||
maxPages: document.getElementById('maxPages').value,
|
||
}));
|
||
}
|
||
function loadFormCache() {
|
||
try {
|
||
const c = JSON.parse(localStorage.getItem('wc_form')); if (!c) return;
|
||
if (c.domain) document.getElementById('domain').value = c.domain;
|
||
if (c.username) document.getElementById('username').value = c.username;
|
||
if (c.language) document.getElementById('language').value = c.language;
|
||
if (c.maxPages) document.getElementById('maxPages').value = c.maxPages;
|
||
} catch (e) {}
|
||
}
|
||
loadFormCache();
|
||
|
||
// ==================== CRAWL ====================
|
||
async function startCheck() {
|
||
const domain = document.getElementById('domain').value.trim();
|
||
const username = document.getElementById('username').value.trim();
|
||
const apiKey = document.getElementById('apiKey').value.trim();
|
||
const maxPages = parseInt(document.getElementById('maxPages').value) || 50;
|
||
document.getElementById('formError').style.display = 'none';
|
||
if (!domain) { showError('Bitte eine Domain eingeben.'); return; }
|
||
if (!username || !apiKey) { showError('Bitte LanguageTool Credentials eingeben.'); return; }
|
||
saveFormCache();
|
||
const btn = document.getElementById('startBtn');
|
||
btn.disabled = true; btn.textContent = 'Crawle Seiten...';
|
||
try {
|
||
const resp = await fetch('/api/crawl', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ domain, maxPages }) });
|
||
const data = await resp.json();
|
||
if (data.error) { showError(data.error); btn.disabled = false; btn.textContent = 'Website prüfen'; return; }
|
||
crawledUrls = data.urls; renderPreview(data); showView('preview');
|
||
} catch (e) { showError('Netzwerkfehler: ' + e.message); }
|
||
btn.disabled = false; btn.textContent = 'Website prüfen';
|
||
}
|
||
function showError(msg) { const el = document.getElementById('formError'); el.textContent = msg; el.style.display = 'block'; }
|
||
|
||
// ==================== PREVIEW ====================
|
||
function renderPreview(data) {
|
||
document.getElementById('previewInfo').textContent = `${data.urls.length} Seiten gefunden via ${data.sitemap_used ? 'Sitemap' : 'Link-Crawling'} auf ${data.domain}`;
|
||
document.getElementById('previewError').style.display = 'none';
|
||
document.getElementById('previewList').innerHTML = data.urls.map((url, i) =>
|
||
`<div class="preview-item"><input type="checkbox" id="url_${i}" checked data-url="${esc(url)}"><label for="url_${i}">${esc(url)}</label></div>`
|
||
).join('');
|
||
updatePreviewCount();
|
||
document.querySelectorAll('#previewList input[type="checkbox"]').forEach(cb => cb.addEventListener('change', updatePreviewCount));
|
||
}
|
||
function updatePreviewCount() {
|
||
const all = document.querySelectorAll('#previewList input[type="checkbox"]');
|
||
const checked = document.querySelectorAll('#previewList input[type="checkbox"]:checked');
|
||
document.getElementById('previewCount').textContent = `${checked.length} / ${all.length} ausgewählt`;
|
||
document.getElementById('checkSelectedBtn').disabled = checked.length === 0;
|
||
}
|
||
function toggleAllUrls(state) { document.querySelectorAll('#previewList input[type="checkbox"]').forEach(cb => cb.checked = state); updatePreviewCount(); }
|
||
|
||
// ==================== CHECK ====================
|
||
async function startCheckSelected() {
|
||
const urls = []; document.querySelectorAll('#previewList input[type="checkbox"]:checked').forEach(cb => urls.push(cb.getAttribute('data-url')));
|
||
if (!urls.length) { document.getElementById('previewError').textContent = 'Bitte mindestens eine Seite auswählen.'; document.getElementById('previewError').style.display = 'block'; return; }
|
||
const domain = document.getElementById('domain').value.trim(), username = document.getElementById('username').value.trim();
|
||
const apiKey = document.getElementById('apiKey').value.trim(), language = document.getElementById('language').value;
|
||
const btn = document.getElementById('checkSelectedBtn'); btn.disabled = true; btn.textContent = 'Starte Prüfung...';
|
||
try {
|
||
const resp = await fetch('/api/check', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ domain, username, apiKey, language, urls }) });
|
||
const data = await resp.json();
|
||
if (data.error) { document.getElementById('previewError').textContent = data.error; document.getElementById('previewError').style.display = 'block'; btn.disabled = false; btn.textContent = 'Ausgewählte prüfen'; return; }
|
||
currentSessionId = data.sessionId; showView('progress'); pollStatus();
|
||
} catch (e) { document.getElementById('previewError').textContent = 'Netzwerkfehler: ' + e.message; document.getElementById('previewError').style.display = 'block'; }
|
||
btn.disabled = false; btn.textContent = 'Ausgewählte prüfen';
|
||
}
|
||
|
||
// ==================== POLLING ====================
|
||
function pollStatus() {
|
||
const evtSource = new EventSource('/api/stream/' + currentSessionId);
|
||
evtSource.onmessage = function(event) {
|
||
const d = JSON.parse(event.data);
|
||
document.getElementById('progressText').textContent = d.message;
|
||
if (d.progress && d.progress.total > 0) {
|
||
document.getElementById('progressBar').style.width = Math.round((d.progress.current / d.progress.total) * 100) + '%';
|
||
document.getElementById('progressDetail').textContent = `Seite ${d.progress.current} von ${d.progress.total}`;
|
||
}
|
||
if (d.status === 'done') { evtSource.close(); loadResults(); }
|
||
else if (d.status === 'error') { evtSource.close(); showView('form'); showError(d.message); document.getElementById('startBtn').disabled = false; document.getElementById('startBtn').textContent = 'Website prüfen'; }
|
||
};
|
||
evtSource.onerror = function() { evtSource.close(); setTimeout(pollViaRest, 1000); };
|
||
}
|
||
async function pollViaRest() {
|
||
try {
|
||
const resp = await fetch('/api/status/' + currentSessionId); const d = await resp.json();
|
||
document.getElementById('progressText').textContent = d.message;
|
||
if (d.progress && d.progress.total > 0) { document.getElementById('progressBar').style.width = Math.round((d.progress.current / d.progress.total) * 100) + '%'; document.getElementById('progressDetail').textContent = `Seite ${d.progress.current} von ${d.progress.total}`; }
|
||
if (d.status === 'done') loadResults(); else if (d.status === 'error') { showView('form'); showError(d.message); } else setTimeout(pollViaRest, 1000);
|
||
} catch (e) { setTimeout(pollViaRest, 2000); }
|
||
}
|
||
|
||
// ==================== RESULTS ====================
|
||
async function loadResults() {
|
||
try {
|
||
const resp = await fetch('/api/results/' + currentSessionId); currentResults = await resp.json();
|
||
if (currentResults.error) { showView('form'); showError(currentResults.error); return; }
|
||
dismissedErrors = new Set(); dismissedPages = new Set();
|
||
renderOverview(); showView('results');
|
||
} catch (e) { showView('form'); showError('Fehler beim Laden der Ergebnisse.'); }
|
||
document.getElementById('startBtn').disabled = false; document.getElementById('startBtn').textContent = 'Website prüfen';
|
||
}
|
||
function renderOverview() {
|
||
const r = currentResults;
|
||
const total = r.total_errors.spelling + r.total_errors.grammar + r.total_errors.style;
|
||
document.getElementById('statsGrid').innerHTML = `
|
||
<div class="stat-card"><div class="stat-number">${r.pages_checked}</div><div class="stat-label">Seiten</div></div>
|
||
<div class="stat-card"><div class="stat-number spelling">${r.total_errors.spelling}</div><div class="stat-label">Rechtschreibung</div></div>
|
||
<div class="stat-card"><div class="stat-number grammar">${r.total_errors.grammar}</div><div class="stat-label">Grammatik</div></div>
|
||
<div class="stat-card"><div class="stat-number style">${r.total_errors.style}</div><div class="stat-label">Stil</div></div>`;
|
||
document.getElementById('resultsInfo').textContent = `${r.domain} — ${r.pages_checked} Seiten geprüft, ${total} Fehler gesamt` + (r.pages_skipped > 0 ? `, ${r.pages_skipped} übersprungen` : '');
|
||
document.getElementById('globalSearch').value = ''; document.getElementById('searchResults').innerHTML = '';
|
||
document.getElementById('pageList').innerHTML = r.pages.map((page, idx) => {
|
||
if (page.skipped) return `<div class="page-item skipped"><span class="page-url">${esc(page.url)}</span><span class="badge error">${esc(page.error_message || 'Übersprungen')}</span></div>`;
|
||
const dm = dismissedPages.has(idx);
|
||
const b = [];
|
||
if (page.error_count.spelling > 0) b.push(`<span class="badge spelling">${page.error_count.spelling}</span>`);
|
||
if (page.error_count.grammar > 0) b.push(`<span class="badge grammar">${page.error_count.grammar}</span>`);
|
||
if (page.error_count.style > 0) b.push(`<span class="badge style">${page.error_count.style}</span>`);
|
||
if (page.total_errors === 0) b.push(`<span class="badge success">Fehlerfrei</span>`);
|
||
if (dm) b.push(`<span class="badge success">Erledigt</span>`);
|
||
const pageIcon = dm
|
||
? '<span class="page-dismiss-icon done" title="Erledigt – klick zum Enthaken" onclick="event.stopPropagation();togglePageDismiss('+idx+')">✓</span>'
|
||
: '<span class="page-dismiss-icon pending" title="Ausstehend – klick zum Abhaken" onclick="event.stopPropagation();togglePageDismiss('+idx+')">✕</span>';
|
||
return `<div class="page-item ${dm?'dismissed':''}" onclick="showDetail(${idx})"><span class="page-url">${esc(page.url)}</span><div class="page-badges">${b.join('')}${pageIcon}</div></div>`;
|
||
}).join('');
|
||
}
|
||
|
||
// ==================== GLOBAL SEARCH ====================
|
||
function onGlobalSearch() {
|
||
const q = document.getElementById('globalSearch').value.trim().toLowerCase(), c = document.getElementById('searchResults');
|
||
if (!q || q.length < 2 || !currentResults) { c.innerHTML = ''; return; }
|
||
const res = [];
|
||
for (let pi = 0; pi < currentResults.pages.length && res.length < 50; pi++) {
|
||
const page = currentResults.pages[pi]; if (page.skipped) continue;
|
||
for (let si = 0; si < page.sections.length && res.length < 50; si++) {
|
||
const sec = page.sections[si]; if (!sec.matches) continue;
|
||
for (let mi = 0; mi < sec.matches.length && res.length < 50; mi++) {
|
||
const m = sec.matches[mi]; if (dismissedErrors.has(`${pi}:${si}:${mi}`)) continue;
|
||
const et = sec.text.substring(m.offset, m.offset + m.length), msg = m.message || '';
|
||
const ctx = sec.text.substring(Math.max(0, m.offset - 20), m.offset + m.length + 20);
|
||
if (et.toLowerCase().includes(q) || msg.toLowerCase().includes(q) || (m.replacements && m.replacements.some(r => r.toLowerCase().includes(q))))
|
||
res.push({ pi, et, ctx, msg, url: page.url });
|
||
}
|
||
}
|
||
}
|
||
if (!res.length) { c.innerHTML = '<div style="font-size:13px;color:var(--text-secondary);padding:8px 0;">Keine Treffer.</div>'; return; }
|
||
c.innerHTML = res.map(r => {
|
||
const hl = r.ctx.replace(new RegExp(`(${escRx(r.et)})`, 'gi'), '<span class="search-highlight">$1</span>');
|
||
return `<div class="search-result-item" onclick="showDetail(${r.pi})"><div class="search-result-url">${esc(r.url)}</div><div class="search-result-context">${hl}</div><div style="font-size:11px;color:var(--text-secondary);margin-top:4px">${esc(r.msg)}</div></div>`;
|
||
}).join('');
|
||
}
|
||
|
||
// ==================== PAGE DISMISS ====================
|
||
function togglePageDismiss(idx) {
|
||
const p = currentResults.pages[idx];
|
||
if (dismissedPages.has(idx)) {
|
||
dismissedPages.delete(idx);
|
||
if (p) p.sections.forEach((s,si) => (s.matches||[]).forEach((m,mi) => dismissedErrors.delete(`${idx}:${si}:${mi}`)));
|
||
} else {
|
||
dismissedPages.add(idx);
|
||
if (p) p.sections.forEach((s,si) => (s.matches||[]).forEach((m,mi) => dismissedErrors.add(`${idx}:${si}:${mi}`)));
|
||
}
|
||
renderOverview();
|
||
}
|
||
|
||
// ==================== DETAIL VIEW ====================
|
||
function showDetail(idx) {
|
||
const page = currentResults.pages[idx]; if (!page || page.skipped) return;
|
||
currentDetailIdx = idx; currentFilter = 'all';
|
||
document.getElementById('detailUrl').href = page.url; document.getElementById('detailUrl').textContent = page.url;
|
||
const s = [];
|
||
if (page.error_count.spelling > 0) s.push(`<span class="badge spelling">${page.error_count.spelling} Rechtschreibung</span>`);
|
||
if (page.error_count.grammar > 0) s.push(`<span class="badge grammar">${page.error_count.grammar} Grammatik</span>`);
|
||
if (page.error_count.style > 0) s.push(`<span class="badge style">${page.error_count.style} Stil</span>`);
|
||
if (page.total_errors === 0) s.push(`<span class="badge success">Fehlerfrei</span>`);
|
||
document.getElementById('detailStats').innerHTML = s.join('');
|
||
renderToolbar(page); renderSections(page, 'all'); showView('detail'); window.scrollTo(0, 0);
|
||
}
|
||
function renderToolbar(page) {
|
||
const bar = document.getElementById('detailToolbar'), c = page.error_count;
|
||
if (page.total_errors === 0) { bar.innerHTML = ''; return; }
|
||
let h = `<button class="filter-btn active" data-filter="all" onclick="setFilter('all')">Alle</button>`;
|
||
if (c.spelling > 0) h += `<button class="filter-btn" data-filter="spelling" onclick="setFilter('spelling')">Rechtschreibung (${c.spelling})</button>`;
|
||
if (c.grammar > 0) h += `<button class="filter-btn" data-filter="grammar" onclick="setFilter('grammar')">Grammatik (${c.grammar})</button>`;
|
||
if (c.style > 0) h += `<button class="filter-btn" data-filter="style" onclick="setFilter('style')">Stil (${c.style})</button>`;
|
||
h += `<span class="toolbar-spacer"></span><button class="dismiss-all-btn" onclick="dismissAllOnPage(${currentDetailIdx})">Alle abhaken</button>`;
|
||
bar.innerHTML = h;
|
||
}
|
||
function setFilter(f) {
|
||
currentFilter = f;
|
||
document.querySelectorAll('.filter-btn').forEach(b => b.classList.toggle('active', b.getAttribute('data-filter') === f));
|
||
renderSections(currentResults.pages[currentDetailIdx], f);
|
||
}
|
||
function dismissAllOnPage(idx) {
|
||
const p = currentResults.pages[idx];
|
||
p.sections.forEach((s, si) => (s.matches || []).forEach((m, mi) => dismissedErrors.add(`${idx}:${si}:${mi}`)));
|
||
dismissedPages.add(idx); renderSections(p, currentFilter); renderToolbar(p);
|
||
}
|
||
function toggleError(pi, si, mi) {
|
||
const key = `${pi}:${si}:${mi}`;
|
||
if (dismissedErrors.has(key)) {
|
||
dismissedErrors.delete(key);
|
||
dismissedPages.delete(pi); // un-dismiss page if any error unchecked
|
||
} else {
|
||
dismissedErrors.add(key);
|
||
// Check if all errors on this page are now dismissed
|
||
const page = currentResults.pages[pi]; let allDone = true;
|
||
page.sections.forEach((s, si2) => (s.matches || []).forEach((m, mi2) => { if (!dismissedErrors.has(`${pi}:${si2}:${mi2}`)) allDone = false; }));
|
||
if (allDone) dismissedPages.add(pi);
|
||
}
|
||
renderSections(currentResults.pages[pi], currentFilter);
|
||
}
|
||
|
||
// ==================== RENDER SECTIONS ====================
|
||
function renderSections(page, filter) {
|
||
const pi = currentDetailIdx, html = [];
|
||
for (let si = 0; si < page.sections.length; si++) {
|
||
const sec = page.sections[si], allM = sec.matches || [];
|
||
const fm = filter === 'all' ? allM : allM.filter(m => m.category === filter);
|
||
if (filter !== 'all' && fm.length === 0) continue;
|
||
|
||
const mws = fm.map(m => { const ri = allM.indexOf(m); return { ...m, dis: dismissedErrors.has(`${pi}:${si}:${ri}`), ri }; });
|
||
const active = mws.filter(m => !m.dis).length;
|
||
const allDone = mws.length > 0 && active === 0;
|
||
const check = (!mws.length || allDone) ? '<span class="section-check">✓</span>' : '';
|
||
|
||
// Build highlighted text with inline checkboxes
|
||
const textHtml = mws.length > 0 ? renderText(sec.text, mws, pi, si) : esc(sec.text);
|
||
|
||
// Build corrections directly in the section header
|
||
let correctionsHtml = '';
|
||
if (mws.length > 0) {
|
||
correctionsHtml = '<div class="section-corrections">' + mws.map(m => {
|
||
const orig = sec.text.substring(m.offset, m.offset + m.length);
|
||
const sugg = m.replacements && m.replacements.length > 0 ? m.replacements[0] : '';
|
||
const dimClass = m.dis ? ' dismissed-item' : '';
|
||
const statusIcon = m.dis
|
||
? '<span class="section-corr-status done">✓</span>'
|
||
: '<span class="section-corr-status pending">✕</span>';
|
||
const copyClick = sugg ? `event.stopPropagation();copyText('${esc(sugg.replace(/'/g, "\\'"))}')` : '';
|
||
return `<div class="section-correction-item${dimClass}" onclick="toggleError(${pi},${si},${m.ri})"><span class="section-corr-marker ${m.category}"></span><span class="section-corr-original">${esc(orig)}</span>${sugg ? `<span class="section-corr-arrow">→</span><span class="section-corr-suggestion" onclick="${copyClick}" title="Klick kopiert">${esc(sugg)}</span>` : ''}${m.replacements && m.replacements.length > 1 ? `<span class="section-corr-extra">(+${m.replacements.length - 1})</span>` : ''}<span class="section-corr-msg">${esc(m.message)}</span>${statusIcon}</div>`;
|
||
}).join('') + '</div>';
|
||
}
|
||
|
||
html.push(`<div class="section-block">
|
||
<div class="section-header"><div class="section-header-left"><span class="section-type-label">${esc(sec.type)}</span>${correctionsHtml}</div>${check}</div>
|
||
<div class="section-body">${textHtml}</div>
|
||
</div>`);
|
||
}
|
||
document.getElementById('detailSections').innerHTML = html.join('');
|
||
}
|
||
|
||
function renderText(text, matches, pi, si) {
|
||
const sorted = [...matches].sort((a, b) => a.offset - b.offset);
|
||
let r = '', last = 0;
|
||
for (const m of sorted) {
|
||
const s = m.offset, e = s + m.length;
|
||
if (s < last) continue;
|
||
r += esc(text.slice(last, s));
|
||
const word = text.slice(s, e);
|
||
const sugg = m.replacements && m.replacements.length > 0 ? m.replacements[0] : '';
|
||
const dc = m.dis ? ' dismissed' : '';
|
||
const clickCopy = sugg && !m.dis ? `onclick="copyText('${esc(sugg.replace(/'/g, "\\'"))}')"` : '';
|
||
const title = sugg && !m.dis ? `title="Klick kopiert: ${esc(sugg)}"` : '';
|
||
r += `<span class="error-mark ${m.category}${dc}" ${clickCopy} ${title}>${esc(word)}</span>`;
|
||
last = e;
|
||
}
|
||
r += esc(text.slice(last));
|
||
return r;
|
||
}
|
||
|
||
// ==================== CLIPBOARD ====================
|
||
function copyText(text) {
|
||
navigator.clipboard.writeText(text).then(() => {
|
||
const t = document.getElementById('copiedToast');
|
||
t.classList.add('show'); setTimeout(() => t.classList.remove('show'), 1500);
|
||
});
|
||
}
|
||
|
||
// ==================== UTILS ====================
|
||
function esc(s) { return s ? s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''') : ''; }
|
||
function escRx(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
|
||
document.querySelectorAll('#formView input').forEach(el => el.addEventListener('keydown', e => { if (e.key === 'Enter') startCheck(); }));
|
||
</script>
|
||
</body>
|
||
</html>
|