110 lines
4.1 KiB
JavaScript
110 lines
4.1 KiB
JavaScript
// Shared helpers used by app.js, explore.js, and share.js.
|
|
|
|
window.$ = (sel, root = document) => root.querySelector(sel);
|
|
|
|
window.api = {
|
|
async req(method, path, body) {
|
|
const res = await fetch(path, {
|
|
method,
|
|
headers: body ? { "Content-Type": "application/json" } : undefined,
|
|
body: body ? JSON.stringify(body) : undefined,
|
|
});
|
|
const data = res.status === 204 ? null : await res.json().catch(() => null);
|
|
if (!res.ok) {
|
|
const e = new Error((data && data.error) || "Request failed");
|
|
e.status = res.status;
|
|
throw e;
|
|
}
|
|
return data;
|
|
},
|
|
};
|
|
|
|
window.escapeHtml = (s) =>
|
|
String(s)
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """);
|
|
|
|
// SQLite stores UTC datetimes as "YYYY-MM-DD HH:MM:SS"; parse as UTC.
|
|
window.parseUTC = (s) => new Date(s.replace(" ", "T") + "Z");
|
|
|
|
window.relativeTime = (s) => {
|
|
const then = parseUTC(s);
|
|
const diff = (Date.now() - then.getTime()) / 1000;
|
|
if (diff < 45) return "just now";
|
|
if (diff < 90) return "a minute ago";
|
|
if (diff < 3600) return `${Math.round(diff / 60)} min ago`;
|
|
if (diff < 7200) return "an hour ago";
|
|
if (diff < 86400) return `${Math.round(diff / 3600)} hours ago`;
|
|
if (diff < 172800) return "yesterday";
|
|
if (diff < 604800) return `${Math.round(diff / 86400)} days ago`;
|
|
return then.toLocaleDateString(undefined, {
|
|
month: "short",
|
|
day: "numeric",
|
|
year: "numeric",
|
|
});
|
|
};
|
|
|
|
window.fullTime = (s) =>
|
|
parseUTC(s).toLocaleString(undefined, {
|
|
dateStyle: "medium",
|
|
timeStyle: "short",
|
|
});
|
|
|
|
let toastTimer;
|
|
window.toast = (msg, isError = false) => {
|
|
const el = $("#toast");
|
|
if (!el) return;
|
|
el.textContent = msg;
|
|
el.classList.toggle("error", isError);
|
|
el.classList.add("show");
|
|
clearTimeout(toastTimer);
|
|
toastTimer = setTimeout(() => el.classList.remove("show"), 2600);
|
|
};
|
|
|
|
// Render a list of notes into a container element (read-only unless onDelete given).
|
|
window.renderNotesInto = (container, notes, onDelete) => {
|
|
if (!container) return;
|
|
if (!notes.length) {
|
|
container.innerHTML =
|
|
'<div class="empty" style="padding:24px">No notes yet.</div>';
|
|
return;
|
|
}
|
|
container.innerHTML = notes
|
|
.map(
|
|
(n) => `
|
|
<div class="note">
|
|
<div class="avatar">📝</div>
|
|
<div class="note-body">
|
|
<div class="text">${escapeHtml(n.body)}</div>
|
|
<div class="note-time">
|
|
<span title="${escapeHtml(fullTime(n.created_at))}">${relativeTime(
|
|
n.created_at
|
|
)}</span>
|
|
${onDelete ? `<span class="del" data-note="${n.id}">delete</span>` : ""}
|
|
</div>
|
|
</div>
|
|
</div>`
|
|
)
|
|
.join("");
|
|
if (onDelete) {
|
|
container.querySelectorAll(".del").forEach((el) =>
|
|
el.addEventListener("click", () => onDelete(Number(el.dataset.note)))
|
|
);
|
|
}
|
|
};
|
|
|
|
// Provider sign-in buttons (used by login gate).
|
|
window.providerButton = (p) => {
|
|
const icons = {
|
|
github:
|
|
'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 .5C5.7.5.5 5.7.5 12c0 5.1 3.3 9.4 7.9 10.9.6.1.8-.3.8-.6v-2c-3.2.7-3.9-1.4-3.9-1.4-.5-1.3-1.3-1.7-1.3-1.7-1.1-.7.1-.7.1-.7 1.2.1 1.8 1.2 1.8 1.2 1 1.8 2.7 1.3 3.4 1 .1-.8.4-1.3.7-1.6-2.6-.3-5.3-1.3-5.3-5.7 0-1.3.5-2.3 1.2-3.1-.1-.3-.5-1.5.1-3.1 0 0 1-.3 3.3 1.2a11.5 11.5 0 0 1 6 0C17.3 4.7 18.3 5 18.3 5c.6 1.6.2 2.8.1 3.1.8.8 1.2 1.8 1.2 3.1 0 4.4-2.7 5.4-5.3 5.7.4.4.8 1.1.8 2.2v3.3c0 .3.2.7.8.6 4.6-1.5 7.9-5.8 7.9-10.9C23.5 5.7 18.3.5 12 .5z"/></svg>',
|
|
google:
|
|
'<svg viewBox="0 0 24 24"><path fill="#4285F4" d="M22.5 12.2c0-.8-.1-1.4-.2-2.1H12v3.9h6c-.1 1-.8 2.5-2.2 3.5v2.9h3.5c2.1-1.9 3.2-4.7 3.2-8.1z"/><path fill="#34A853" d="M12 23c2.9 0 5.4-1 7.2-2.6l-3.5-2.9c-1 .7-2.2 1.1-3.7 1.1-2.8 0-5.2-1.9-6.1-4.5H2.3v2.9C4.1 20.6 7.8 23 12 23z"/><path fill="#FBBC05" d="M5.9 14.1c-.2-.7-.4-1.4-.4-2.1s.1-1.4.4-2.1V7H2.3C1.5 8.5 1 10.2 1 12s.5 3.5 1.3 5l3.6-2.9z"/><path fill="#EA4335" d="M12 5.4c1.6 0 2.9.5 4 1.6l3-3C17.4 2.1 14.9 1 12 1 7.8 1 4.1 3.4 2.3 7l3.6 2.9C6.8 7.3 9.2 5.4 12 5.4z"/></svg>',
|
|
};
|
|
return `<button class="provider-btn" data-provider="${p.id}">${
|
|
icons[p.id] || ""
|
|
}<span>Continue with ${escapeHtml(p.label)}</span></button>`;
|
|
};
|