1165 lines
40 KiB
HTML
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()">← 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">✓</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, '"');
|
|
|
|
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(/"/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, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
.replace(/"/g, '"').replace(/'/g, ''');
|
|
}
|
|
|
|
// Enter key on form
|
|
document.querySelectorAll('#formView input').forEach(el => {
|
|
el.addEventListener('keydown', e => {
|
|
if (e.key === 'Enter') startCheck();
|
|
});
|
|
});
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|