From 59c84bd248d922c5bb098d63ada946a5e8380d51 Mon Sep 17 00:00:00 2001 From: Dogfather Date: Fri, 18 Sep 2026 17:00:16 +0200 Subject: [PATCH] Videos: Stufe 7 -- aus einem Wunsch wird ein Video, mit Namen "Ein Video aus einem Wunsch zeigt den Namen." Wer sich etwas gewuenscht hat, soll sehen, dass daraus etwas geworden ist -- das ist der Sinn eines Wunschbretts. Ohne die Verbindung ist die Galerie eine Sammlung, und der Wunsch von letzter Woche bleibt unbeantwortet im Raum stehen. DER KNOPF STEHT AM WUNSCH, nicht im Formular oben. Das ist dieselbe Entscheidung wie bei den Kettenknoepfen daneben, aus demselben Grund (dort woertlich): "Wer ein Formular ausfuellt, denkt nicht an den Wunsch von letzter Woche." Hier denkt er an nichts anderes. DAS LECK, das es nicht geben darf: Der Titel der Quelle wird auf der Kachel ANGEZEIGT. Eine Kette in die Content-Planung waere deshalb kein Schoenheitsfehler, sondern ein Weg, interne Arbeit in die Community zu tragen. Die Quelle muss ein Community-Brett sein -- dieselbe Regel wie beim "Daraus"-Knopf. ZWEI WEGE, EIN ZIEL: "Daraus ein Highlight machen" gibt es laenger. Wer ihn geht und DANACH das Video einfuegt, bekaeme sonst ein zweites Highlight aus demselben Wunsch -- zwei Wege zum selben Ziel, die nichts voneinander wissen. Das Video haengt sich jetzt an den vorhandenen Eintrag. Der Titel bleibt dabei der des Wunsches: Er stammt von einem Menschen, der von TikTok ist eine Bildunterschrift mit Schlagwoertern. Geprueft: pruef-video 51 -> 67. Gegenprobe (Brett-Pruefung und Anhaengen ausgebaut) macht 6 rot, darunter "ein Eintrag aus der Content-Planung kommt NICHT als Quelle durch (201)". UND DAS BILDSCHIRMFOTO HAT ETWAS GEFUNDEN, das keine Zahl gemeldet haette: Das Eingabefeld erschien am ENDE der Karte, unter der Fusszeile -- getrennt von dem Knopf, den man gerade gedrueckt hatte. Alle Pruefungen waren gruen, das Feld war da und reagierte. Jetzt haengt es an der Knopfreihe. Co-Authored-By: Claude Opus 5 --- server/pruef-video.mjs | 125 +++++++++++++++++++++++++++++++ server/workspace-bereiche.js | 14 +++- server/workspace-video.js | 79 ++++++++++++++++--- workspace/app.webmanifest | 8 +- workspace/assets/css/bereich.css | 37 +++++++++ workspace/assets/js/bereich.js | 80 ++++++++++++++++++++ workspace/aufgaben.html | 24 +++--- workspace/automation.html | 26 +++---- workspace/befinden.html | 24 +++--- workspace/bereich.html | 36 ++++----- workspace/bewerben.html | 30 ++++---- workspace/bewerbungen.html | 28 +++---- workspace/calls.html | 28 +++---- workspace/chat.html | 28 +++---- workspace/content.html | 30 ++++---- workspace/crew-index.html | 10 +-- workspace/crew.webmanifest | 8 +- workspace/dateien.html | 26 +++---- workspace/entwicklung.html | 24 +++--- workspace/index.html | 8 +- workspace/kalender.html | 28 +++---- workspace/leistung.html | 26 +++---- workspace/personen.html | 26 +++---- workspace/profil.html | 28 +++---- workspace/rechte.html | 24 +++--- workspace/report.html | 28 +++---- workspace/scouting.html | 26 +++---- workspace/start.html | 24 +++--- workspace/startcheck.html | 28 +++---- workspace/steckbrief.html | 28 +++---- workspace/talente.html | 26 +++---- workspace/team.html | 26 +++---- workspace/teamlage.html | 24 +++--- workspace/teilen.html | 22 +++--- workspace/treff-moderation.html | 24 +++--- workspace/treff-regeln.html | 26 +++---- workspace/uebersicht.html | 26 +++---- workspace/wissen.html | 26 +++---- 38 files changed, 726 insertions(+), 413 deletions(-) diff --git a/server/pruef-video.mjs b/server/pruef-video.mjs index 23007ff9..20315ad9 100644 --- a/server/pruef-video.mjs +++ b/server/pruef-video.mjs @@ -421,6 +421,131 @@ melde("=== 5b. Wenn ein Video bei TikTok verschwindet ==="); void l1; } +/* ======================================================================= + 5c. DER KREISLAUF (Stufe 7) + + „Ein Video aus einem Wunsch zeigt den Namen." Wer sich etwas + gewuenscht hat, soll sehen, dass daraus etwas geworden ist. + + DER WICHTIGSTE TEIL IST DER ZWEITE: Der Titel der Quelle wird auf + der Kachel ANGEZEIGT. Eine Kette in die Content-Planung waere + deshalb kein Schoenheitsfehler, sondern ein Leck -- dort steht + Arbeit, die die Community nichts angeht. + ======================================================================= */ +melde(""); +melde("=== 5c. Aus einem Wunsch wird ein Video ==="); +{ + const db3 = new DatabaseSync(process.env.WORKSPACE_DB); + const nun = new Date().toISOString(); + const anlegen = (bereich, art, titel) => { + const { lastInsertRowid } = db3.prepare(` + INSERT INTO eintraege (bereich, art, titel, datum, status, erstellt, erstellt_von) + VALUES (?,?,?,?, 'offen', ?, (SELECT id FROM personen LIMIT 1))`) + .run(bereich, art, titel, nun.slice(0, 10), nun); + return Number(lastInsertRowid); + }; + const wunschId = anlegen("wunsch", "wunsch", "Bitte mal Casper beim Baden zeigen"); + /* Ein Eintrag aus der Content-Planung -- NICHT aus der Community. + Er ist der Koeder fuer die Leck-Pruefung weiter unten. */ + const internId = anlegen("content", "idee", "Interne Planung: Kampagne Q4"); + + /* ---- Der gute Weg ------------------------------------------------ */ + { + const a = await fetch(`${BASIS}/workspace/api/bereich/highlight/video`, { + method: "POST", headers: kopf, + body: JSON.stringify({ url: videoVon("dogfather0804", "7a"), aus: wunschId }), + }); + const r = await a.json().catch(() => ({})); + ok(a.status === 201 && r.aus === wunschId, + `ein Video zu einem Wunsch wird angenommen (${a.status})`); + + const e = (await liste()).find((x) => x.id === r.id); + ok(e?.aus?.id === wunschId, "der Eintrag kennt seinen Wunsch"); + ok(e?.aus?.titel === "Bitte mal Casper beim Baden zeigen", + `und zeigt dessen Titel (${e?.aus?.titel})`); + ok(!!e?.aus?.von_name, `und den Namen dessen, der ihn geaeussert hat (${e?.aus?.von_name})`); + } + + /* ---- DAS LECK, das es nicht geben darf --------------------------- */ + { + const a = await fetch(`${BASIS}/workspace/api/bereich/highlight/video`, { + method: "POST", headers: kopf, + body: JSON.stringify({ url: videoVon("dogfather0804", "7b"), aus: internId }), + }); + ok(a.status === 404, + `ein Eintrag aus der Content-Planung kommt NICHT als Quelle durch (${a.status})`); + const bleibt = db3.prepare( + "SELECT COUNT(*) AS n FROM eintraege WHERE aus_eintrag_id = ?").get(internId); + ok(bleibt.n === 0, `und es ist auch nichts entstanden (${bleibt.n})`); + } + { + const a = await fetch(`${BASIS}/workspace/api/bereich/highlight/video`, { + method: "POST", headers: kopf, + body: JSON.stringify({ url: videoVon("dogfather0804", "7c"), aus: 999999 }), + }); + ok(a.status === 404, `eine Nummer, die es nicht gibt, ebenfalls nicht (${a.status})`); + } + { + const a = await fetch(`${BASIS}/workspace/api/bereich/highlight/video`, { + method: "POST", headers: kopf, + body: JSON.stringify({ url: videoVon("dogfather0804", "7d"), aus: "irgendwas" }), + }); + ok(a.status === 400, `und etwas, das keine Nummer ist, wird abgewiesen (${a.status})`); + } + + /* ---- ZWEI WEGE, EIN ZIEL ----------------------------------------- */ + { + /* „Daraus ein Highlight machen" gibt es laenger. Wer ihn geht und + DANACH das Video einfuegt, darf kein zweites Highlight bekommen. */ + const wunsch2 = anlegen("wunsch", "wunsch", "Ein Video vom Gassigehen"); + const d = await fetch(`${BASIS}/workspace/api/bereich/wunsch/${wunsch2}/daraus`, { + method: "POST", headers: kopf, body: JSON.stringify({ nach: "highlight" }), + }); + const dr = await d.json().catch(() => ({})); + ok(d.status === 201 && !!dr.id, `erst „Daraus ein Highlight machen" (${d.status})`); + + const v = await fetch(`${BASIS}/workspace/api/bereich/highlight/video`, { + method: "POST", headers: kopf, + body: JSON.stringify({ url: videoVon("dogfather0804", "7e"), aus: wunsch2 }), + }); + const vr = await v.json().catch(() => ({})); + ok(v.status === 201 && vr.id === dr.id && vr.angehaengt === true, + `dann das Video -- es haengt sich an den vorhandenen Eintrag (#${vr.id} vs #${dr.id})`); + + const wieviele = db3.prepare( + "SELECT COUNT(*) AS n FROM eintraege WHERE aus_eintrag_id = ? AND bereich = 'highlight'") + .get(wunsch2); + ok(wieviele.n === 1, + `aus einem Wunsch ist GENAU EIN Highlight geworden, nicht zwei (${wieviele.n})`); + + const e2 = (await liste()).find((x) => x.id === dr.id); + ok(e2?.titel === "Ein Video vom Gassigehen", + `und der Titel bleibt der des Wunsches, nicht der von TikTok (${e2?.titel})`); + ok(!!e2?.quelle_url, "das Video ist trotzdem dran"); + + /* Noch einmal dasselbe: nichts Neues, kein Fehler. */ + const w = await fetch(`${BASIS}/workspace/api/bereich/highlight/video`, { + method: "POST", headers: kopf, + body: JSON.stringify({ url: videoVon("dogfather0804", "7f"), aus: wunsch2 }), + }); + const wr = await w.json().catch(() => ({})); + ok(w.status === 200 && wr.schon === true, + `ein zweites Video zum selben Wunsch aendert nichts (${w.status})`); + + /* Und die Wunschkarte weiss, dass es schon ein Video gibt -- + daran haengt, ob der Knopf noch angeboten wird. */ + const wl = await fetch(`${BASIS}/workspace/api/bereich/wunsch`, { headers: { cookie: keks } }); + const wliste = (await wl.json().catch(() => null))?.eintraege || []; + const karte = wliste.find((x) => x.id === wunsch2); + ok((karte?.wurde || []).some((z) => z.bereich === "highlight" && z.hat_video === true), + "die Wunschkarte sieht, dass daraus ein Highlight MIT Video geworden ist"); + const karte1 = wliste.find((x) => x.id === wunschId); + ok(!!karte1, "und der erste Wunsch steht auch noch in der Liste"); + } + + db3.close(); +} + /* ======================================================================= 6. UND ES ÜBERLEBT, WENN TIKTOK WEG IST ← der eigentliche Punkt ======================================================================= */ diff --git a/server/workspace-bereiche.js b/server/workspace-bereiche.js index 472436e4..49f3388a 100644 --- a/server/workspace-bereiche.js +++ b/server/workspace-bereiche.js @@ -1293,13 +1293,23 @@ bereicheRouter.get("/workspace/api/bereich/:bereich", (req, res) => { } } const p3 = eintraege.map(() => "?").join(", "); + /* `quelle_url` kommt mit, damit die Karte unterscheiden kann: + Aus dem Wunsch ist ein Highlight geworden -- hat es auch schon + ein Video? Sonst waere der Knopf „Video dazu" entweder immer da + (auch wenn es laengst eins gibt) oder nie (sobald irgendein + Highlight daraus entstanden ist). */ const draus = db().prepare( - `SELECT id, bereich, titel, aus_eintrag_id FROM eintraege + `SELECT id, bereich, titel, aus_eintrag_id, quelle_url FROM eintraege WHERE aus_eintrag_id IN (${p3})`).all(...eintraege.map((e) => e.id)); const jeQuelle = new Map(); for (const z of draus) { if (!jeQuelle.has(z.aus_eintrag_id)) jeQuelle.set(z.aus_eintrag_id, []); - jeQuelle.get(z.aus_eintrag_id).push({ id: z.id, bereich: z.bereich, titel: z.titel }); + jeQuelle.get(z.aus_eintrag_id).push({ + id: z.id, bereich: z.bereich, titel: z.titel, + /* Nur ja/nein -- die Adresse selbst gehoert nicht in eine + Liste, die nur „was wurde daraus" beantworten soll. */ + hat_video: !!z.quelle_url, + }); } for (const e of eintraege) e.wurde = jeQuelle.get(e.id) || []; diff --git a/server/workspace-video.js b/server/workspace-video.js index d3ec8179..9447b9f5 100644 --- a/server/workspace-video.js +++ b/server/workspace-video.js @@ -49,6 +49,7 @@ import { KANAELE, kanalVonHandle, TREFF_BRETTER, } from "./workspace.js"; import { darfSchreiben } from "./workspace-treff.js"; +import { sichtbar } from "./workspace-bereiche.js"; import { bildArtVon } from "./workspace-dateien.js"; export const videoRouter = express.Router(); @@ -170,6 +171,36 @@ videoRouter.post("/workspace/api/bereich/:bereich/video", async (req, res) => { return res.status(400).json({ fehler: "Das ist keine TikTok-Adresse." }); } + /* ---- DER KREISLAUF (Stufe 7, 18.09.2026) -------------------------- + + „Ein Video aus einem Wunsch zeigt den Namen." Wer sich etwas + gewuenscht hat, soll sehen, dass daraus etwas geworden ist -- + das ist der ganze Sinn eines Wunschbretts. Ohne diese Verbindung + ist die Galerie eine Sammlung, und der Wunsch von letzter Woche + bleibt unbeantwortet im Raum stehen. + + DIE QUELLE MUSS EIN COMMUNITY-BRETT SEIN. Eine Kette, die in die + Content-Planung eines Creators fuehrt, waere ein Leck: Dort steht + Arbeit, die die Community nichts angeht -- und der TITEL der + Quelle wird auf der Kachel angezeigt. Dieselbe Regel wie beim + „Daraus"-Knopf, aus demselben Grund. */ + const aus = Number(req.body?.aus || 0); + let ausQuelle = null; + if (req.body?.aus !== undefined && req.body?.aus !== null && req.body?.aus !== "") { + if (!Number.isInteger(aus) || aus <= 0) { + return res.status(400).json({ fehler: "Die Quelle ist keine Nummer." }); + } + if (!sichtbar(req.person)) return res.status(404).json({ fehler: "nicht_gefunden" }); + ausQuelle = db().prepare( + "SELECT id, bereich, titel FROM eintraege WHERE id = ?").get(aus); + /* Nicht da, oder kein Community-Brett: dieselbe Antwort. Sonst + liesse sich durch Ausprobieren herausfinden, welche Nummern es + gibt und in welchem Bereich sie liegen. */ + if (!ausQuelle || !TREFF_BRETTER.includes(ausQuelle.bereich)) { + return res.status(404).json({ fehler: "nicht_gefunden" }); + } + } + /* SCHON DA? Derselbe Link zweimal soll dasselbe Ergebnis geben, nicht zwei Eintraege. Wer den Knopf zweimal drueckt, meint nicht zwei Videos. */ @@ -177,6 +208,22 @@ videoRouter.post("/workspace/api/bereich/:bereich/video", async (req, res) => { "SELECT id FROM eintraege WHERE quelle_url = ? AND bereich = ?").get(url, bereich); if (schon) return res.status(200).json({ id: schon.id, schon: true }); + /* IST AUS DIESEM WUNSCH SCHON ETWAS GEWORDEN? + + Den Weg „Daraus ein Highlight machen" gibt es laenger als diesen + hier. Wer ihn gegangen ist und DANACH das Video einfuegt, bekaeme + sonst ein ZWEITES Highlight aus demselben Wunsch -- zwei Wege zum + selben Ziel, die nichts voneinander wissen. Das Video wird + deshalb an den vorhandenen Eintrag gehaengt. */ + let anhaengenAn = 0; + if (ausQuelle) { + const da = db().prepare( + "SELECT id, quelle_url FROM eintraege WHERE aus_eintrag_id = ? AND bereich = ?") + .get(aus, bereich); + if (da?.quelle_url) return res.status(200).json({ id: da.id, schon: true }); + if (da) anhaengenAn = da.id; + } + const auskunft = await videoAuskunft(url); if (auskunft.fehler) return res.status(502).json({ fehler: auskunft.fehler }); @@ -194,14 +241,27 @@ videoRouter.post("/workspace/api/bereich/:bereich/video", async (req, res) => { const jetzt = new Date().toISOString(); const arten = { highlight: "clip", anschlag: "hinweis", treff: "lob" }; - const { lastInsertRowid } = db().prepare(` - INSERT INTO eintraege - (bereich, art, titel, datum, status, erstellt, erstellt_von, - quelle_url, quelle_kanal, quelle_geholt_am) - VALUES (?,?,?,?, 'offen', ?,?,?,?,?)`) - .run(bereich, arten[bereich] || "clip", auskunft.titel, jetzt.slice(0, 10), - jetzt, req.person.id, url, auskunft.kanal, jetzt); - const eintragId = Number(lastInsertRowid); + let eintragId; + if (anhaengenAn) { + /* DER TITEL WIRD NICHT UEBERSCHRIEBEN. Er stammt vom Wunsch und + damit von einem Menschen; der von TikTok ist eine Bildunter- + schrift mit Schlagwoertern. Wer den ersetzte, naehme dem + Eintrag genau das, was ihn mit dem Wunsch verbindet. */ + db().prepare(`UPDATE eintraege SET quelle_url = ?, quelle_kanal = ?, + quelle_geholt_am = ?, geaendert = ? + WHERE id = ?`) + .run(url, auskunft.kanal, jetzt, jetzt, anhaengenAn); + eintragId = anhaengenAn; + } else { + const { lastInsertRowid } = db().prepare(` + INSERT INTO eintraege + (bereich, art, titel, datum, status, erstellt, erstellt_von, + quelle_url, quelle_kanal, quelle_geholt_am, aus_eintrag_id) + VALUES (?,?,?,?, 'offen', ?,?,?,?,?,?)`) + .run(bereich, arten[bereich] || "clip", auskunft.titel, jetzt.slice(0, 10), + jetzt, req.person.id, url, auskunft.kanal, jetzt, ausQuelle ? aus : null); + eintragId = Number(lastInsertRowid); + } if (abgelegt) { db().prepare(` @@ -215,11 +275,12 @@ videoRouter.post("/workspace/api/bereich/:bereich/video", async (req, res) => { protokolliere("video_eingelesen", { personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req), - detail: `${auskunft.kanal} ${url}`.slice(0, 120), + detail: `${auskunft.kanal} ${url}${ausQuelle ? ` (aus #${aus})` : ""}`.slice(0, 120), }); res.status(201).json({ id: eintragId, schon: false, kanal: auskunft.kanal, titel: auskunft.titel, cover: !!abgelegt || undefined, + ...(ausQuelle ? { aus: aus, angehaengt: !!anhaengenAn } : {}), }); } catch (fehler) { /* GING ETWAS SCHIEF, NACHDEM DIE DATEI SCHON LAG: wegraeumen. Eine diff --git a/workspace/app.webmanifest b/workspace/app.webmanifest index 35a1f0a8..272e8a3d 100644 --- a/workspace/app.webmanifest +++ b/workspace/app.webmanifest @@ -22,25 +22,25 @@ }, "icons": [ { - "src": "/assets/img/app-symbole/workspace-192.png?v=202609181607", + "src": "/assets/img/app-symbole/workspace-192.png?v=202609181700", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-512.png?v=202609181607", + "src": "/assets/img/app-symbole/workspace-512.png?v=202609181700", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609181607", + "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609181700", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609181607", + "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609181700", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/assets/css/bereich.css b/workspace/assets/css/bereich.css index 6443391a..ad2c5290 100644 --- a/workspace/assets/css/bereich.css +++ b/workspace/assets/css/bereich.css @@ -1396,3 +1396,40 @@ color: color-mix(in srgb, var(--akzent) 42%, #ffffff); } +/* ---- „Video dazu einfügen" an einer Wunschkarte (Stufe 7) ---------- + Eine Zeile, kein Kasten mit Rahmen: Es ist ein Zwischenschritt, der + gleich wieder verschwindet. Ein aufwendig gestaltetes Formular an + dieser Stelle sähe aus, als gehörte es dauerhaft zur Karte. */ +.videodazu { + display: flex; + flex-wrap: wrap; + gap: .5rem; + align-items: center; + margin-top: .7rem; + padding-top: .7rem; + border-top: 1px solid rgba(140, 160, 185, .18); +} +.videodazu__feld { + flex: 1 1 15rem; + min-width: 0; + padding: .45rem .6rem; + border: 1px solid rgba(140, 160, 185, .35); + border-radius: .55rem; + background: rgba(10, 18, 30, .55); + color: #e8f1fa; + font-size: .9rem; +} +.videodazu__feld:focus-visible { + outline: 2px solid #6ea8d8; + outline-offset: 1px; +} +.videodazu__senden { flex: 0 0 auto; } +/* Die Rückmeldung bekommt eine eigene Zeile, damit eine lange + Fehlermeldung das Feld nicht aus der Zeile drückt. */ +.videodazu__sagt { + flex: 1 0 100%; + margin: 0; + min-height: 1.1em; + color: #9fb3c9; + font-size: .84rem; +} diff --git a/workspace/assets/js/bereich.js b/workspace/assets/js/bereich.js index 310157a5..37acf590 100644 --- a/workspace/assets/js/bereich.js +++ b/workspace/assets/js/bereich.js @@ -869,6 +869,36 @@ })); } + /* ---- VIDEO DAZU (Stufe 7, 18.09.2026) -------------------------- + + „Ein Video aus einem Wunsch zeigt den Namen." Der Knopf steht + AN DEM WUNSCH -- aus demselben Grund wie die Kettenknoepfe + darueber: Wer oben im Formular einen Link einfuegt, denkt nicht + an den Wunsch von letzter Woche. Hier denkt er an nichts + anderes. + + Er verschwindet, sobald aus dem Wunsch ein Highlight MIT Video + geworden ist. Steht dort schon ein Highlight ohne Video, bleibt + er -- der Server haengt das Video dann an den vorhandenen + Eintrag, statt ein zweites anzulegen. */ + if (VIDEO_AUS.includes(bereich) + && !(e.wurde || []).some((w) => w.bereich === 'highlight' && w.hat_video)) { + knoepfe.append(tuKnopf('Video dazu einfügen', () => { + /* Schon offen? Dann wieder zu. Ein Knopf, der beim zweiten + Druecken ein zweites Feld baut, ist ein Fehler, den man + erst sieht, wenn drei davon dastehen. */ + const alt = k.querySelector('.videodazu'); + if (alt) { alt.remove(); return; } + /* DIREKT UNTER DIE KNOPFREIHE, nicht ans Ende der Karte. + Zuerst stand hier `k.append` -- und das Bildschirmfoto zeigte + das Feld UNTER der Fusszeile („18.09.2026 - von Lena"), + getrennt von dem Knopf, den man gerade gedrueckt hat. Eine + Zahl haette das nie gemeldet: Das Feld war da, es reagierte, + alle Pruefungen gruen. */ + knoepfe.insertAdjacentElement('afterend', videoDazuFeld(e)); + })); + } + /* VIER ZUSTAENDE STATT EINES UMSCHALTERS (17.09.2026). "erledigt / wieder offen" reicht fuer einen Vorfall. Ein Wunsch @@ -1095,6 +1125,56 @@ Link einfuegen, den Rest macht der Server. Nur bei den Highlights: Dort gehoeren Clips hin, und ein Feld an jedem anderen Brett waere eine Einladung, sie ueberall hinzuschreiben. */ + /* Aus welchen Brettern heraus ein Video eingefuegt werden kann. + „mitmachen" und „regeln" stehen bewusst nicht dabei: Dort geht es + nicht um Inhalte, aus denen ein Clip wird. */ + const VIDEO_AUS = ['wunsch', 'ansteht']; + + /** Das Eingabefeld, das direkt an der Karte aufgeht. */ + function videoDazuFeld(e) { + const kasten = el('div', 'videodazu'); + const feld = el('input', 'videodazu__feld'); + feld.type = 'url'; + feld.placeholder = 'TikTok-Link einfügen'; + /* Ohne Beschriftung waere das Feld fuer eine Vorleseansage ein + Kaestchen ohne Bedeutung. Sichtbar ist der Knopftext daneben. */ + feld.setAttribute('aria-label', 'TikTok-Link zu diesem Eintrag'); + const senden = el('button', 'schritt videodazu__senden', 'Übernehmen'); + senden.type = 'button'; + const sagt = el('p', 'videodazu__sagt'); + + senden.addEventListener('click', async () => { + const url = feld.value.trim(); + if (!url) { feld.focus(); return; } + senden.disabled = true; + sagt.textContent = 'Wird geholt …'; + try { + const a = await hole('/workspace/api/bereich/highlight/video', { + method: 'POST', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ url, aus: e.id }), + }); + const r = await a.json().catch(() => ({})); + if (!a.ok) { + /* Der Grund des Servers, nicht „Ging nicht" -- er weiss, ob + der Account fremd war oder die Adresse keine ist. */ + sagt.textContent = r.fehler || 'Ging nicht.'; + senden.disabled = false; + return; + } + await laden(); + } finally { senden.disabled = false; } + }); + feld.addEventListener('keydown', (ev) => { + if (ev.key === 'Enter') { ev.preventDefault(); senden.click(); } + }); + + kasten.append(feld, senden, sagt); + /* Der Fokus wandert ins Feld: Wer den Knopf gedrueckt hat, will + tippen, nicht erst noch zielen. */ + setTimeout(() => feld.focus(), 0); + return kasten; + } + function videoFeldZeigen() { const ziel = $('videoholen'); if (!ziel) return; diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index a3d860e5..5ad1139b 100644 --- a/workspace/aufgaben.html +++ b/workspace/aufgaben.html @@ -12,19 +12,19 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - + + + + + + - + @@ -291,10 +291,10 @@ - - - - - + + + + + diff --git a/workspace/automation.html b/workspace/automation.html index 1de1580b..ee5e7a8c 100644 --- a/workspace/automation.html +++ b/workspace/automation.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -168,10 +168,10 @@ - - - - - + + + + + diff --git a/workspace/befinden.html b/workspace/befinden.html index 4f043935..bc6144e6 100644 --- a/workspace/befinden.html +++ b/workspace/befinden.html @@ -7,14 +7,14 @@ - - - - - - - - + + + + + + + + @@ -105,9 +105,9 @@ - - - - + + + + diff --git a/workspace/bereich.html b/workspace/bereich.html index d0e4cd83..8bc22168 100644 --- a/workspace/bereich.html +++ b/workspace/bereich.html @@ -12,24 +12,24 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - + + + + + + - - + + - + @@ -243,14 +243,14 @@ - - - - - - - - - + + + + + + + + + diff --git a/workspace/bewerben.html b/workspace/bewerben.html index 85ef4d21..a3e8f911 100644 --- a/workspace/bewerben.html +++ b/workspace/bewerben.html @@ -7,17 +7,17 @@ - - - - - - - - - + + + + + + + + + - + @@ -107,10 +107,10 @@ - - - - - + + + + + diff --git a/workspace/bewerbungen.html b/workspace/bewerbungen.html index 77e48757..3a8aeb7d 100644 --- a/workspace/bewerbungen.html +++ b/workspace/bewerbungen.html @@ -7,16 +7,16 @@ - - - - - - - - + + + + + + + + - + @@ -80,10 +80,10 @@ - - - - - + + + + + diff --git a/workspace/calls.html b/workspace/calls.html index a4660570..6d972c54 100644 --- a/workspace/calls.html +++ b/workspace/calls.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -89,11 +89,11 @@ - - - - - - + + + + + + diff --git a/workspace/chat.html b/workspace/chat.html index 4db3b2c0..21ed90f5 100644 --- a/workspace/chat.html +++ b/workspace/chat.html @@ -12,26 +12,26 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - + + + + - - - + + + - + @@ -341,15 +341,15 @@ - - - - + + + + - - + + diff --git a/workspace/content.html b/workspace/content.html index 47a0c7c6..ae843fa2 100644 --- a/workspace/content.html +++ b/workspace/content.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -179,12 +179,12 @@ - - - - - - - + + + + + + + diff --git a/workspace/crew-index.html b/workspace/crew-index.html index 232763cf..4c62a9ab 100644 --- a/workspace/crew-index.html +++ b/workspace/crew-index.html @@ -36,15 +36,15 @@ - - - + + + - + @@ -339,6 +339,6 @@ - + diff --git a/workspace/crew.webmanifest b/workspace/crew.webmanifest index 3273f7fd..ab3eaf68 100644 --- a/workspace/crew.webmanifest +++ b/workspace/crew.webmanifest @@ -22,25 +22,25 @@ }, "icons": [ { - "src": "/assets/img/app-symbole/crew-192.png?v=202609181607", + "src": "/assets/img/app-symbole/crew-192.png?v=202609181700", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/crew-512.png?v=202609181607", + "src": "/assets/img/app-symbole/crew-512.png?v=202609181700", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/crew-192-maskable.png?v=202609181607", + "src": "/assets/img/app-symbole/crew-192-maskable.png?v=202609181700", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/crew-512-maskable.png?v=202609181607", + "src": "/assets/img/app-symbole/crew-512-maskable.png?v=202609181700", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/dateien.html b/workspace/dateien.html index 77ff6476..008076e8 100644 --- a/workspace/dateien.html +++ b/workspace/dateien.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -108,10 +108,10 @@ - - - - - + + + + + diff --git a/workspace/entwicklung.html b/workspace/entwicklung.html index 18acd7ef..8d1b1fc2 100644 --- a/workspace/entwicklung.html +++ b/workspace/entwicklung.html @@ -7,14 +7,14 @@ - - - - - - - - + + + + + + + + @@ -102,9 +102,9 @@ - - - - + + + + diff --git a/workspace/index.html b/workspace/index.html index 7490d393..af44fc77 100644 --- a/workspace/index.html +++ b/workspace/index.html @@ -30,9 +30,9 @@ - - - + + + @@ -401,6 +401,6 @@ - + diff --git a/workspace/kalender.html b/workspace/kalender.html index 9062e557..1dc41261 100644 --- a/workspace/kalender.html +++ b/workspace/kalender.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -377,11 +377,11 @@ - - - - - - + + + + + + diff --git a/workspace/leistung.html b/workspace/leistung.html index 4011e04f..e708dabe 100644 --- a/workspace/leistung.html +++ b/workspace/leistung.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -444,10 +444,10 @@ - - - - - + + + + + diff --git a/workspace/personen.html b/workspace/personen.html index d76a275d..a0ebf863 100644 --- a/workspace/personen.html +++ b/workspace/personen.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -423,10 +423,10 @@ - - - - - + + + + + diff --git a/workspace/profil.html b/workspace/profil.html index 8d82b928..683060aa 100644 --- a/workspace/profil.html +++ b/workspace/profil.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -214,11 +214,11 @@ - - - - - - + + + + + + diff --git a/workspace/rechte.html b/workspace/rechte.html index 3a5eec43..43ba6dc0 100644 --- a/workspace/rechte.html +++ b/workspace/rechte.html @@ -7,14 +7,14 @@ - - - - - - - - + + + + + + + + @@ -134,9 +134,9 @@ Beim ersten Anlauf fehlte die Zeile auf genau diesen beiden neuen Seiten. Gefunden hat das nicht das Auge, sondern pruef-css-klassen, die seit dem 07.09. jede Seite danach absucht. --> - - - - + + + + diff --git a/workspace/report.html b/workspace/report.html index b9fcd167..85062b85 100644 --- a/workspace/report.html +++ b/workspace/report.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -106,11 +106,11 @@ - - - - - - + + + + + + diff --git a/workspace/scouting.html b/workspace/scouting.html index e54968a9..e4ab2ca1 100644 --- a/workspace/scouting.html +++ b/workspace/scouting.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -173,10 +173,10 @@ - - - - - + + + + + diff --git a/workspace/start.html b/workspace/start.html index b00f9d44..25f7fac7 100644 --- a/workspace/start.html +++ b/workspace/start.html @@ -12,21 +12,21 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - + + + + - - + + - + @@ -674,10 +674,10 @@

- - - - - + + + + + diff --git a/workspace/startcheck.html b/workspace/startcheck.html index fc8be5bb..81c946cc 100644 --- a/workspace/startcheck.html +++ b/workspace/startcheck.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -85,11 +85,11 @@ - - - - - - + + + + + + diff --git a/workspace/steckbrief.html b/workspace/steckbrief.html index 6e0e777b..ba7d7e3b 100644 --- a/workspace/steckbrief.html +++ b/workspace/steckbrief.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -146,11 +146,11 @@ - - - - - - + + + + + + diff --git a/workspace/talente.html b/workspace/talente.html index f365f790..30102ac1 100644 --- a/workspace/talente.html +++ b/workspace/talente.html @@ -7,18 +7,18 @@ - - - - - - + + + + + + - - - + + + @@ -111,9 +111,9 @@ - - - - + + + + diff --git a/workspace/team.html b/workspace/team.html index b29657d7..89fc7485 100644 --- a/workspace/team.html +++ b/workspace/team.html @@ -7,20 +7,20 @@ - - - - - - - + + + + + + + - + @@ -110,10 +110,10 @@ - - - - - + + + + + diff --git a/workspace/teamlage.html b/workspace/teamlage.html index bd5d22d5..79cb6882 100644 --- a/workspace/teamlage.html +++ b/workspace/teamlage.html @@ -7,20 +7,20 @@ - - - - - - - + + + + + + + - + - - - - + + + + diff --git a/workspace/teilen.html b/workspace/teilen.html index 59c860b2..bea02fb2 100644 --- a/workspace/teilen.html +++ b/workspace/teilen.html @@ -7,13 +7,13 @@ - - - - - - - + + + + + + + @@ -112,9 +112,9 @@ - - - - + + + + diff --git a/workspace/treff-moderation.html b/workspace/treff-moderation.html index 10dd3a9f..85b8a5ea 100644 --- a/workspace/treff-moderation.html +++ b/workspace/treff-moderation.html @@ -7,14 +7,14 @@ - - - - - - - - + + + + + + + + @@ -126,9 +126,9 @@ - - - - + + + + diff --git a/workspace/treff-regeln.html b/workspace/treff-regeln.html index d56f5362..05d61055 100644 --- a/workspace/treff-regeln.html +++ b/workspace/treff-regeln.html @@ -7,18 +7,18 @@ - - - - - - - + + + + + + + - + @@ -211,10 +211,10 @@ Beim ersten Anlauf fehlte die Zeile auf genau diesen beiden neuen Seiten. Gefunden hat das nicht das Auge, sondern pruef-css-klassen, die seit dem 07.09. jede Seite danach absucht. --> - - - - - + + + + + diff --git a/workspace/uebersicht.html b/workspace/uebersicht.html index 0dea434b..a9016832 100644 --- a/workspace/uebersicht.html +++ b/workspace/uebersicht.html @@ -12,22 +12,22 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - + + + + - - - + + + - + @@ -149,11 +149,11 @@ - + - - - - + + + + diff --git a/workspace/wissen.html b/workspace/wissen.html index c71aec0c..b51cd7c9 100644 --- a/workspace/wissen.html +++ b/workspace/wissen.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -190,10 +190,10 @@ - - - - - + + + + +