add: postgres support

chore: restructure
This commit is contained in:
trisua 2025-03-22 22:17:47 -04:00
parent cda879f6df
commit b6fe2fba37
58 changed files with 3403 additions and 603 deletions

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,9 @@
{% extends "root.html" %} {% block body %}
<main class="flex flex-col gap-2" style="max-width: 25rem">
<h2 class="w-full text-center">{% block title %}{% endblock %}</h2>
<div class="card w-full flex flex-col gap-4 justify-center align-center">
{% block content %}{% endblock %}
</div>
{% block footer %}{% endblock %}
</main>
{% endblock %}

View file

@ -0,0 +1,33 @@
{% extends "auth/base.html" %} {% block head %}
<title>🐐 Login</title>
{% endblock %} {% block title %}Login{% endblock %} {% block content %}
<form class="w-full flex flex-col gap-4">
<div class="flex flex-col gap-1">
<label for="username"><b>Username</b></label>
<input
type="text"
placeholder="username"
required
name="username"
id="username"
/>
</div>
<div class="flex flex-col gap-1">
<label for="username"><b>Password</b></label>
<input
type="password"
placeholder="password"
required
name="password"
id="password"
/>
</div>
<button>Submit</button>
</form>
{% endblock %} {% block footer %}
<span class="small w-full text-center"
>Or, <a href="/auth/register">register</a></span
>
{% endblock %}

View file

@ -0,0 +1,62 @@
{% extends "auth/base.html" %} {% block head %}
<title>🐐 Register</title>
{% endblock %} {% block title %}Register{% endblock %} {% block content %}
<form class="w-full flex flex-col gap-4" onsubmit="register(event)">
<div class="flex flex-col gap-1">
<label for="username"><b>Username</b></label>
<input
type="text"
placeholder="username"
required
name="username"
id="username"
/>
</div>
<div class="flex flex-col gap-1">
<label for="username"><b>Password</b></label>
<input
type="password"
placeholder="password"
required
name="password"
id="password"
/>
</div>
<button>Submit</button>
</form>
<script>
function register(e) {
e.preventDefault();
fetch("/api/v1/auth/register", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
username: e.target.username.value,
password: e.target.password.value,
}),
})
.then((res) => res.json())
.then((res) => {
trigger("atto::toast", [
res.ok ? "sucesss" : "error",
res.message,
]);
if (res.ok) {
setTimeout(() => {
window.location.href = "/";
}, 150);
}
});
}
</script>
{% endblock %} {% block footer %}
<span class="small w-full text-center"
>Or, <a href="/auth/login">login</a></span
>
{% endblock %}

View file

@ -0,0 +1,45 @@
{% macro nav(selected="", show_lhs=true) -%}
<nav>
<div class="content_container">
<div class="flex nav_side">
<a href="/" class="button desktop title">
<b>{{ config.name }}</b>
</a>
{% if show_lhs %}
<a
href="/"
class="button {% if selected == 'home' %}active{% endif %}"
>Home</a
>
{% endif %}
</div>
<div class="flex nav_side">
{% if user %}
<div class="dropdown">
<button
class="flex-row title"
onclick="trigger('atto::hooks::dropdown', [event])"
exclude="dropdown"
style="gap: 0.25rem !important"
>
{{ macros::avatar(username=user.username, size="24px") }}
</button>
</div>
{% else %}
<a href="/auth/login" class="button">Login</a>
<a href="/auth/register" class="button">Register</a>
{% endif %}
</div>
</div>
</nav>
{%- endmacro %} {% macro avatar(username, size="24px") -%}
<img
title="{{ username }}'s avatar"
src="/api/v1/auth/profile/{{ username }}/avatar"
alt="@{{ username }}"
class="avatar shadow"
style="--size: {{ size }}"
/>
{%- endmacro %}

View file

