1: UI works, receiving location updates

This commit is contained in:
metacryst
2025-10-31 19:51:09 -05:00
commit 152faaecee
13 changed files with 1253 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
db/db.json
.env
node_modules
package-lock.json

10
model/Node.js Normal file
View File

@@ -0,0 +1,10 @@
export default function Node(node) {
let traits = [
"labels"
]
for(let i = 0; i < traits.length; i++) {
if(!node[traits[i]]) {
throw new Error(`Node is missing field "${traits[i]}": ${JSON.stringify(node)}`)
}
}
}

22
package.json Normal file
View File

@@ -0,0 +1,22 @@
{
"name": "blockcatcher-admin",
"type": "module",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "node server/index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"chalk": "^5.6.2",
"cors": "^2.8.5",
"dotenv": "^17.2.3",
"express": "^4.18.2",
"http-proxy": "^1.18.1",
"jsonwebtoken": "^9.0.2",
"moment": "^2.30.1"
}
}

46
server/auth.js Normal file
View File

@@ -0,0 +1,46 @@
import dotenv from 'dotenv';
import chalk from 'chalk';
import jwt from 'jsonwebtoken'
import { randomUUID } from 'node:crypto'
dotenv.config();
export default class AuthHandler {
ips = new Map()
#secret
constructor() {
this.#secret = process.env.JWT_SECRET || 'random-id-for-now-123013123u1o23o12i3ukjdsbvkfndijnx1ijs';
}
check(req, res) {
if(this.ips.get(req.headers["x-forwarded-for"])) {
console.log(chalk.green(" ", req.headers["x-forwarded-for"]))
return true
} else {
console.log(chalk.red(" ", req.headers["x-forwarded-for"]))
return false
}
}
login(req, res) {
const { login } = req.body;
if(login === process.env.LOGIN) {
this.ips.set(req.headers["x-forwarded-for"], new Date())
return true;
} else {
return false
}
}
sign(payload, options = {}) {
return jwt.sign(
payload,
this.#secret,
{ expiresIn: '30d', ...options }
);
}
verify(token) {
return jwt.verify(token, this.#secret)
}
}

77
server/db.js Normal file
View File

@@ -0,0 +1,77 @@
import chalk from 'chalk'
import path from 'path';
import fs from 'fs/promises';
import { pathToFileURL } from 'url';
import Node from "../model/Node.js"
export default class Database {
#nodes;
#edges;
#labels = {}
constructor() {
this.getData()
}
async getData() {
const dbData = await fs.readFile(path.join(process.cwd(), 'db/db.json'), 'utf8');
let dbJson;
try {
dbJson = JSON.parse(dbData);
} catch {
dbJson = []
}
this.#nodes = dbJson["nodes"];
this.#edges = dbJson["edges"];
console.log(chalk.yellow("DB established."))
Object.preventExtensions(this);
}
// superKey = "nodes" || "edges"
async writeData(superKey, key, value) {
const dbData = await fs.readFile(path.join(process.cwd(), 'db/db.json'), 'utf8');
let dbJson;
try {
dbJson = JSON.parse(dbData);
} catch {
dbJson = []
}
dbJson[superKey][key] = value;
await fs.writeFile(path.join(process.cwd(), 'db/db.json'), JSON.stringify(dbJson, null, 2), 'utf8')
}
async getLabelModels() {
const labelHandlers = {};
const labelDir = path.join(process.cwd(), 'src/model/labels');
const files = await fs.readdir(labelDir);
for (const file of files) {
if (!file.endsWith('.js')) continue;
const label = path.basename(file, '.js');
const modulePath = path.join(labelDir, file);
const module = await import(pathToFileURL(modulePath).href);
labelHandlers[label] = module.default;
if (!this.#labels[label]) {
this.#labels[label] = [];
}
}
return labelHandlers
}
generateUserID() {
let id = this.#labels["User"].length + 1;
while (this.get.user(`user-${id}`)) {
id++;
}
return `user-${id}`; // O(1) most of the time
}
async getAll() {
return { nodes: this.#nodes }
}
}

7
server/handlers.js Normal file
View File

@@ -0,0 +1,7 @@
const handlers = {
updateLocation(req, res) {
console.log("req received")
}
}
export default handlers;

152
server/index.js Normal file
View File

@@ -0,0 +1,152 @@
import express from 'express';
import cors from 'cors'
import http from 'http'
import chalk from 'chalk'
import moment from 'moment'
import path from 'path';
import httpProxy from 'http-proxy';
const proxy = httpProxy.createProxyServer({});
import { fileURLToPath } from 'url';
import Database from "./db.js"
import AuthHandler from './auth.js';
import handlers from "./handlers.js";
// Get __dirname in ES6 environment
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
class Server {
db;
auth;
UIPath = path.join(__dirname, '../ui')
registerRoutes(router) {
router.post('/api/location', handlers.updateLocation)
router.post('/login', this.login)
router.get('/*', this.get)
return router
}
authMiddleware = (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader) {
return res.status(401).json({ error: 'Authorization token required.' });
}
const [scheme, token] = authHeader.split(' ');
if (scheme !== 'Bearer' || !token) {
return res.status(401).json({ error: 'Malformed authorization header.' })
}
try {
const payload = this.auth.verify(token);
req.user = payload;
return next();
} catch (err) {
return res.status(403).json({ error: 'Invalid or expired token.' });
}
}
login = async (req, res) => {
if(this.auth.login(req, res)) {
res.writeHead(302, { 'Location': "/" }).end()
// res.status(200).send();
} else {
res.status(400).send();
}
}
get = async (req, res) => {
if(!this.auth.check(req, res)) {
if(req.url === "/") {
res.sendFile(path.join(this.UIPath, 'auth/auth.html'));
return;
} else if(req.url === "/betsyross.svg") {
res.sendFile(path.join(this.UIPath, '_/betsyross.svg'));
return
} else {
return
}
} else {
let url = req.url
if(url === "/") {
url = "/index.html"
}
let filePath;
if(url.startsWith("/_")) {
filePath = path.join(this.UIPath, url);
} else {
filePath = path.join(this.UIPath, "app", url);
}
res.sendFile(filePath, (err) => {
if (err) {
console.error(`Error serving ${filePath}:`, err);
res.status(err.status || 500).send('File not found or error serving file.');
}
});
}
}
logRequest(req, res, next) {
const formattedDate = moment().format('M.D');
const formattedTime = moment().format('h:mma');
if(req.url.includes("/api/")) {
console.log(chalk.blue(` ${req.method} ${req.url} | ${formattedDate} ${formattedTime}`));
} else {
if(req.url === "/")
console.log(chalk.gray(` ${req.method} ${req.url} | ${formattedDate} ${formattedTime}`));
}
next();
}
logResponse(req, res, next) {
const originalSend = res.send;
res.send = function (body) {
if(res.statusCode >= 400) {
console.log(chalk.blue( `<-${chalk.red(res.statusCode)}- ${req.method} ${req.url} | ${chalk.red(body)}`));
} else {
console.log(chalk.blue(`<-${res.statusCode}- ${req.method} ${req.url}`));
}
originalSend.call(this, body);
};
next();
}
constructor() {
this.db = new Database()
this.auth = new AuthHandler()
const app = express();
app.use(cors({ origin: '*' }));
app.use(express.json());
app.use(this.logRequest);
app.use(this.logResponse);
let router = express.Router();
this.registerRoutes(router)
app.use('/', router);
const server = http.createServer(app);
const PORT = 3008;
server.listen(PORT, () => {
console.log("\n")
console.log(chalk.yellow("**************America****************"))
console.log(chalk.yellowBright(`Server is running on port ${PORT}: http://localhost`));
console.log(chalk.yellow("***************************************"))
console.log("\n")
});
process.on('SIGINT', async () => {
console.log(chalk.red('Closing server...'));
console.log(chalk.green('Database connection closed.'));
process.exit(0);
});
Object.preventExtensions(this);
}
}
const server = new Server()

826
ui/_/code/quill.js Normal file
View File

@@ -0,0 +1,826 @@
/* $ NAVIGATION */
let oldPushState = history.pushState;
history.pushState = function pushState() {
let ret = oldPushState.apply(this, arguments);
window.dispatchEvent(new Event('pushstate'));
window.dispatchEvent(new Event('navigate'));
return ret;
};
window.addEventListener('popstate', () => {
window.dispatchEvent(new Event('navigate'));
});
window.navigateTo = function(url) {
window.dispatchEvent(new Event('navigate'));
window.history.pushState({}, '', url);
}
/* $ SELECTOR */
HTMLElement.prototype.$ = function(selector) {
return window.$(selector, this)
}
DocumentFragment.prototype.$ = function(selector) {
return window.$(selector, this)
}
window.$ = function(selector, el = document) {
return el.querySelector(selector)
}
window.$$ = function(selector, el = document) {
return Array.from(el.querySelectorAll(selector))
}
/* CONSOLE */
console.red = function(message) {
this.log(`%c${message}`, "color: rgb(254, 79, 42);");
};
console.green = function(message) {
this.log(`%c${message}`, "color: rgb(79, 254, 42);");
}
/* GET CSS VARIABLES FOR DARK OR LIGHT MODE */
window.darkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
document.documentElement.classList.add(darkMode ? 'dark' : 'light');
window.getColor = function(name) {
const rootStyles = getComputedStyle(document.documentElement);
const color = rootStyles.getPropertyValue(`--${name}`).trim();
if(!color) {
throw new Error("Color not found")
}
return color
}
/* MOBILE */
window.isMobile = function isMobile() {
return /Android|iPhone|iPad|iPod|Opera Mini|IEMobile|WPDesktop/i.test(navigator.userAgent);
}
window.css = function css(cssString) {
let container = document.querySelector("style#pageStyle");
if(!container) {
container = document.createElement('style');
container.id = "pageStyle";
document.head.appendChild(container);
}
let primarySelector = cssString.substring(0, cssString.indexOf("{")).trim();
primarySelector = primarySelector.replace(/\*/g, "all");
primarySelector = primarySelector.replace(/#/g, "id-");
primarySelector = primarySelector.replace(/,/g, "");
let stylesheet = container.querySelector(`:scope > style[id='${primarySelector}']`)
if(!stylesheet) {
stylesheet = document.createElement('style');
stylesheet.id = primarySelector;
stylesheet.appendChild(document.createTextNode(cssString));
container.appendChild(stylesheet);
} else {
stylesheet.innerText = cssString
}
}
window.html = function html(elementString) {
let parser = new DOMParser();
let doc = parser.parseFromString(elementString, 'text/html');
return doc.body.firstChild;
}
window.util = {}
window.util.observeClassChange = (el, callback) => {
if (!el || !(el instanceof Element)) {
throw new Error("observeClassChange requires a valid DOM element.");
}
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type === "attributes" && mutation.attributeName === "class") {
callback(el.classList);
}
}
});
observer.observe(el, {
attributes: true,
attributeFilter: ["class"]
});
return observer; // Optional: return it so you can disconnect later
}
/* PAGE SETUP */
Object.defineProperty(Array.prototype, 'last', {
get() {
return this[this.length - 1];
},
enumerable: false,
});
/* QUILL */
window.quill = {
rendering: [],
render: (el) => {
if(el instanceof Shadow) {
let parent = quill.rendering[quill.rendering.length-1]
if(!parent) {
parent = document.body
}
parent.appendChild(el)
} else {
if(!el.render) {el.render = () => {}}
let parent = quill.rendering[quill.rendering.length-1]
if(!parent) throw new Error("Quill: no parent for element")
parent.appendChild(el)
}
quill.rendering.push(el)
el.render()
quill.rendering.pop(el)
},
rerender: (el) => {
Array.from(el.attributes).forEach(attr => el.removeAttribute(attr.name));
el.innerHTML = ""
el.removeAllListeners()
quill.rendering.push(el)
el.render()
quill.rendering.pop()
},
loadPage: () => {
let URL = window.location.pathname
if(!window.routes[URL]) {
throw new Error("No URL for this route: ", URL)
}
let pageClass = window[routes[URL]]
document.title = pageClass.title ?? document.title
document.body.innerHTML = ""
let page = new pageClass()
quill.render(page)
},
isStack: (el) => {
return el.classList.contains("HStack") || el.classList.contains("ZStack") || el.classList.contains("VStack")
},
}
window.Shadow = class extends HTMLElement {
constructor() {
super()
}
}
window.registerShadow = (el, tagname) => {
if (typeof el.prototype.render !== 'function') {
throw new Error("Element must have a render: " + el.prototype.constructor.name)
}
if(!tagname) {
tagname = el.prototype.constructor.name.toLowerCase() + "-"
}
customElements.define(tagname, el)
if(el.css) {
css(el.css)
}
window[el.prototype.constructor.name] = function (...params) {
let instance = new el(...params)
quill.render(instance)
return instance
}
}
HTMLElement.prototype.rerender = function() {
quill.rerender(this)
}
/* Styling */
window.vh = "vh"
window.vw = "vw"
window.px = "px"
window.em = "em"
window.rem = "rem"
window.inches = "in"
HTMLElement.prototype.addStyle = function(func) {
return func(this)
}
window.css = function css(cssString) {
let container = document.querySelector("style#pageStyle");
if(!container) {
container = document.createElement('style');
container.id = "pageStyle";
document.head.appendChild(container);
}
let primarySelector = cssString.substring(0, cssString.indexOf("{")).trim();
primarySelector = primarySelector.replace(/\*/g, "all");
primarySelector = primarySelector.replace(/#/g, "id-");
primarySelector = primarySelector.replace(/,/g, "");
let stylesheet = container.querySelector(`:scope > style[id='${primarySelector}']`)
if(!stylesheet) {
stylesheet = document.createElement('style');
stylesheet.id = primarySelector;
stylesheet.appendChild(document.createTextNode(cssString));
container.appendChild(stylesheet);
} else {
stylesheet.innerText = cssString
}
}
function extendHTMLElementWithStyleSetters() {
let allStyleProps = Object.keys(document.createElement("div").style)
allStyleProps.forEach(prop => {
if(prop === "translate") return
HTMLElement.prototype[prop] = function(value) {
this.style[prop] = value;
return this;
};
});
}
extendHTMLElementWithStyleSetters();
HTMLElement.prototype.padding = function(one, two, three = "px") {
const setPadding = (side, val) => {
const directionName = `padding${side.charAt(0).toUpperCase()}${side.slice(1)}`;
this.style[directionName] = (typeof val === 'number') ? `${val}${three}` : val;
};
if(one === "horizontal" || one === "vertical") { // is one a direction
if (one === "horizontal") {
setPadding("left", two);
setPadding("right", two);
} else if (one === "vertical") {
setPadding("top", two);
setPadding("bottom", two);
}
} else { // is two a value
if(typeof one !== 'number' || isNaN(one)) {
this.style.padding = one
} else {
this.style.padding = one + two
}
}
return this;
};
HTMLElement.prototype.paddingTop = function(value, unit = "px") {
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
throw new Error(`Invalid value: ${value}. Expected a number.`);
this.style.paddingTop = value + unit
return this
}
HTMLElement.prototype.paddingLeft = function(value, unit = "px") {
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
throw new Error(`Invalid value: ${value}. Expected a number.`);
this.style.paddingLeft = value + unit
return this
}
HTMLElement.prototype.paddingBottom = function(value, unit = "px") {
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
throw new Error(`Invalid value: ${value}. Expected a number.`);
this.style.paddingBottom = value + unit
return this
}
HTMLElement.prototype.paddingRight = function(value, unit = "px") {
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
throw new Error(`Invalid value: ${value}. Expected a number.`);
this.style.paddingRight = value + unit
return this
}
HTMLElement.prototype.margin = function(direction, value, unit = "px") {
if (!value) {
this.style.margin = direction;
return this;
}
const setMargin = (side, val) => {
const directionName = `margin${side.charAt(0).toUpperCase()}${side.slice(1)}`;
this.style[directionName] = (typeof val === 'number') ? `${val}${unit}` : val;
};
if (direction === "horizontal") {
setMargin("left", value);
setMargin("right", value);
} else if (direction === "vertical") {
setMargin("top", value);
setMargin("bottom", value);
} else {
setMargin(direction, value);
}
return this;
};
HTMLElement.prototype.marginTop = function(value, unit = "px") {
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
throw new Error(`Invalid value: ${value}. Expected a number.`);
this.style.marginTop = value + unit
return this
}
HTMLElement.prototype.marginLeft = function(value, unit = "px") {
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
throw new Error(`Invalid value: ${value}. Expected a number.`);
this.style.marginLeft = value + unit
return this
}
HTMLElement.prototype.marginBottom = function(value, unit = "px") {
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
throw new Error(`Invalid value: ${value}. Expected a number.`);
this.style.marginBottom = value + unit
return this
}
HTMLElement.prototype.marginRight = function(value, unit = "px") {
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
throw new Error(`Invalid value: ${value}. Expected a number.`);
this.style.marginRight = value + unit
return this
}
HTMLElement.prototype.width = function(value, unit = "px") {
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
throw new Error(`Invalid value: ${value}. Expected a number.`);
this.style.width = value + unit
return this
}
HTMLElement.prototype.minWidth = function(value, unit = "px") {
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
throw new Error(`Invalid value: ${value}. Expected a number.`);
this.style.minWidth = value + unit
return this
}
HTMLElement.prototype.height = function(value, unit = "px") {
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
throw new Error(`Invalid value: ${value}. Expected a number.`);
this.style.height = value + unit
return this
}
HTMLElement.prototype.minHeight = function(value, unit = "px") {
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
throw new Error(`Invalid value: ${value}. Expected a number.`);
this.style.minHeight = value + unit
return this
}
HTMLElement.prototype.fontSize = function(value, unit = "px") {
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
throw new Error(`Invalid value: ${value}. Expected a number.`);
switch(value) {
case "6xl":
value = "3.75"; unit = "rem"
break;
case "5xl":
value = "3"; unit = "rem"
break;
case "4xl":
value = "2.25"; unit = "rem"
break;
case "3xl":
value = "1.875"; unit = "rem"
break;
case "2xl":
value = "1.5"; unit = "rem"
break;
case "xl":
value = "1.25"; unit = "rem"
break;
case "l":
value = "1.125"; unit = "rem"
break;
case "s":
value = "0.875"; unit = "rem"
break;
case "xs":
value = "0.75"; unit = "rem"
break;
default:
break;
}
this.style.fontSize = value + unit
return this
}
function checkPositionType(el) {
let computed = window.getComputedStyle(el).position
if(!(computed === "absolute" || computed === "fixed")) {
el.style.position = "absolute"
}
}
HTMLElement.prototype.x = function(value, unit = "px") {
if (typeof value !== 'number' || isNaN(value))
throw new Error(`Invalid value: ${value}. Expected a number.`);
checkPositionType(this)
this.style.left = value + unit
return this
}
HTMLElement.prototype.y = function(value, unit = "px") {
if (typeof value !== 'number' || isNaN(value))
throw new Error(`Invalid value: ${value}. Expected a number.`);
checkPositionType(this)
this.style.top = value + unit
return this
}
HTMLElement.prototype.xRight = function(value, unit = "px") {
if (typeof value !== 'number' || isNaN(value))
throw new Error(`Invalid value: ${value}. Expected a number.`);
checkPositionType(this)
this.style.right = value + unit
return this
}
HTMLElement.prototype.yBottom = function(value, unit = "px") {
if (typeof value !== 'number' || isNaN(value))
throw new Error(`Invalid value: ${value}. Expected a number.`);
checkPositionType(this)
this.style.bottom = value + unit
return this
}
HTMLElement.prototype.borderRadius = function(value, unit = "px") {
if (typeof value !== 'number' || isNaN(value))
throw new Error(`Invalid value: ${value}. Expected a number.`);
this.style.borderRadius = value + unit
return this
}
HTMLElement.prototype.positionType = function (value) {
if(!(value === "absolute" || value === "relative" || value === "static" || value === "fixed" || value === "sticky")) {
console.error("HTMLElement.overlflow: must have valid overflow value!")
return;
}
this.style.position = value
return this
}
HTMLElement.prototype.gap = function(value, unit = "px") {
if (typeof value !== 'number' || Number.isNaN(value))
throw new Error(`Invalid value: ${value}. Expected a number.`);
this.style.gap = value + unit
return this
}
/* Elements */
quill.setChildren = function(el, innerContent) {
if(typeof innerContent === "string") {
el.innerText = innerContent
} else if(typeof innerContent === "function") {
el.render = innerContent
} else {
throw new Error("Children of unknown type")
}
}
window.a = function a( href, inner=href ) {
if(!href) throw new Error("quill a: missing href argument. Function: a( href, inner=href )")
let link = document.createElement("a")
link.setAttribute('href', href);
quill.setChildren(link, inner)
quill.render(link)
return link
}
window.img = function img(src, width="", height="") {
let image = document.createElement("img")
if(!src || !(typeof src==="string")) {
throw new Error("img: missing first argument: src | String")
} else {
image.src = src
}
if(width && typeof width === "string") {
image.style.width = width
} else if(width) {
image.style.width = width + "px"
}
if(height && typeof height === "string") {
image.style.height = height
} else if(height) {
image.style.height = height + "px"
}
quill.render(image)
return image
}
HTMLImageElement.prototype.backgroundColor = function(value) {
if (this.src.endsWith('.svg') || this.src.startsWith('data:image/svg+xml')) {
fetch(this.src).then(response => response.text())
.then(svgText => {
const modifiedSvg = svgText.replace(/\bfill="[^"]*"/g, `fill="${value}"`);
const blob = new Blob([modifiedSvg], { type: 'image/svg+xml' });
this.src = URL.createObjectURL(blob);
}).catch(error => {
console.error('Error updating SVG fill:', error);
});
} else {
this.style.backgroundColor = value;
}
return this; // Always returns the element itself
};
window.p = function p(innerText) {
let el = document.createElement("p")
if(typeof innerText === "function") {
el.render = innerText
} else {
el.innerText = innerText
}
el.style.margin = "0";
quill.render(el)
return el
}
window.h1 = function h1(innerText) {
let el = document.createElement("h1")
el.innerText = innerText
quill.render(el)
return el
}
window.h2 = function h2(innerText) {
let el = document.createElement("h2")
el.innerText = innerText
quill.render(el)
return el
}
window.h3 = function h3(innerText) {
let el = document.createElement("h3")
el.innerText = innerText
quill.render(el)
return el
}
window.div = function (innerText) {
let el = document.createElement("div")
el.innerText = innerText ?? ""
quill.render(el)
return el
}
window.span = function (innerText) {
let el = document.createElement("span")
el.innerText = innerText
quill.render(el)
return el
}
window.button = function (children) {
let el = document.createElement("button")
quill.setChildren(el, children)
quill.render(el)
return el
}
window.form = function(cb) {
let el = document.createElement("form")
el.render = cb
quill.render(el)
return el
}
window.input = function(placeholder, width, height) {
let el = document.createElement("input")
el.placeholder = placeholder
el.style.width = width
el.style.height = height
quill.render(el)
return el
}
window.label = function(text) {
let el = document.createElement("label")
el.innerText = text
quill.render(el)
return el
}
window.textarea = function(placeholder) {
let el = document.createElement("textarea")
el.placeholder = placeholder
quill.render(el)
return el
}
/* STACKS */
window.VStack = function (cb = () => {}) {
let styles = `
display: flex;
flex-direction: column;
`
let nowRendering = quill.rendering[quill.rendering.length-1]
if (nowRendering.innerHTML.trim() === "" && !quill.isStack(nowRendering)) {
nowRendering.style.cssText += styles
nowRendering.classList.add("VStack")
cb()
return nowRendering
}
let div = document.createElement("div")
div.classList.add("VStack")
div.style.cssText += styles
div.render = cb
quill.render(div)
return div
}
window.HStack = function (cb = () => {}) {
let styles = `
display: flex;
flex-direction: row;
`;
let nowRendering = quill.rendering[quill.rendering.length - 1];
if (nowRendering.innerHTML.trim() === "" && !quill.isStack(nowRendering)) {
nowRendering.style.cssText += styles;
nowRendering.classList.add("HStack")
cb();
return nowRendering;
}
let div = document.createElement("div");
div.classList.add("HStack");
div.style.cssText += styles;
div.render = cb;
quill.render(div);
return div;
};
window.ZStack = function (cb = () => {}) {
let nowRendering = quill.rendering[quill.rendering.length - 1];
if (nowRendering.innerHTML.trim() === "" && !quill.isStack(nowRendering)) {
nowRendering.classList.add("ZStack")
cb();
return nowRendering;
}
let div = document.createElement("div");
div.classList.add("ZStack");
div.render = cb;
quill.render(div);
return div;
};
/* EVENTS */
HTMLElement.prototype.onAppear = function(func) {
func(this);
return this;
};
HTMLElement.prototype.onClick = function(func) {
this._storeListener("click", func);
return this;
};
HTMLElement.prototype.onMouseDown = function(func) {
this._storeListener("mousedown", func);
return this;
};
HTMLElement.prototype.onMouseUp = function(func) {
this._storeListener("mouseup", func);
return this;
};
HTMLElement.prototype.onRightClick = function(func) {
this._storeListener("contextmenu", func);
return this;
};
HTMLElement.prototype.onHover = function(cb) {
const onEnter = () => cb.call(this, true);
const onLeave = () => cb.call(this, false);
this._storeListener("mouseover", onEnter);
this._storeListener("mouseleave", onLeave);
return this;
};
HTMLElement.prototype.onFocus = function(cb) {
if (!this.matches('input, textarea, select, button')) {
throw new Error("Can't put focus event on non-form element!");
}
this._storeListener("focus", cb);
return this;
};
HTMLElement.prototype.onBlur = function(cb) {
if (!this.matches('input, textarea, select, button')) {
throw new Error("Can't put blur event on non-form element!");
}
this._storeListener("blur", cb);
return this;
};
HTMLElement.prototype.onKeyDown = function(cb) {
this._storeListener("keydown", cb);
return this;
};
/* QUIRK 1:
In all the other callback functions, the user can choose the scope of "this". It can be either the parent shadow or the element itself.
This listener only allows for the latter functionality. This is because the navigate event fires on the window.
Without binding, "this" would refer only to the window. So here we are compromising on one of the two.
*/
HTMLElement.prototype.onNavigate = function(cb) {
window._storeListener(window, "navigate", cb.bind(this));
return this;
};
HTMLElement.prototype.onEvent = function(name, cb) {
window._storeListener(window, name, cb);
return this;
};
HTMLElement.prototype._storeListener = function(type, handler, options) {
window._storeListener(this, type, handler, options)
}
window.__listeners = []
function _storeListener(target, type, handler, options) {
if (!target.__listeners) target.__listeners = [];
const optionsString = JSON.stringify(options);
const index = target.__listeners.findIndex(listener =>
listener.type === type &&
listener.handler.toString() === handler.toString() &&
JSON.stringify(listener.options) === optionsString
);
if (index === -1) { // Listener is new
target.addEventListener(type, handler, options);
target.__listeners.push({ type, handler, options });
} else { // Listener is a duplicate, can be replaced
const old = target.__listeners[index];
target.removeEventListener(old.type, old.handler, old.options);
// Replace with the new one
target.addEventListener(type, handler, options);
target.__listeners[index] = { type, handler, options };
}
}
HTMLElement.prototype.removeAllListeners = function() {
if (!this.__listeners) return;
for (const { type, handler, options } of this.__listeners) {
this.removeEventListener(type, handler, options);
}
this.__listeners = [];
return this;
};
/* ATTRIBUTES */
HTMLElement.prototype.attr = function(attributes) {
if (
typeof attributes !== "object" ||
attributes === null ||
Array.isArray(attributes)
) {
throw new TypeError("attr() expects an object with key-value pairs");
}
for (const [key, value] of Object.entries(attributes)) {
this.setAttribute(key, value);
}
return this;
};

24
ui/_/icons/logo.svg Normal file
View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="1235" height="650" viewBox="0 0 1235 650" version="1.1" id="svg890" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg">
<rect width="1235" height="650" fill="#bf0a30" id="rect860" x="0" y="0" style="stroke-width:0.5" />
<g fill="#ffffff" id="g888" transform="scale(0.5)">
<g id="Stripes3">
<rect id="Stripe" width="2470" height="100" y="100" x="0" />
<use xlink:href="#Stripe" y="200" id="use863" x="0" width="100%" height="100%" />
<use xlink:href="#Stripe" y="400" id="use865" x="0" width="100%" height="100%" />
</g>
<use xlink:href="#Stripes3" y="600" id="use868" x="0" width="100%" height="100%" />
<rect width="823.33331" height="700" fill="#002868" id="rect870" x="0" y="0" />
<g transform="translate(411.66667,350)" id="g886">
<path transform="matrix(0.047,0,0,0.047,0,-233)" id="Star" d="M 0,-1000 -587.78525,809.01699 951.05652,-309.01699 H -951.05652 L 587.78525,809.01699 Z" />
<g id="Stars4">
<use xlink:href="#Star" transform="rotate(27.692308)" id="use873" x="0" y="0" width="100%" height="100%" />
<use xlink:href="#Star" transform="rotate(55.384615)" id="use875" x="0" y="0" width="100%" height="100%" />
<use xlink:href="#Star" transform="rotate(83.076923)" id="use877" x="0" y="0" width="100%" height="100%" />
<use xlink:href="#Star" transform="rotate(110.76923)" id="use879" x="0" y="0" width="100%" height="100%" />
</g>
<use xlink:href="#Stars4" transform="rotate(110.76923)" id="use882" x="0" y="0" width="100%" height="100%" />
<use xlink:href="#Stars4" transform="rotate(-138.46154)" id="use884" x="0" y="0" width="100%" height="100%" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

12
ui/app/components/Home.js Normal file
View File

@@ -0,0 +1,12 @@
class Home extends Shadow {
render() {
ZStack(() => {
})
.backgroundColor("#aebdff")
.display("block")
.width(100, vw).height(100, vh)
}
}
registerShadow(Home)

12
ui/app/index.html Normal file
View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Blockcatcher</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="/_/icons/logo.svg">
<script src="/_/code/quill.js"></script>
<script type="module" src="/index.js"></script>
</head>
<body style="margin: 0px">
</body>
</html>

2
ui/app/index.js Normal file
View File

@@ -0,0 +1,2 @@
import "./components/Home.js"
Home()

59
ui/auth/auth.html Normal file
View File

@@ -0,0 +1,59 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Login</title>
<link rel="icon" href="/_/icons/logo.svg">
<script type="module">
document.addEventListener('DOMContentLoaded', () => {
const loginInput = document.getElementById('login');
// Listen for the Enter key to submit the login
loginInput.addEventListener('keydown', async (event) => {
if (event.key === 'Enter') { // Check if the key pressed is Enter
event.preventDefault(); // Prevent any default form submission behavior
// Get the value from the input field
const loginValue = loginInput.value;
// Send the login data to the backend using fetch
try {
const response = await fetch('/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ login: loginValue }),
});
const result = await response;
// Handle the response from the backend
if (response.ok) {
document.getElementById("login").style.borderBottom = "2px solid black"
window.location.reload()
} else {
document.getElementById("login").style.borderBottom = "2px solid red"
}
} catch (error) {
console.error('Error during login:', error);
document.getElementById("login").style.borderBottom = "2px solid red"
}
}
});
});
</script>
</head>
<body style="background-color: bisque">
<input id="login" type="text" style="
position: absolute;
top: 50vh;
left: 50vw;
transform: translate(-50%, -50%);
background-color: transparent;
border: 0px;
border-bottom: 2px solid black;
text-decoration: none;
outline: none;
">
</body>
</html>