This commit is contained in:
2026-08-25 14:43:21 +02:00
commit 19c753839a
4 changed files with 415 additions and 0 deletions
+116
View File
@@ -0,0 +1,116 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>IdeaQueue</title>
<style>
:root { color-scheme: light dark; }
body {
margin: 0; min-height: 100vh;
display: flex; align-items: center; justify-content: center;
font: 16px/1.5 system-ui, sans-serif;
background: Canvas; color: CanvasText;
}
main { width: min(680px, 92vw); padding: 2rem 0; }
h1 { font-size: 1.25rem; margin: 0 0 1rem; }
textarea {
width: 100%; min-height: 40vh; padding: .75rem; box-sizing: border-box;
font: inherit; font-family: ui-monospace, monospace;
border: 1px solid color-mix(in srgb, CanvasText 30%, transparent);
border-radius: 8px; background: Canvas; color: inherit; resize: vertical;
}
.row { display: flex; align-items: center; gap: 1rem; margin-top: .75rem; }
button {
font: inherit; padding: .5rem 1.25rem; border-radius: 8px;
border: 1px solid transparent; background: AccentColor; color: AccentColorText;
cursor: pointer;
}
button:disabled { opacity: .5; cursor: default; }
#status { font-size: .875rem; opacity: .75; }
#status.error { color: #d33; opacity: 1; }
#count { margin-left: auto; font-size: .875rem; opacity: .6; font-variant-numeric: tabular-nums; }
#count.over { color: #d33; opacity: 1; }
</style>
</head>
<body>
<main>
<h1>IdeaQueue</h1>
<form id="form">
<textarea id="text" placeholder="Idee eintippen … (Strg+Enter zum Speichern)" autofocus></textarea>
<div class="row">
<button id="submit" type="submit">Speichern</button>
<span id="status" role="status" aria-live="polite"></span>
<span id="count"></span>
</div>
</form>
</main>
<script>
const KEY = 'ideaqueue.draft';
const MAX = 5000;
const form = document.getElementById('form');
const text = document.getElementById('text');
const submit = document.getElementById('submit');
const status = document.getElementById('status');
const count = document.getElementById('count');
// Entwurf wiederherstellen und bei jeder Änderung sichern.
try { text.value = localStorage.getItem(KEY) || ''; } catch {}
text.addEventListener('input', () => {
try { localStorage.setItem(KEY, text.value); } catch {}
render();
});
// Zeichen zählen wie der Server: Codepoints, nicht UTF-16-Einheiten.
function len(s) { return [...s].length; }
function render() {
const n = len(text.value);
count.textContent = n + ' / ' + MAX;
count.classList.toggle('over', n > MAX);
submit.disabled = n > MAX;
}
function setStatus(msg, isError) {
status.textContent = msg;
status.classList.toggle('error', !!isError);
}
text.addEventListener('keydown', e => {
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') form.requestSubmit();
});
render();
form.addEventListener('submit', async e => {
e.preventDefault();
const body = text.value;
if (!body.trim()) { setStatus('Nichts zu speichern.', true); return; }
if (len(body) > MAX) { setStatus('Zu lang: ' + len(body) + ' von max ' + MAX + ' Zeichen.', true); return; }
submit.disabled = true;
setStatus('Speichere …');
try {
const res = await fetch('api/notes', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: body }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(data.error || ('HTTP ' + res.status));
// Erst nach bestätigtem Speichern leeren.
text.value = '';
try { localStorage.removeItem(KEY); } catch {}
setStatus('Gespeichert als ' + data.filename);
render();
} catch (err) {
setStatus('Fehlgeschlagen: ' + err.message + ' – Text bleibt erhalten.', true);
} finally {
render();
text.focus();
}
});
</script>
</body>
</html>