@ -0,0 +1,23 @@
{% import "macros.html" as macros %} {% extends "root.html" %} {% block body %}
{{ macros::nav(selected="home") }}
<main class="flex flex-col gap-2">
<h1>Hello, world!</h1>
<div class="pillmenu">
<a class="active" href="#">A</a>
<a href="#">B</a>
<a href="#">C</a>
</div>
<div class="card w-full flex flex-col gap-2">
<div class="flex gap-2 flex-wrap">
<button>Hello, world!</button>
<button class="secondary">Hello, world!</button>
<button class="camo">Hello, world!</button>
</div>
<input type="text" placeholder="abcd" />
</div>
</main>
{% endblock %}

View file

@ -0,0 +1,73 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<link rel="stylesheet" href="/css/style.css" />
<script src="/js/loader.js"></script>
<script defer async src="/js/atto.js"></script>
<script>
globalThis.ns_verbose = false;
globalThis.ns_config = {
root: "/js/",
verbose: globalThis.ns_verbose,
};
globalThis._app_base = {
name: "tetratto",
ns_store: {},
classes: {},
};
globalThis.no_policy = false;
</script>
<meta name="theme-color" content="{{ config.color }}" />
<meta name="description" content="{{ config.description }}" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="{{ config.name }}" />
<meta name="turbo-prefetch" content="false" />
<meta name="turbo-refresh-method" content="morph" />
<meta name="turbo-refresh-scroll" content="preserve" />
<script
src="https://unpkg.com/@hotwired/turbo@8.0.5/dist/turbo.es2017-esm.js"
type="module"
async
defer
></script>
{% block head %}{% endblock %}
</head>
<body>
<div id="toast_zone"></div>
{% block body %}{% endblock %}
<script data-turbo-permanent="true" id="init-script">
document.documentElement.addEventListener("turbo:load", () => {
const atto = ns("atto");
atto.disconnect_observers();
atto.clean_date_codes();
atto.link_filter();
atto["hooks::scroll"](document.body, document.documentElement);
atto["hooks::dropdown.init"](window);
atto["hooks::character_counter.init"]();
atto["hooks::long_text.init"]();
atto["hooks::alt"]();
// atto["hooks::ips"]();
atto["hooks::check_reactions"]();
atto["hooks::tabs"]();
atto["hooks::partial_embeds"]();
});
</script>
</body>
</html>

View file

@ -0,0 +1,3 @@
<svg width="460" height="460" viewBox="0 0 460 460" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="460" height="460" fill="#C9B1BC"/>
</svg>

After

Width:  |  Height:  |  Size: 159 B

View file

@ -0,0 +1,3 @@
<svg width="460" height="460" viewBox="0 0 460 460" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="460" height="460" fill="#C9B1BC"/>
</svg>

After

Width:  |  Height:  |  Size: 159 B

View file

