Files
wordle-cheater/templates/index.html

386 lines
25 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>WordleCheater (DE)</title>
<meta name="description" content="WordleCheater: Finde deutsche 5BuchstabenWörter anhand bekannter Buchstaben und Positionen. Quellen: OpenThesaurus und wordfreq." />
<meta name="robots" content="index,follow" />
<link rel="canonical" href="{{ request.url_root }}" />
<meta property="og:type" content="website" />
<meta property="og:title" content="WordleCheater (DE)" />
<meta property="og:description" content="Finde deutsche 5BuchstabenWörter mit Positions- und Buchstabenfiltern. Quellen: OpenThesaurus & wordfreq." />
<meta property="og:url" content="{{ request.url_root }}" />
<meta name="twitter:card" content="summary" />
<meta name="twitter:title" content="WordleCheater (DE)" />
<meta name="twitter:description" content="Finde deutsche 5BuchstabenWörter mit Positions- und Buchstabenfiltern. Quellen: OpenThesaurus & wordfreq." />
<link rel="icon" type="image/svg+xml" href="{{ url_for('static', filename='favicon.svg') }}" />
<link rel="manifest" href="/manifest.webmanifest" />
<meta name="theme-color" content="#0b1220" />
<meta name="color-scheme" content="light dark" />
<script>
(function() {
try {
var saved = localStorage.getItem('theme');
var prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
var theme = saved || (prefersDark ? 'dark' : 'light');
document.documentElement.setAttribute('data-theme', theme);
} catch (e) {}
})();
</script>
<style>
:root { --bg:#ffffff; --text:#111827; --muted:#6b7280; --badge-bg:#e5e7eb; --badge-text:#111827; --border:#e5e7eb; --skip-bg:#111827; --skip-text:#ffffff; --button-bg:#111827; --button-text:#ffffff; --input-bg:#ffffff; --input-text:#111827; --error:#b91c1c; }
[data-theme="dark"] { --bg:#0b1220; --text:#e5e7eb; --muted:#9ca3af; --badge-bg:#374151; --badge-text:#f9fafb; --border:#334155; --skip-bg:#e5e7eb; --skip-text:#111827; --button-bg:#e5e7eb; --button-text:#111827; --input-bg:#111827; --input-text:#e5e7eb; --error:#ef4444; }
html, body { background: var(--bg); color: var(--text); }
body { font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; margin: 2rem; }
.container { max-width: 800px; margin: 0 auto; }
.page-header { display: flex; align-items: center; gap: .75rem; flex-wrap: wrap; }
.page-header h1 { margin: 0; }
@media (max-width: 480px) { .page-header h1 { font-size: 1.5rem; } }
.grid { display: grid; grid-template-columns: repeat(5, 3rem); gap: .5rem; }
.grid input { text-align: center; font-size: 1.25rem; padding: .4rem; background: var(--input-bg); color: var(--input-text); border: 1px solid var(--border); border-radius: .375rem; font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', 'Monaco', 'Courier New', monospace; text-transform: uppercase; }
.text-input { font-size: 1.1rem; padding: .5rem; background: var(--input-bg); color: var(--input-text); border: 1px solid var(--border); border-radius: .375rem; width: 100%; box-sizing: border-box; font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', 'Monaco', 'Courier New', monospace; text-transform: uppercase; }
.letter-input { width: calc(1ch + 2rem); padding: .5rem 1rem; font-size: 1rem; line-height: 1; background: var(--input-bg); color: var(--input-text); border: 1px solid var(--border); border-radius: .5rem; text-align: center; text-transform: uppercase; font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', 'Monaco', 'Courier New', monospace; }
.plus-button { width: calc(1ch + 2rem); text-align: center; line-height: 1; display: inline-flex; align-items: center; justify-content: center; font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', 'Monaco', 'Courier New', monospace; }
label { font-weight: 600; display: block; margin-top: 1rem; margin-bottom: .25rem; }
.results { margin-top: 1.5rem; }
.results-box { border: 1px solid var(--border); border-radius: .5rem; padding: .75rem; }
.badge { display: inline-block; padding: .25rem .5rem; background: var(--badge-bg); color: var(--badge-text); border-radius: .375rem; margin-right: .25rem; margin-bottom: .25rem; }
#includes-list .badge, #excludes-list .badge { cursor: pointer; }
.source { font-size: .75rem; padding: .1rem .35rem; border-radius: .25rem; margin-left: .25rem; }
.source.ot { background: #dbeafe; color: #1e40af; }
.source.wf { background: #dcfce7; color: #065f46; }
button { margin-top: 1rem; padding: .5rem 1rem; font-size: 1rem; margin-right: 0.5rem; background: var(--button-bg); color: var(--button-text); border: 1px solid var(--border); border-radius: .5rem; cursor: pointer; }
.reset-button { background: var(--muted); }
summary { cursor: pointer; }
.footer { margin-top: 2rem; font-size: .9rem; color: var(--muted); }
.footer a { color: inherit; text-decoration: underline; }
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 1px, 1px); white-space: nowrap; border: 0; }
.skip-link { position: absolute; left: -9999px; top: auto; width: 1px; height: 1px; overflow: hidden; }
.skip-link:focus { position: static; width: auto; height: auto; padding: .5rem .75rem; background: var(--skip-bg); color: var(--skip-text); border-radius: .25rem; }
.hint { margin-top: .25rem; color: var(--muted); font-size: .9rem; }
.inline-controls { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; }
.filter-box { border: 1px solid var(--border); border-radius: .5rem; padding: .5rem .75rem; margin: .5rem 0 1rem; }
.word-list { list-style: none; padding: 0; margin: 0; }
#includes-list, #excludes-list { margin-top: .75rem; }
.word-list li { display: inline-block; margin: 0 .25rem .25rem 0; }
fieldset { border: 1px solid var(--border); border-radius: .5rem; padding: .75rem; }
legend { font-weight: 700; padding: 0 .25rem; }
.theme-toggle { background: var(--button-bg); color: var(--button-text); border: 1px solid var(--border); border-radius: .5rem; padding: .4rem .6rem; font-size: .95rem; margin-top: 0; }
.theme-toggle:focus { outline: 2px solid #3b82f6; outline-offset: 2px; }
.inline-controls .plus-button { margin-top: 0; margin-right: 0; }
.drop-target { outline: 2px dashed #3b82f6; outline-offset: 2px; }
</style>
</head>
<body>
<a href="#results" class="skip-link">Zum Ergebnisbereich springen</a>
<div class="container">
<header class="page-header">
<button id="theme-toggle" class="theme-toggle" aria-pressed="false" aria-label="Theme umschalten" title="Theme umschalten">🌞/🌙</button>
<h1>WordleCheater (Deutsch)</h1>
</header>
<p>Wortliste geladen: <strong>{{ words_count }}</strong> Wörter</p>
<main id="main" role="main">
<form id="search-form" method="post" aria-describedby="form-hint">
<p id="form-hint" class="hint">Gib bekannte Buchstaben ein. Leere Felder werden ignoriert.</p>
<fieldset>
<legend>Buchstaben mit korrekter Position</legend>
<div class="grid" aria-describedby="pos-hint">
<input id="pos1" name="pos1" maxlength="1" aria-label="Position 1" inputmode="text" autocomplete="off" pattern="[A-Za-zÄÖÜäöüß]" value="{{ pos[0] }}" />
<input id="pos2" name="pos2" maxlength="1" aria-label="Position 2" inputmode="text" autocomplete="off" pattern="[A-Za-zÄÖÜäöüß]" value="{{ pos[1] }}" />
<input id="pos3" name="pos3" maxlength="1" aria-label="Position 3" inputmode="text" autocomplete="off" pattern="[A-Za-zÄÖÜäöüß]" value="{{ pos[2] }}" />
<input id="pos4" name="pos4" maxlength="1" aria-label="Position 4" inputmode="text" autocomplete="off" pattern="[A-Za-zÄÖÜäöüß]" value="{{ pos[3] }}" />
<input id="pos5" name="pos5" maxlength="1" aria-label="Position 5" inputmode="text" autocomplete="off" pattern="[A-Za-zÄÖÜäöüß]" value="{{ pos[4] }}" />
</div>
<p id="pos-hint" class="hint">Je Feld genau ein Buchstabe. Umlaute (ä, ö, ü) und ß sind erlaubt.</p>
</fieldset>
<label for="includes-input-one">Weitere enthaltene Buchstaben (beliebige Reihenfolge)</label>
<div class="inline-controls" aria-describedby="includes-hint">
<input id="includes-input-one" maxlength="1" aria-label="Buchstabe hinzufügen (enthalten)" inputmode="text" autocomplete="off" pattern="[A-Za-zÄÖÜäöüß]" class="letter-input" />
<button type="button" id="includes-add-button" class="plus-button" aria-label="Buchstabe zu 'Enthalten' hinzufügen">+</button>
<input type="hidden" id="includes" name="includes" value="{{ includes }}" />
</div>
<ul id="includes-list" class="word-list" aria-live="polite"></ul>
<p id="includes-hint" class="hint">Gib einen Buchstaben ein und füge ihn mit „+“ zur Liste hinzu.</p>
<label for="excludes-input-one">Ausgeschlossene Buchstaben</label>
<div class="inline-controls" aria-describedby="excludes-hint">
<input id="excludes-input-one" maxlength="1" aria-label="Buchstabe hinzufügen (ausschließen)" inputmode="text" autocomplete="off" pattern="[A-Za-zÄÖÜäöüß]" class="letter-input" />
<button type="button" id="excludes-add-button" class="plus-button" aria-label="Buchstabe zu 'Ausschließen' hinzufügen">+</button>
<input type="hidden" id="excludes" name="excludes" value="{{ excludes }}" />
</div>
<ul id="excludes-list" class="word-list" aria-live="polite"></ul>
<p id="excludes-hint" class="hint">Gib einen Buchstaben ein und füge ihn mit „+“ zur Liste hinzu.</p>
<button type="submit">Suchen</button>
<button type="button" id="reset-button" class="reset-button">Zurücksetzen</button>
</form>
{% if results is not none %}
<div class="results" id="results" role="region" aria-labelledby="results-title">
<h2 id="results-title">Vorschläge (<span id="visible-count">{{ results|length }}</span>)</h2>
<fieldset class="filter-box" role="group">
<legend>WortquellenFilter</legend>
<div class="inline-controls">
<label><input id="filter-ot" type="checkbox" name="use_ot" form="search-form" {% if use_ot %}checked{% endif %}/> OT (OpenThesaurus)</label>
<label><input id="filter-wf" type="checkbox" name="use_wf" form="search-form" {% if use_wf %}checked{% endif %}/> WF (wordfreq)</label>
</div>
</fieldset>
<fieldset class="filter-box" role="group">
<legend>Umlaute</legend>
<div class="inline-controls">
<label><input id="filter-umlaut" type="checkbox" /> Umlaute einbeziehen (ä, ö, ü, ß)</label>
</div>
</fieldset>
<div class="results-box">
{% if results|length == 0 %}
<p>Keine Treffer. Bitte Bedingungen anpassen.</p>
{% else %}
<details open>
<summary>Liste anzeigen</summary>
<ul class="word-list">
{% for w in results %}
{% set srcs = sources_map.get(w, []) %}
{% set has_umlaut = ('ä' in w or 'ö' in w or 'ü' in w or 'ß' in w) %}
<li data-sources="{{ srcs|join(' ') }}" data-umlaut="{{ 1 if has_umlaut else 0 }}">
<span class="badge">{{ w }}
{% for s in srcs %}
{% if s == 'ot' %}
<span class="source ot">OT</span>
{% elif s == 'wf' %}
<span class="source wf">WF</span>
{% endif %}
{% endfor %}
</span>
</li>
{% endfor %}
</ul>
</details>
{% endif %}
</div>
<p>
<strong>Legende:</strong>
<span class="source wf">WF</span> = <a href="https://pypi.org/project/wordfreq/" target="_blank" rel="noopener noreferrer">Wordfreq</a>,
<span class="source ot">OT</span> = <a href="https://www.openthesaurus.de" target="_blank" rel="noopener noreferrer">OpenThesaurus</a>
</p>
</div>
{% endif %}
</main>
<footer class="footer">
Yet another <a href="https://gitea.elpatron.me/elpatron/wordle-cheater" rel="noopener noreferrer">Open Source Project</a>, vibe coded in less than 1 hour by <a href="mailto:elpatron@mailbox.org">Markus F.J. Busche</a>
</footer>
</div>
<script>
(function() {
var btn = document.getElementById('theme-toggle');
if (!btn) return;
function currentTheme() { return document.documentElement.getAttribute('data-theme') || 'light'; }
function setTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
try { localStorage.setItem('theme', theme); } catch (e) {}
btn.setAttribute('aria-pressed', theme === 'dark');
btn.textContent = theme === 'dark' ? '🌙' : '🌞';
btn.title = 'Theme umschalten (' + (theme === 'dark' ? 'Dunkel' : 'Hell') + ')';
}
setTheme(currentTheme());
btn.addEventListener('click', function() {
var next = currentTheme() === 'dark' ? 'light' : 'dark';
setTheme(next);
});
})();
</script>
<script>
function applyFilters() {
var ot = document.getElementById('filter-ot');
var wf = document.getElementById('filter-wf');
var uml = document.getElementById('filter-umlaut');
if (!ot || !wf) return;
var allowed = [];
if (ot.checked) allowed.push('ot');
if (wf.checked) allowed.push('wf');
var items = document.querySelectorAll('.word-list li');
var visible = 0;
items.forEach(function(li) {
var sources = (li.dataset.sources || '').split(/\s+/).filter(Boolean);
var hasUmlaut = li.dataset.umlaut === '1';
var showSource = allowed.length === 0 ? false : sources.some(function(s){ return allowed.indexOf(s) !== -1; });
var showUmlaut = uml && uml.checked ? true : !hasUmlaut; // an => alle, aus => ohne Umlaute
var show = showSource && showUmlaut;
li.style.display = show ? '' : 'none';
if (show) visible++;
});
var countEl = document.getElementById('visible-count');
if (countEl) countEl.textContent = String(visible);
}
function updateLettersList(listElementId, lettersString) {
var ul = document.getElementById(listElementId);
if (!ul) return;
while (ul.firstChild) ul.removeChild(ul.firstChild);
var letters = (lettersString || '')
.split('')
.filter(function(ch){ return !!ch; })
.map(function(ch){ return ch.toLowerCase(); })
.sort(function(a,b){ return a.localeCompare(b, 'de'); });
letters.forEach(function(ch){
var li = document.createElement('li');
var badge = document.createElement('span');
badge.className = 'badge';
var lower = ch.toLowerCase();
badge.textContent = lower.toUpperCase();
badge.setAttribute('data-letter', lower);
badge.title = 'Zum Entfernen klicken';
badge.setAttribute('aria-label', "Buchstabe '" + lower.toUpperCase() + "' entfernen");
// Drag & Drop für Includes-Liste aktivieren
if (listElementId === 'includes-list') {
badge.draggable = true;
badge.addEventListener('dragstart', function(e){
try {
e.dataTransfer.setData('text/plain', lower);
e.dataTransfer.setData('text/list', listElementId);
e.dataTransfer.effectAllowed = 'move';
} catch (err) {}
});
}
li.appendChild(badge);
ul.appendChild(li);
});
}
function removeLetterFromList(hiddenId, listId, letter) {
var hidden = document.getElementById(hiddenId);
if (!hidden) return;
var current = hidden.value || '';
var next = (current || '').split('').filter(function(ch){ return ch !== letter; }).join('');
hidden.value = next;
updateLettersList(listId, next);
}
function addLetterFromInput(inputId, hiddenId, listId) {
var input = document.getElementById(inputId);
var hidden = document.getElementById(hiddenId);
if (!input || !hidden) return;
var raw = (input.value || '').trim();
if (!raw) return;
var ch = raw[0];
if (!/[A-Za-zÄÖÜäöüß]/.test(ch)) { input.value = ''; return; }
var lower = ch.toLowerCase();
var current = hidden.value || '';
if (current.indexOf(lower) === -1) {
hidden.value = current + lower;
updateLettersList(listId, hidden.value);
}
input.value = '';
input.focus();
}
window.addEventListener('load', function() {
var ot = document.getElementById('filter-ot');
var wf = document.getElementById('filter-wf');
var uml = document.getElementById('filter-umlaut');
if (ot) ot.addEventListener('change', applyFilters);
if (wf) wf.addEventListener('change', applyFilters);
if (uml) uml.addEventListener('change', applyFilters);
applyFilters();
// Listen initial aus Hidden-Feldern rendern
var hiddenInc = document.getElementById('includes');
var hiddenExc = document.getElementById('excludes');
updateLettersList('includes-list', hiddenInc ? hiddenInc.value : '');
updateLettersList('excludes-list', hiddenExc ? hiddenExc.value : '');
// Add-Buttons verdrahten
var addIncBtn = document.getElementById('includes-add-button');
if (addIncBtn) addIncBtn.addEventListener('click', function(){ addLetterFromInput('includes-input-one', 'includes', 'includes-list'); });
var addExcBtn = document.getElementById('excludes-add-button');
if (addExcBtn) addExcBtn.addEventListener('click', function(){ addLetterFromInput('excludes-input-one', 'excludes', 'excludes-list'); });
// Enter-Handling in den Ein-Feld-Eingaben
var incInputOne = document.getElementById('includes-input-one');
if (incInputOne) incInputOne.addEventListener('keydown', function(e){ if (e.key === 'Enter') { e.preventDefault(); addLetterFromInput('includes-input-one', 'includes', 'includes-list'); } });
var excInputOne = document.getElementById('excludes-input-one');
if (excInputOne) excInputOne.addEventListener('keydown', function(e){ if (e.key === 'Enter') { e.preventDefault(); addLetterFromInput('excludes-input-one', 'excludes', 'excludes-list'); } });
// Entfernen per Klick auf Badge (Event Delegation)
var incList = document.getElementById('includes-list');
if (incList) incList.addEventListener('click', function(e){
var t = e.target;
if (t && t.classList && t.classList.contains('badge')) {
var letter = (t.getAttribute('data-letter') || t.textContent || '').trim().toLowerCase();
if (letter) removeLetterFromList('includes', 'includes-list', letter);
}
});
var excList = document.getElementById('excludes-list');
if (excList) excList.addEventListener('click', function(e){
var t = e.target;
if (t && t.classList && t.classList.contains('badge')) {
var letter = (t.getAttribute('data-letter') || t.textContent || '').trim().toLowerCase();
if (letter) removeLetterFromList('excludes', 'excludes-list', letter);
}
});
// Drop-Ziele für Positionsfelder (pos1..pos5)
for (var i = 1; i <= 5; i++) {
(function(idx){
var input = document.getElementById('pos' + idx);
if (!input) return;
input.addEventListener('dragover', function(e){
e.preventDefault();
try { e.dataTransfer.dropEffect = 'move'; } catch (err) {}
input.classList.add('drop-target');
});
input.addEventListener('dragleave', function(){ input.classList.remove('drop-target'); });
input.addEventListener('drop', function(e){
e.preventDefault();
input.classList.remove('drop-target');
var letter = (e.dataTransfer.getData('text/plain') || '').trim().toLowerCase();
var sourceList = e.dataTransfer.getData('text/list') || '';
if (letter && sourceList === 'includes-list' && /[a-zäöüß]/i.test(letter)) {
input.value = letter;
removeLetterFromList('includes', 'includes-list', letter);
input.focus();
}
});
})(i);
}
// Reset-Button Funktionalität
var resetButton = document.getElementById('reset-button');
if (resetButton) {
resetButton.addEventListener('click', function() {
// Alle Positionsfelder zurücksetzen
for (var i = 1; i <= 5; i++) {
var posField = document.getElementById('pos' + i);
if (posField) posField.value = '';
}
// Weitere Felder zurücksetzen
var includesField = document.getElementById('includes');
if (includesField) includesField.value = '';
var includesList = document.getElementById('includes-list');
if (includesList) includesList.innerHTML = '';
var incInput = document.getElementById('includes-input-one');
if (incInput) incInput.value = '';
var excludesField = document.getElementById('excludes');
if (excludesField) excludesField.value = '';
var excludesList = document.getElementById('excludes-list');
if (excludesList) excludesList.innerHTML = '';
var excInput = document.getElementById('excludes-input-one');
if (excInput) excInput.value = '';
// Suchergebnisse ausblenden
var resultsDiv = document.getElementById('results');
if (resultsDiv) {
resultsDiv.style.display = 'none';
}
});
}
});
</script>
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
navigator.serviceWorker.register('/sw.js');
});
}
</script>
</body>
</html>