Files
Text-Check-Webapp/website-checker/templates/index.html
s4luorth 8b844b8291 first
2026-02-07 13:00:51 +01:00

1165 lines
40 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 */
.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;
color: var(--text);
}
.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);
transition: border-color 0.15s;
}
.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;
transition: background 0.15s;
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 message */
.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;
}
.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;
transition: border-color 0.15s, box-shadow 0.15s;
}
.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-url {
font-size: 14px;
color: var(--text);
word-break: break-all;
flex: 1;
margin-right: 12px;
}
.page-badges { display: flex; gap: 6px; flex-shrink: 0; }
.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); }
/* 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;
}
.filter-bar {
display: flex;
gap: 8px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.filter-btn {
padding: 6px 14px;
border: 2px solid var(--border);
border-radius: 20px;
background: var(--surface);
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.15s;
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); }
.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: center;
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);
}
.section-check {
color: var(--success);
font-size: 14px;
}
.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 highlights */
.error-mark {
position: relative;
cursor: help;
padding: 1px 2px;
border-radius: 2px;
}
.error-mark.spelling {
background: var(--spelling-bg);
border-bottom: 2px solid var(--spelling);
text-decoration-color: 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);
}
/* Tooltip */
.tooltip {
display: none;
position: fixed;
background: var(--text);
color: white;
padding: 12px 16px;
border-radius: var(--radius);
font-size: 13px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
max-width: 360px;
z-index: 1000;
box-shadow: 0 8px 24px rgba(0,0,0,0.2);
line-height: 1.5;
pointer-events: none;
}
.tooltip.visible { display: block; }
.tooltip-category {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 4px;
opacity: 0.7;
}
.tooltip-message { margin-bottom: 6px; }
.tooltip-suggestion {
background: rgba(255,255,255,0.15);
padding: 4px 8px;
border-radius: 4px;
font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
display: inline-block;
margin-top: 2px;
}
/* Preview View */
.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); }
/* Responsive */
@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>
<!-- Header -->
<div class="header">
<div class="container header-inner">
<h1>LanguageTool Website Checker</h1>
<a class="header-back" id="backBtn" onclick="goBack()">&#8592; Zurück zur Übersicht</a>
<div class="legend" id="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>
<!-- Tooltip -->
<div class="tooltip" id="tooltip"></div>
<!-- Form View -->
<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 View -->
<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 View -->
<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="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="filter-bar" id="filterBar"></div>
<div id="detailSections"></div>
</div>
</div>
<script>
// State
let currentSessionId = null;
let currentResults = null;
let currentView = 'form'; // form, preview, progress, results, detail
let currentDetailIdx = null;
let currentFilter = 'all'; // all, spelling, grammar, style
let crawledUrls = []; // URLs from crawl preview
// Views
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');
// --- localStorage cache (NOT the API key) ---
function saveFormCache() {
const cache = {
domain: document.getElementById('domain').value,
username: document.getElementById('username').value,
language: document.getElementById('language').value,
maxPages: document.getElementById('maxPages').value,
};
localStorage.setItem('wc_form', JSON.stringify(cache));
}
function loadFormCache() {
try {
const cache = JSON.parse(localStorage.getItem('wc_form'));
if (!cache) return;
if (cache.domain) document.getElementById('domain').value = cache.domain;
if (cache.username) document.getElementById('username').value = cache.username;
if (cache.language) document.getElementById('language').value = cache.language;
if (cache.maxPages) document.getElementById('maxPages').value = cache.maxPages;
} catch (e) {}
}
loadFormCache();
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') ? 'inline' : 'none';
}
function goBack() {
if (currentView === 'detail') showView('results');
}
// Step 1: Crawl and show preview
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;
const errorEl = document.getElementById('formError');
errorEl.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';
}
// Preview rendering
function renderPreview(data) {
const info = `${data.urls.length} Seiten gefunden via ${data.sitemap_used ? 'Sitemap' : 'Link-Crawling'} auf ${data.domain}`;
document.getElementById('previewInfo').textContent = info;
document.getElementById('previewError').style.display = 'none';
const listHtml = data.urls.map((url, i) =>
`<div class="preview-item">
<input type="checkbox" id="url_${i}" checked data-url="${escapeHtml(url)}">
<label for="url_${i}">${escapeHtml(url)}</label>
</div>`
).join('');
document.getElementById('previewList').innerHTML = listHtml;
updatePreviewCount();
// Listen for checkbox changes
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();
}
// Step 2: Check selected URLs
async function startCheckSelected() {
const selectedUrls = [];
document.querySelectorAll('#previewList input[type="checkbox"]:checked').forEach(cb => {
selectedUrls.push(cb.getAttribute('data-url'));
});
if (selectedUrls.length === 0) {
document.getElementById('previewError').textContent = 'Bitte mindestens eine Seite auswählen.';
document.getElementById('previewError').style.display = 'block';
return;
}
const domain = document.getElementById('domain').value.trim();
const username = document.getElementById('username').value.trim();
const apiKey = document.getElementById('apiKey').value.trim();
const 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: selectedUrls }),
});
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';
}
function showError(msg) {
const el = document.getElementById('formError');
el.textContent = msg;
el.style.display = 'block';
}
// Poll status via SSE
function pollStatus() {
const evtSource = new EventSource('/api/stream/' + currentSessionId);
evtSource.onmessage = function(event) {
const data = JSON.parse(event.data);
document.getElementById('progressText').textContent = data.message;
if (data.progress && data.progress.total > 0) {
const pct = Math.round((data.progress.current / data.progress.total) * 100);
document.getElementById('progressBar').style.width = pct + '%';
document.getElementById('progressDetail').textContent =
`Seite ${data.progress.current} von ${data.progress.total}`;
}
if (data.status === 'done') {
evtSource.close();
loadResults();
} else if (data.status === 'error') {
evtSource.close();
showView('form');
const btn = document.getElementById('startBtn');
btn.disabled = false;
btn.textContent = 'Website prüfen';
showError(data.message);
}
};
evtSource.onerror = function() {
evtSource.close();
// Fallback: poll via REST
setTimeout(() => pollViaRest(), 1000);
};
}
async function pollViaRest() {
try {
const resp = await fetch('/api/status/' + currentSessionId);
const data = await resp.json();
document.getElementById('progressText').textContent = data.message;
if (data.progress && data.progress.total > 0) {
const pct = Math.round((data.progress.current / data.progress.total) * 100);
document.getElementById('progressBar').style.width = pct + '%';
document.getElementById('progressDetail').textContent =
`Seite ${data.progress.current} von ${data.progress.total}`;
}
if (data.status === 'done') {
loadResults();
} else if (data.status === 'error') {
showView('form');
const btn = document.getElementById('startBtn');
btn.disabled = false;
btn.textContent = 'Website prüfen';
showError(data.message);
} else {
setTimeout(() => pollViaRest(), 1000);
}
} catch (e) {
setTimeout(() => pollViaRest(), 2000);
}
}
// Load and render 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;
}
renderOverview();
showView('results');
} catch (e) {
showView('form');
showError('Fehler beim Laden der Ergebnisse.');
}
// Reset form button
const btn = document.getElementById('startBtn');
btn.disabled = false;
btn.textContent = 'Website prüfen';
}
function renderOverview() {
const r = currentResults;
const totalErrors = r.total_errors.spelling + r.total_errors.grammar + r.total_errors.style;
// Stats
document.getElementById('statsGrid').innerHTML = `
<div class="stat-card">
<div class="stat-number">${r.pages_checked}</div>
<div class="stat-label">Seiten geprüft</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>
`;
// Info line
document.getElementById('resultsInfo').textContent =
`${r.domain}${r.pages_checked} Seiten geprüft, ${totalErrors} Fehler gesamt` +
(r.pages_skipped > 0 ? `, ${r.pages_skipped} übersprungen` : '');
// Page list
const listHtml = r.pages.map((page, idx) => {
if (page.skipped) {
return `<div class="page-item skipped">
<span class="page-url">${escapeHtml(page.url)}</span>
<span class="badge error">${escapeHtml(page.error_message || 'Übersprungen')}</span>
</div>`;
}
const badges = [];
if (page.error_count.spelling > 0)
badges.push(`<span class="badge spelling">${page.error_count.spelling} Rechtschr.</span>`);
if (page.error_count.grammar > 0)
badges.push(`<span class="badge grammar">${page.error_count.grammar} Grammatik</span>`);
if (page.error_count.style > 0)
badges.push(`<span class="badge style">${page.error_count.style} Stil</span>`);
if (page.total_errors === 0)
badges.push(`<span class="badge success">Fehlerfrei</span>`);
return `<div class="page-item" onclick="showDetail(${idx})">
<span class="page-url">${escapeHtml(page.url)}</span>
<div class="page-badges">${badges.join('')}</div>
</div>`;
}).join('');
document.getElementById('pageList').innerHTML = listHtml;
}
function showDetail(pageIdx) {
const page = currentResults.pages[pageIdx];
if (!page || page.skipped) return;
currentDetailIdx = pageIdx;
currentFilter = 'all';
// URL
const urlEl = document.getElementById('detailUrl');
urlEl.href = page.url;
urlEl.textContent = page.url;
// Stats
const statsHtml = [];
if (page.error_count.spelling > 0)
statsHtml.push(`<span class="badge spelling">${page.error_count.spelling} Rechtschreibung</span>`);
if (page.error_count.grammar > 0)
statsHtml.push(`<span class="badge grammar">${page.error_count.grammar} Grammatik</span>`);
if (page.error_count.style > 0)
statsHtml.push(`<span class="badge style">${page.error_count.style} Stil</span>`);
if (page.total_errors === 0)
statsHtml.push(`<span class="badge success">Alle Abschnitte fehlerfrei</span>`);
document.getElementById('detailStats').innerHTML = statsHtml.join('');
// Filter buttons
renderFilterBar(page);
// Sections
renderDetailSections(page, 'all');
showView('detail');
window.scrollTo(0, 0);
}
function renderFilterBar(page) {
const bar = document.getElementById('filterBar');
const counts = page.error_count;
const hasAny = page.total_errors > 0;
if (!hasAny) {
bar.innerHTML = '';
return;
}
let html = `<button class="filter-btn active" data-filter="all" onclick="setFilter('all')">Alle Fehler</button>`;
if (counts.spelling > 0)
html += `<button class="filter-btn" data-filter="spelling" onclick="setFilter('spelling')">Rechtschreibung (${counts.spelling})</button>`;
if (counts.grammar > 0)
html += `<button class="filter-btn" data-filter="grammar" onclick="setFilter('grammar')">Grammatik (${counts.grammar})</button>`;
if (counts.style > 0)
html += `<button class="filter-btn" data-filter="style" onclick="setFilter('style')">Stil (${counts.style})</button>`;
bar.innerHTML = html;
}
function setFilter(filter) {
currentFilter = filter;
const page = currentResults.pages[currentDetailIdx];
// Update active button
document.querySelectorAll('.filter-btn').forEach(btn => {
btn.classList.toggle('active', btn.getAttribute('data-filter') === filter);
});
renderDetailSections(page, filter);
}
function renderDetailSections(page, filter) {
const sectionsHtml = [];
for (const section of page.sections) {
const allMatches = section.matches || [];
// Filter matches by category
const filteredMatches = filter === 'all'
? allMatches
: allMatches.filter(m => m.category === filter);
// If filtering by type, skip sections that have no matches of that type
if (filter !== 'all' && filteredMatches.length === 0) continue;
const hasErrors = filteredMatches.length > 0;
const checkMark = !hasErrors ? '<span class="section-check">&#10003;</span>' : '';
const errorCount = hasErrors
? `<span style="font-size:11px;color:var(--spelling)">${filteredMatches.length} Fehler</span>`
: '';
let bodyHtml;
if (hasErrors) {
bodyHtml = renderHighlightedText(section.text, filteredMatches);
} else {
bodyHtml = escapeHtml(section.text);
}
sectionsHtml.push(`<div class="section-block">
<div class="section-header">
<span>${escapeHtml(section.type)}</span>
<span>${errorCount}${checkMark}</span>
</div>
<div class="section-body">${bodyHtml}</div>
</div>`);
}
document.getElementById('detailSections').innerHTML = sectionsHtml.join('');
}
function renderHighlightedText(text, matches) {
if (!matches || matches.length === 0) return escapeHtml(text);
// Sort matches by offset
const sorted = [...matches].sort((a, b) => a.offset - b.offset);
let result = '';
let lastIdx = 0;
for (const match of sorted) {
const start = match.offset;
const end = start + match.length;
// Skip overlapping matches
if (start < lastIdx) continue;
// Text before match
result += escapeHtml(text.slice(lastIdx, start));
// Matched text
const matchedText = text.slice(start, end);
const categoryLabel = {
spelling: 'Rechtschreibung',
grammar: 'Grammatik',
style: 'Stil',
}[match.category] || match.category;
const suggestions = match.replacements && match.replacements.length > 0
? match.replacements.join(', ') : '';
const tooltipData = JSON.stringify({
category: categoryLabel,
message: match.message,
suggestions: suggestions,
}).replace(/"/g, '&quot;');
result += `<span class="error-mark ${match.category}" data-tooltip="${tooltipData}" onmouseenter="showTooltip(event, this)" onmouseleave="hideTooltip()">${escapeHtml(matchedText)}</span>`;
lastIdx = end;
}
// Remaining text
result += escapeHtml(text.slice(lastIdx));
return result;
}
// Tooltip
const tooltipEl = document.getElementById('tooltip');
function showTooltip(event, el) {
const data = JSON.parse(el.getAttribute('data-tooltip').replace(/&quot;/g, '"'));
let html = `<div class="tooltip-category">${escapeHtml(data.category)}</div>`;
html += `<div class="tooltip-message">${escapeHtml(data.message)}</div>`;
if (data.suggestions) {
html += `<div class="tooltip-suggestion">${escapeHtml(data.suggestions)}</div>`;
}
tooltipEl.innerHTML = html;
tooltipEl.classList.add('visible');
// Position
const rect = el.getBoundingClientRect();
let left = rect.left;
let top = rect.bottom + 8;
// Keep in viewport
const tipRect = tooltipEl.getBoundingClientRect();
if (left + 360 > window.innerWidth) left = window.innerWidth - 370;
if (left < 10) left = 10;
if (top + tipRect.height > window.innerHeight) top = rect.top - tipRect.height - 8;
tooltipEl.style.left = left + 'px';
tooltipEl.style.top = top + 'px';
}
function hideTooltip() {
tooltipEl.classList.remove('visible');
}
function escapeHtml(str) {
if (!str) return '';
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/"/g, '&quot;').replace(/'/g, '&#039;');
}
// Enter key on form
document.querySelectorAll('#formView input').forEach(el => {
el.addEventListener('keydown', e => {
if (e.key === 'Enter') startCheck();
});
});
</script>
</body>
</html>