Files
ytrecap/public/app.js
T
Frank Schwenk 5b8042b173 feat: hide recap creation behind five goldfish logo taps
Replace the visible create button with a secret tap gesture that still
opens the PIN-protected recap flow.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-16 16:05:13 +02:00

194 lines
5.2 KiB
JavaScript

import {
api,
VERDICT_LABELS,
formatTimestamp,
getToken,
setToken,
isLoggedIn,
} from "./api.js";
const createModal = document.getElementById("createModal");
const pinModal = document.getElementById("pinModal");
const recapList = document.getElementById("recapList");
const emptyState = document.getElementById("emptyState");
const listError = document.getElementById("listError");
function escapeHtml(text) {
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}
function renderRecapCard(item) {
const verdict = VERDICT_LABELS[item.watch_verdict] || VERDICT_LABELS.skim;
const href = `/r/${encodeURIComponent(item.video_id)}`;
return `
<a class="recapCard" href="${href}">
<img class="recapThumb" src="${escapeHtml(item.thumbnail_url || "")}" alt="" loading="lazy">
<div class="recapCardBody">
<span class="verdictBadge ${verdict.className}">${verdict.emoji} ${verdict.text}</span>
<h3>${escapeHtml(item.title)}</h3>
<p class="recapTldr">${escapeHtml(item.tldr)}</p>
<span class="recapMeta">${escapeHtml(item.channel || "")}</span>
</div>
</a>
`;
}
async function loadRecaps() {
listError.hidden = true;
try {
const data = await api.listRecaps(30);
const items = data.items || [];
if (!items.length) {
recapList.innerHTML = "";
emptyState.hidden = false;
return;
}
emptyState.hidden = true;
recapList.innerHTML = items.map(renderRecapCard).join("");
} catch (err) {
listError.textContent = err.message || "Liste konnte nicht geladen werden.";
listError.hidden = false;
}
}
function openPinModal() {
document.getElementById("pinError").hidden = true;
document.getElementById("pinInput").value = "";
pinModal.showModal();
document.getElementById("pinInput").focus();
}
function waitForPinLogin() {
return new Promise((resolve, reject) => {
if (isLoggedIn()) {
resolve();
return;
}
openPinModal();
pinModal.addEventListener(
"close",
() => {
if (isLoggedIn()) resolve();
else reject(new Error("Abgebrochen"));
},
{ once: true }
);
});
}
function openCreateModal() {
document.getElementById("createError").hidden = true;
document.getElementById("createProgress").hidden = true;
document.getElementById("urlInput").value = "";
document.getElementById("forceCheckbox").checked = false;
document.getElementById("createSubmit").disabled = false;
createModal.showModal();
document.getElementById("urlInput").focus();
}
async function handleCreate(e) {
e.preventDefault();
const url = document.getElementById("urlInput").value.trim();
const force = document.getElementById("forceCheckbox").checked;
const errEl = document.getElementById("createError");
const progress = document.getElementById("createProgress");
const submit = document.getElementById("createSubmit");
errEl.hidden = true;
progress.hidden = false;
submit.disabled = true;
try {
const result = await api.createRecap(url, force);
createModal.close();
window.location.href = `/r/${encodeURIComponent(result.recap.video_id)}`;
} catch (err) {
errEl.textContent = err.message || "Recap fehlgeschlagen.";
errEl.hidden = false;
progress.hidden = true;
submit.disabled = false;
}
}
function wirePinForm() {
document.getElementById("pinCancel").addEventListener("click", () => pinModal.close());
document.getElementById("pinForm").addEventListener("submit", async (e) => {
e.preventDefault();
const pin = document.getElementById("pinInput").value.trim();
const errEl = document.getElementById("pinError");
errEl.hidden = true;
try {
const { token } = await api.login(pin);
setToken(token);
pinModal.close();
} catch {
errEl.textContent = "Falscher PIN. Dein Goldfisch auch.";
errEl.hidden = false;
}
});
}
const GOLDFISH_TAPS = 5;
const GOLDFISH_RESET_MS = 2000;
async function openCreateFlow() {
try {
await waitForPinLogin();
openCreateModal();
} catch {
/* cancelled */
}
}
function wireGoldfishSecret() {
const logo = document.getElementById("goldfishLogo");
let taps = 0;
let resetTimer = null;
logo.addEventListener("click", () => {
taps += 1;
logo.classList.add("logoWiggle");
window.setTimeout(() => logo.classList.remove("logoWiggle"), 300);
if (resetTimer) window.clearTimeout(resetTimer);
resetTimer = window.setTimeout(() => {
taps = 0;
}, GOLDFISH_RESET_MS);
if (taps >= GOLDFISH_TAPS) {
taps = 0;
if (resetTimer) window.clearTimeout(resetTimer);
openCreateFlow();
}
});
}
function wireCreate() {
wireGoldfishSecret();
document.getElementById("createCancel").addEventListener("click", () => createModal.close());
document.getElementById("createForm").addEventListener("submit", async (e) => {
e.preventDefault();
if (!isLoggedIn()) {
try {
await waitForPinLogin();
} catch {
return;
}
}
await handleCreate(e);
});
}
function registerSw() {
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("/sw.js").catch(() => {});
}
}
wirePinForm();
wireCreate();
loadRecaps();
registerSw();