favoritter/web/static/js/app.js

71 lines
2.6 KiB
JavaScript
Raw Normal View History

feat: implement Phase 1 (auth) and Phase 2 (faves CRUD) foundation Go backend with server-rendered HTML/HTMX frontend, SQLite database, and filesystem image storage. Self-hostable single-binary architecture. Phase 1 — Authentication & project foundation: - Argon2id password hashing with timing-attack prevention - Session management with cookie-based auth and periodic cleanup - Login, signup (open/requests/closed modes), logout, forced password reset - CSRF double-submit cookie pattern with HTMX auto-inclusion - Proxy-aware real IP extraction (WireGuard/Tailscale support) - Configurable base path for subdomain and subpath deployment - Rate limiting on auth endpoints with background cleanup - Security headers (CSP, X-Frame-Options, Referrer-Policy) - Structured logging with slog, graceful shutdown - Pico CSS + HTMX vendored and embedded via go:embed Phase 2 — Faves CRUD with tags and images: - Full CRUD for favorites with ownership checks - Image upload with EXIF stripping, resize to 1920px, UUID filenames - Tag system with HTMX autocomplete (prefix search, popularity-sorted) - Privacy controls (public/private per fave, user-configurable default) - Tag browsing, pagination, batch tag loading (avoids N+1) - OpenGraph meta tags on public fave detail pages Includes code quality pass: extracted shared helpers, fixed signup request persistence bug, plugged rate limiter memory leak, removed dead code, and logged previously-swallowed errors. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 15:55:22 +02:00
// Favoritter — minimal JavaScript for HTMX configuration and form helpers.
// SPDX-License-Identifier: AGPL-3.0-or-later
(function () {
"use strict";
// Auto-include the CSRF token in all HTMX requests.
document.body.addEventListener("htmx:configRequest", function (event) {
var csrfCookie = getCookie("csrf_token");
if (csrfCookie) {
event.detail.headers["X-CSRF-Token"] = csrfCookie;
}
// For the tag search input, send the current value of the last
// comma-separated segment as the 'q' parameter.
var elt = event.detail.elt;
if (elt && elt.id === "tags") {
var val = elt.value;
var parts = val.split(",");
var lastPart = parts[parts.length - 1].trim();
event.detail.parameters["q"] = lastPart;
}
});
// Focus management after HTMX content swaps for accessibility.
document.body.addEventListener("htmx:afterSwap", function (event) {
var target = event.detail.target;
if (target) {
var autoFocus = target.querySelector("[autofocus]");
if (autoFocus) {
autoFocus.focus();
}
}
});
// After a successful HTMX DELETE, redirect if the element has a data-redirect attribute.
document.body.addEventListener("htmx:afterRequest", function (event) {
if (event.detail.successful && event.detail.verb === "delete") {
var redirect = event.detail.elt.getAttribute("data-redirect");
if (redirect) {
window.location.href = redirect;
}
}
});
// Tag autocomplete: add a selected tag to the tag input.
window.addTag = function (element, tagName) {
var input = document.getElementById("tags");
if (!input) return;
var parts = input.value.split(",").map(function (s) { return s.trim(); });
// Replace the last (incomplete) segment with the selected tag.
parts[parts.length - 1] = tagName;
// Add a trailing separator so the user can keep typing.
input.value = parts.join(", ") + ", ";
input.focus();
// Clear suggestions by removing all child elements.
var suggestions = document.getElementById("tag-suggestions");
if (suggestions) {
while (suggestions.firstChild) {
suggestions.removeChild(suggestions.firstChild);
}
}
};
function getCookie(name) {
var match = document.cookie.match(new RegExp("(^| )" + name + "=([^;]+)"));
return match ? match[2] : null;
}
})();