@ -0,0 +1,618 @@
console.log("🐐 tetratto - https://github.com/trisuaso/tetratto");
// theme preference
function media_theme_pref() {
document.documentElement.removeAttribute("class");
if (
window.matchMedia("(prefers-color-scheme: dark)").matches &&
!window.localStorage.getItem("tetratto:theme")
) {
document.documentElement.classList.add("dark");
// window.localStorage.setItem("theme", "dark");
} else if (
window.matchMedia("(prefers-color-scheme: light)").matches &&
!window.localStorage.getItem("tetratto:theme")
) {
document.documentElement.classList.remove("dark");
// window.localStorage.setItem("theme", "light");
} else if (window.localStorage.getItem("tetratto:theme")) {
/* restore theme */
const current = window.localStorage.getItem("tetratto:theme");
document.documentElement.className = current;
}
}
function set_theme(theme) {
window.localStorage.setItem("tetratto:theme", theme);
document.documentElement.className = theme;
}
media_theme_pref();
// atto ns
(() => {
const self = reg_ns("atto");
// env
self.DEBOUNCE = [];
self.OBSERVERS = [];
// ...
self.define("try_use", (_, ns_name, callback) => {
// attempt to get existing namespace
if (globalThis._app_base.ns_store[`$${ns_name}`]) {
return callback(globalThis._app_base.ns_store[`$${ns_name}`]);
}
// otherwise, call normal use
use(ns_name, callback);
});
self.define("debounce", ({ $ }, name) => {
return new Promise((resolve, reject) => {
if ($.DEBOUNCE.includes(name)) {
return reject();
}
$.DEBOUNCE.push(name);
setTimeout(() => {
delete $.DEBOUNCE[$.DEBOUNCE.indexOf(name)];
}, 1000);
return resolve();
});
});
self.define("rel_date", (_, date) => {
// stolen and slightly modified because js dates suck
const diff = (new Date().getTime() - date.getTime()) / 1000;
const day_diff = Math.floor(diff / 86400);
if (Number.isNaN(day_diff) || day_diff < 0 || day_diff >= 31) {
return;
}
return (
(day_diff === 0 &&
((diff < 60 && "just now") ||
(diff < 120 && "1 minute ago") ||
// biome-ignore lint/style/useTemplate: ok
(diff < 3600 && Math.floor(diff / 60) + " minutes ago") ||
(diff < 7200 && "1 hour ago") ||
(diff < 86400 &&
// biome-ignore lint/style/useTemplate: ok
Math.floor(diff / 3600) + " hours ago"))) ||
(day_diff === 1 && "Yesterday") ||
// biome-ignore lint/style/useTemplate: ok
(day_diff < 7 && day_diff + " days ago") ||
// biome-ignore lint/style/useTemplate: ok
(day_diff < 31 && Math.ceil(day_diff / 7) + " weeks ago")
);
});
self.define("clean_date_codes", ({ $ }) => {
for (const element of Array.from(document.querySelectorAll(".date"))) {
if (element.getAttribute("data-unix")) {
// this allows us to run the function twice on the same page
// without errors from already rendered dates
element.innerText = element.getAttribute("data-unix");
}
element.setAttribute("data-unix", element.innerText);
const then = new Date(Number.parseInt(element.innerText));
if (Number.isNaN(element.innerText)) {
continue;
}
element.setAttribute("title", then.toLocaleString());
let pretty = $.rel_date(then);
if (screen.width < 900 && pretty !== undefined) {
// shorten dates even more for mobile
pretty = pretty
.replaceAll(" minutes ago", "m")
.replaceAll(" minute ago", "m")
.replaceAll(" hours ago", "h")
.replaceAll(" hour ago", "h")
.replaceAll(" days ago", "d")
.replaceAll(" day ago", "d")
.replaceAll(" weeks ago", "w")
.replaceAll(" week ago", "w")
.replaceAll(" months ago", "m")
.replaceAll(" month ago", "m")
.replaceAll(" years ago", "y")
.replaceAll(" year ago", "y");
}
element.innerText =
pretty === undefined ? then.toLocaleDateString() : pretty;
element.style.display = "inline-block";
}
});
self.define("copy_text", ({ $ }, text) => {
navigator.clipboard.writeText(text);
$.toast("success", "Copied!");
});
self.define("smooth_remove", (_, element, ms) => {
// run animation
element.style.animation = `fadeout ease-in-out 1 ${ms}ms forwards running`;
// remove
setTimeout(() => {
element.remove();
}, ms);
});
self.define("disconnect_observers", ({ $ }) => {
for (const observer of $.OBSERVERS) {
observer.disconnect();
}
$.OBSERVERS = [];
});
self.define("offload_work_to_client_when_in_view", (_, entry_callback) => {
// instead of spending the time on the server loading everything before
// returning the page, we can instead of just create an IntersectionObserver
// and send individual requests as we see the element it's needed for
const seen = [];
return new IntersectionObserver(
(entries) => {
for (const entry of entries) {
const element = entry.target;
if (!entry.isIntersecting || seen.includes(element)) {
continue;
}
seen.push(element);
entry_callback(element);
}
},
{
root: document.body,
rootMargin: "0px",
threshold: 1.0,
},
);
});
self.define("toggle_flex", (_, element) => {
if (element.style.display === "none") {
element.style.display = "flex";
} else {
element.style.display = "none";
}
});
// hooks
self.define("hooks::scroll", (_, scroll_element, track_element) => {
const goals = [150, 250, 500, 1000];
track_element.setAttribute("data-scroll", "0");
scroll_element.addEventListener("scroll", (e) => {
track_element.setAttribute("data-scroll", scroll_element.scrollTop);
for (const goal of goals) {
const name = `data-scroll-${goal}`;
if (scroll_element.scrollTop >= goal) {
track_element.setAttribute(name, "true");
} else {
track_element.removeAttribute(name);
}
}
});
});
self.define("hooks::dropdown.close", (_) => {
for (const dropdown of Array.from(
document.querySelectorAll(".inner.open"),
)) {
dropdown.classList.remove("open");
}
});
self.define("hooks::dropdown", ({ $ }, event) => {
event.stopImmediatePropagation();
let target = event.target;
while (!target.matches(".dropdown")) {
target = target.parentElement;
}
// close all others
$["hooks::dropdown.close"]();
// open
setTimeout(() => {
for (const dropdown of Array.from(
target.querySelectorAll(".inner"),
)) {
// check y
const box = target.getBoundingClientRect();
let parent = dropdown.parentElement;
while (!parent.matches("html, .window")) {
parent = parent.parentElement;
}
let parent_height = parent.getBoundingClientRect().y;
if (parent.nodeName === "HTML") {
parent_height = window.screen.height;
}
const scroll = window.scrollY;
const height = parent_height;
const y = box.y + scroll;
if (y > height - scroll - 300) {
dropdown.classList.add("top");
} else {
dropdown.classList.remove("top");
}
// open
dropdown.classList.add("open");
if (dropdown.classList.contains("open")) {
dropdown.removeAttribute("aria-hidden");
} else {
dropdown.setAttribute("aria-hidden", "true");
}
}
}, 5);
});
self.define("hooks::dropdown.init", (_, bind_to) => {
for (const dropdown of Array.from(
document.querySelectorAll(".inner"),
)) {
dropdown.setAttribute("aria-hidden", "true");
}
bind_to.addEventListener("click", (event) => {
if (
event.target.matches(".dropdown") ||
event.target.matches("[exclude=dropdown]")
) {
return;
}
for (const dropdown of Array.from(
document.querySelectorAll(".inner.open"),
)) {
dropdown.classList.remove("open");
}
});
});
self.define("hooks::character_counter", (_, event) => {
let target = event.target;
while (!target.matches("textarea, input")) {
target = target.parentElement;
}
const counter = document.getElementById(`${target.id}:counter`);
counter.innerText = `${target.value.length}/${target.getAttribute("maxlength")}`;
});
self.define("hooks::character_counter.init", (_, event) => {
for (const element of Array.from(
document.querySelectorAll("[hook=counter]") || [],
)) {
const counter = document.getElementById(`${element.id}:counter`);
counter.innerText = `0/${element.getAttribute("maxlength")}`;
element.addEventListener("keyup", (e) =>
app["hooks::character_counter"](e),
);
}
});
self.define("hooks::long", (_, element, full_text) => {
element.classList.remove("hook:long.hidden_text");
element.innerHTML = full_text;
});
self.define("hooks::long_text.init", (_, event) => {
for (const element of Array.from(
document.querySelectorAll("[hook=long]") || [],
)) {
const is_long = element.innerText.length >= 64 * 16;
if (!is_long) {
continue;
}
element.classList.add("hook:long.hidden_text");
if (element.getAttribute("hook-arg") === "lowered") {
element.classList.add("hook:long.hidden_text+lowered");
}
const html = element.innerHTML;
const short = html.slice(0, 64 * 16);
element.innerHTML = `${short}...`;
// event
const listener = () => {
app["hooks::long"](element, html);
element.removeEventListener("click", listener);
};
element.addEventListener("click", listener);
}
});
self.define("hooks::alt", (_) => {
for (const element of Array.from(
document.querySelectorAll("img") || [],
)) {
if (element.getAttribute("alt") && !element.getAttribute("title")) {
element.setAttribute("title", element.getAttribute("alt"));
}
}
});
self.define(
"hooks::attach_to_partial",
({ $ }, partial, full, attach, wrapper, page, run_on_load) => {
return new Promise((resolve, reject) => {
async function load_partial() {
const url = `${partial}${partial.includes("?") ? "&" : "?"}page=${page}`;
history.replaceState(
history.state,
"",
url.replace(partial, full),
);
fetch(url)
.then(async (res) => {
const text = await res.text();
if (
text.length < 100 ||
text.includes('data-marker="no-results"')
) {
// pretty much blank content, no more pages
wrapper.removeEventListener("scroll", event);
return resolve();
}
attach.innerHTML += text;
$.clean_date_codes();
$.link_filter();
$["hooks::alt"]();
})
.catch(() => {
// done scrolling, no more pages (http error)
wrapper.removeEventListener("scroll", event);
resolve();
});
}
const event = async () => {
if (
wrapper.scrollTop + wrapper.offsetHeight + 100 >
attach.offsetHeight
) {
self.debounce("app::partials")
.then(async () => {
if (document.getElementById("initial_loader")) {
console.log("partial blocked");
return;
}
// biome-ignore lint/style/noParameterAssign: no it isn't
page += 1;
await load_partial();
await $["hooks::partial_embeds"]();
})
.catch(() => {
console.log("partial stuck");
});
}
};
wrapper.addEventListener("scroll", event);
});
},
);
self.define("hooks::partial_embeds", (_) => {
for (const paragraph of Array.from(
document.querySelectorAll("span[class] p"),
)) {
const groups = /(\/\+r\/)([\w]+)/.exec(paragraph.innerText);
if (groups === null) {
continue;
}
// add embed
paragraph.innerText = paragraph.innerText.replace(groups[0], "");
paragraph.parentElement.innerHTML += `<include-partial
src="/_app/components/response.html?id=${groups[2]}&do_render_nested=false"
uses="app::clean_date_codes,app::link_filter,app::hooks::alt"
></include-partial>`;
}
});
self.define("hooks::check_reactions", async ({ $ }) => {
const observer = $.offload_work_to_client_when_in_view(
async (element) => {
const reaction = await (
await fetch(
`/api/v1/reactions/${element.getAttribute("hook-arg:id")}`,
)
).json();
if (reaction.success) {
element.classList.add("green");
element.querySelector("svg").classList.add("filled");
}
},
);
for (const element of Array.from(
document.querySelectorAll("[hook=check_reaction]") || [],
)) {
observer.observe(element);
}
$.OBSERVERS.push(observer);
});
self.define("hooks::tabs:switch", (_, tab) => {
// tab
for (const element of Array.from(
document.querySelectorAll("[data-tab]"),
)) {
element.classList.add("hidden");
}
document
.querySelector(`[data-tab="${tab}"]`)
.classList.remove("hidden");
// button
if (document.querySelector(`[data-tab-button="${tab}"]`)) {
for (const element of Array.from(
document.querySelectorAll("[data-tab-button]"),
)) {
element.classList.remove("active");
}
document
.querySelector(`[data-tab-button="${tab}"]`)
.classList.add("active");
}
});
self.define("hooks::tabs:check", ({ $ }, hash) => {
if (!hash || !hash.startsWith("#/")) {
return;
}
$["hooks::tabs:switch"](hash.replace("#/", ""));
});
self.define("hooks::tabs", ({ $ }) => {
$["hooks::tabs:check"](window.location.hash); // initial check
window.addEventListener("hashchange", (event) =>
$["hooks::tabs:check"](new URL(event.newURL).hash),
);
});
// web api replacements
self.define("prompt", (_, msg) => {
const dialog = document.getElementById("web_api_prompt");
document.getElementById("web_api_prompt:msg").innerText = msg;
return new Promise((resolve, _) => {
globalThis.web_api_prompt_submit = (value) => {
dialog.close();
return resolve(value);
};
dialog.showModal();
});
});
self.define("prompt_long", (_, msg) => {
const dialog = document.getElementById("web_api_prompt_long");
document.getElementById("web_api_prompt_long:msg").innerText = msg;
return new Promise((resolve, _) => {
globalThis.web_api_prompt_long_submit = (value) => {
dialog.close();
return resolve(value);
};
dialog.showModal();
});
});
self.define("confirm", (_, msg) => {
const dialog = document.getElementById("web_api_confirm");
document.getElementById("web_api_confirm:msg").innerText = msg;
return new Promise((resolve, _) => {
globalThis.web_api_confirm_submit = (value) => {
dialog.close();
return resolve(value);
};
dialog.showModal();
});
});
// toast
self.define("toast", ({ $ }, type, content, time_until_remove = 5) => {
const element = document.createElement("div");
element.id = "toast";
element.classList.add(type);
element.classList.add("toast");
element.innerHTML = `<span>${content
.replaceAll("<", "&lt")
.replaceAll(">", "&gt;")}</span>`;
document.getElementById("toast_zone").prepend(element);
const timer = document.createElement("span");
element.appendChild(timer);
timer.innerText = time_until_remove;
timer.classList.add("timer");
// start timer
setTimeout(() => {
clearInterval(count_interval);
$.smooth_remove(element, 500);
}, time_until_remove * 1000);
const count_interval = setInterval(() => {
// biome-ignore lint/style/noParameterAssign: no it isn't
time_until_remove -= 1;
timer.innerText = time_until_remove;
}, 1000);
});
// link filter
self.define("link_filter", (_) => {
for (const anchor of Array.from(document.querySelectorAll("a"))) {
if (anchor.href.length === 0) {
continue;
}
const url = new URL(anchor.href);
if (
anchor.href.startsWith("/") ||
anchor.href.startsWith("javascript:") ||
url.origin === window.location.origin
) {
continue;
}
anchor.addEventListener("click", (e) => {
e.preventDefault();
document.getElementById("link_filter_url").innerText =
anchor.href;
document.getElementById("link_filter_continue").href =
anchor.href;
document.getElementById("link_filter").showModal();
});
}
});
})();

