Files
anonymous_github/public/script/main.js
T
tdurieuxandClaude Opus 5.5 f8b2ea38a1 fix: polish explorer, error states and page alignment
- Explorer: 13px code, page-coloured dark editor, padded Markdown with a
  readable width, full repository ID on hover, sidebar footer kept clear
  of the floating Ko-fi button.
- Explorer errors render as a card with links home and to the FAQ; the
  empty action bar and Raw/Download buttons are hidden.
- Unknown error codes show the generic message instead of the raw
  ERRORS.<code> key.
- All app pages share one content column; New anonymization starts at
  the top instead of floating mid-screen.
- Status page: spaced action buttons, aligned status pill, no second
  Ko-fi embed under the support cards.
- Claim placeholders; consistent notebook option on new conferences.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 12:36:15 +02:00

149 lines
7.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { loadLibrary } from "./lazy-assets.js";
import { createApp, h, watch, provide, inject, onBeforeUnmount } from "vue";
import { createRouter, createWebHistory, RouterView } from "vue-router";
import { pageRoutes } from "./routes.js";
import { templates } from "./templates.js";
import { render as iconsTemplate } from "../partials/icons.htm";
import { initializeTemplate } from "./template-state.js";
import { createPageState, createTimers } from "./state.js";
import { createHttp, promises } from "./http.js";
import { mainController } from "./app.js";
import { createQuotaService } from "./quota.js";
import * as formatters from "./formatters.js";
import translations from "../i18n/locale-en.json";
import { components, codeEditor, paperScrollspy } from "./components.js";
import Tree from "./tree.js";
import { fieldDirective, formDirective, submitForm } from "./forms.js";
const sessionKey = Symbol("session");
export const serverPaths = /^\/(w|api|github)(\/|$)/;
function translate(key, params = {}) {
let message = key?.split(".").reduce((value, part) => value?.[part], translations);
// Unknown error codes would otherwise surface as the raw "ERRORS.code" key.
if (message == null && key?.startsWith("ERRORS.")) message = translations.ERRORS.unknown_error;
return String(message ?? key ?? "").replace(/{{\s*([^}]+?)\s*}}/g, (_, name) => params[name] ?? "");
}
const fmt = { ...formatters, translate };
export function safeUrl(value) {
if (value == null || value === "") return value;
try {
const url = new URL(value, window.location.href);
return ["http:", "https:", "mailto:", "tel:", "blob:"].includes(url.protocol) ? value : undefined;
} catch { return undefined; }
}
export function mountApplication(target = "#app", options = {}) {
// AngularJS links stored the repository route in the fragment. Normalize it
// before web history reads the URL, replacing the entry so Back still works.
if (!options.history && window.location.pathname === "/" && /^#!\/r\/[^/?#]+/.test(window.location.hash)) {
window.history.replaceState(window.history.state, "", window.location.hash.slice(2));
}
const events = new Map();
const http = createHttp(options.fetch);
let root;
let router;
function services() {
const timers = createTimers();
const location = {
url(value) { if (value === undefined) return router.currentRoute.value.fullPath; router.push(value); return location; },
path() { return router.currentRoute.value.path; },
search() { return router.currentRoute.value.query; },
};
const params = new Proxy({}, { get(_, key) {
const value = router.currentRoute.value.params[key];
return Array.isArray(value) ? value.join("/") : value;
} });
return { http, ...timers, location, params, window, promises, html: { trustAsHtml: value => value }, translate: (key, params) => Promise.resolve(translate(key, params)), quotaService: createQuotaService(http) };
}
function pageComponent(definition) {
return {
name: definition.template,
setup() {
const state = createPageState(inject(sessionKey), events);
definition.setup(state, services());
initializeTemplate(definition.template, state);
const cache = [];
return () => templates[definition.template](state, cache);
},
};
}
router = createRouter({
history: options.history || createWebHistory(),
routes: pageRoutes.map(definition => definition.redirect ? definition : ({
path: definition.path,
component: pageComponent(definition),
meta: { title: definition.title, preserveExplorer: definition.preserveExplorer },
})),
});
const app = createApp({
setup() {
root = createPageState(null, events);
root.fmt = fmt;
root.window = window;
root.Math = Math;
root.sanitize = value => DOMPurify.sanitize(value ?? "");
root.submitForm = submitForm;
root.safeUrl = safeUrl;
const context = services();
mainController(root, context.http, context.location, context.timeout);
provide(sessionKey, root);
watch(() => root.title, title => { document.title = title || "Anonymous GitHub"; });
const headerCache = [];
const navigate = event => {
const anchor = event.target.closest?.("a[href]");
if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || !anchor || anchor.hasAttribute("download") || (anchor.target && anchor.target !== "_self") || anchor.dataset.toggle) return;
const href = anchor.getAttribute("href");
if (href.startsWith("#")) return;
const url = new URL(anchor.href, window.location.href);
if (url.origin !== window.location.origin || serverPaths.test(url.pathname)) return;
event.preventDefault();
router.push(url.pathname + url.search + url.hash);
$("#navbarSupportedContent.show").collapse("hide");
};
document.addEventListener("click", navigate);
onBeforeUnmount(() => document.removeEventListener("click", navigate));
return () => [
iconsTemplate(root, []),
h("header", { class: "app-header" }, templates["partials/header.htm"](root, headerCache)),
h("main", { class: "app-view align-items-stretch w-100" }, h(RouterView, null, {
default: ({ Component, route }) => Component ? h(Component, { key: route.meta.preserveExplorer ? route.matched[0]?.path : route.path }) : null,
})),
h("div", { class: "position-fixed p-3", style: { zIndex: 999999999, right: 0, bottom: 0 } }, root.toasts.map((toast, i) => h("div", { class: "toast show", role: "alert", "aria-live": "assertive", "aria-atomic": "true", key: i }, [
h("div", { class: "toast-header" }, [h("strong", { class: "mr-auto" }, toast.title), h("button", { type: "button", class: "ml-2 mb-1 close", "aria-label": "Close", onClick: () => root.removeToast(toast) }, "×")]),
h("div", { class: "toast-body" }, toast.body),
]))),
];
},
});
for (const [name, component] of Object.entries(components)) app.component(name, component);
app.component("Tree", Tree);
app.component("PartialView", {
props: ["name", "state"],
setup(props) { const cache = []; return () => templates[props.name](props.state, cache); },
});
app.directive("field", fieldDirective);
app.directive("form", formDirective);
app.directive("code-editor", codeEditor);
app.directive("paper-scrollspy", paperScrollspy);
app.use(router);
router.beforeEach(async to => {
root?.emit("routeLeave");
if (/^\/(r|repository|anonymize|pull-request-anonymize|gist-anonymize|pr|gist)(\/|$)/.test(to.path)) {
await loadLibrary("markdown");
}
if (/^\/(r|repository)\//.test(to.path)) await loadLibrary("org");
});
router.afterEach(to => {
if (!root) return;
root.title = to.meta.title;
root.emit("routeChange", { title: to.meta.title });
root.emit("routeUpdate", { title: to.meta.title });
const view = document.querySelector(".app-view");
if (!to.meta.preserveExplorer && view) view.scrollTop = 0;
});
app.mount(target);
return { app, router, state: root };
}
if (document.querySelector("#app")) window.anonymousApp = mountApplication();