View file

@ -0,0 +1,205 @@
//! https://github.com/trisuaso/tetratto
globalThis.ns_config = globalThis.ns_config || {
root: "/static/js/",
version: 0,
verbose: true,
};
globalThis._app_base = globalThis._app_base || { ns_store: {}, classes: {} };
function regns_log(level, ...args) {
if (globalThis.ns_config.verbose) {
console[level](...args);
} else {
return;
}
}
/// Query an existing namespace
globalThis.ns = (ns) => {
regns_log("info", "namespace query:", ns);
// get namespace from app base
const res = globalThis._app_base.ns_store[`$${ns}`];
if (!res) {
return console.error(
"namespace does not exist, please use one of the following:",
Object.keys(globalThis._app_base.ns_store),
);
}
return res;
};
/// Register a new namespace
globalThis.reg_ns = (ns, deps) => {
if (typeof ns !== "string") {
return console.error("type check failed on namespace:", ns);
}
if (!ns) {
return console.error("cannot register invalid namespace!");
}
if (globalThis._app_base.ns_store[`$${ns}`]) {
regns_log("warn", "overwriting existing namespace:", ns);
}
// register new blank namespace
globalThis._app_base.ns_store[`$${ns}`] = {
_ident: ns,
_deps: deps || [],
/// Pull dependencies (other namespaces) as listed in the given `deps` argument
_get_deps: () => {
const self = globalThis._app_base.ns_store[`$${ns}`];
const deps = {};
for (const dep of self._deps) {
const res = globalThis.ns(dep);
if (!res) {
regns_log("warn", "failed to pull dependency:", dep);
continue;
}
deps[dep] = res;
}
deps.$ = self; // give access to self through $
return deps;
},
/// Store the real versions of functions
_fn_store: {},
/// Call a function in a namespace and load namespace dependencies
define: (name, func, types) => {
const self = globalThis.ns(ns);
self._fn_store[name] = func; // store real function
self[name] = function (...args) {
regns_log("info", "namespace call:", ns, name);
// js doesn't provide type checking, we do
if (types) {
for (const i in args) {
// biome-ignore lint: this is incorrect, you do not need a string literal to use typeof
if (types[i] && typeof args[i] !== types[i]) {
return console.error(
"argument does not pass type check:",
i,
args[i],
);
}
}
}
// ...
// we MUST return here, otherwise nothing will work in workers
return self._fn_store[name](self._get_deps(), ...args); // call with deps and arguments
};
},
};
regns_log("log", "registered namespace:", ns);
return globalThis._app_base.ns_store[`$${ns}`];
};
/// Call a namespace function quickly
globalThis.trigger = (id, args) => {
// get namespace
const s = id.split("::");
const [namespace, func] = [s[0], s.slice(1, s.length).join("::")];
const self = ns(namespace);
if (!self) {
return console.error("namespace does not exist:", namespace);
}
if (!self[func]) {
return console.error("namespace function does not exist:", id);
}
return self[func](...(args || []));
};
/// Import a namespace from path (relative to ns_config.root)
globalThis.use = (id, callback) => {
let file = id;
if (id.includes(".h.")) {
const split = id.split(".h.");
file = split[1];
}
// check if namespace already exists
const res = globalThis._app_base.ns_store[`$${file}`];
if (res) {
return callback(res);
}
// create script to load
const script = document.createElement("script");
script.src = `${globalThis.ns_config.root}${id}.js?v=${globalThis.ns_config.version}`;
script.id = `${globalThis.ns_config.version}-${file}.js`;
document.head.appendChild(script);
script.setAttribute("data-turbo-permanent", "true");
script.setAttribute("data-registered", new Date().toISOString());
script.setAttribute("data-version", globalThis.ns_config.version);
// run callback once the script loads
script.addEventListener("load", () => {
const res = globalThis._app_base.ns_store[`$${file}`];
if (!res) {
return console.error("imported namespace failed to register:", id);
}
callback(res);
});
};
// classes
/// Import a class from path (relative to ns_config.root/classes)
globalThis.require = (id, callback) => {
let file = id;
if (id.includes(".h.")) {
const split = id.split(".h.");
file = split[1];
}
// check if class already exists
const res = globalThis._app_base.classes[file];
if (res) {
return callback(res);
}
// create script to load
const script = document.createElement("script");
script.src = `${globalThis.ns_config.root}classes/${id}.js?v=${globalThis.ns_config.version}`;
script.id = `${globalThis.ns_config.version}-${file}.class.js`;
document.head.appendChild(script);
script.setAttribute("data-turbo-permanent", "true");
script.setAttribute("data-registered", new Date().toISOString());
script.setAttribute("data-version", globalThis.ns_config.version);
// run callback once the script loads
script.addEventListener("load", () => {
const res = globalThis._app_base.classes[file];
if (!res) {
return console.error("imported class failed to register:", id);
}
callback(res);
});
};
globalThis.define = (class_name, class_) => {
globalThis._app_base.classes[class_name] = class_;
regns_log("log", "registered class:", class_name);
};