From 152faaecee76428882ef2c508a16564b5a4c4e36 Mon Sep 17 00:00:00 2001 From: metacryst Date: Fri, 31 Oct 2025 19:51:09 -0500 Subject: [PATCH] 1: UI works, receiving location updates --- .gitignore | 4 + model/Node.js | 10 + package.json | 22 + server/auth.js | 46 +++ server/db.js | 77 ++++ server/handlers.js | 7 + server/index.js | 152 +++++++ ui/_/code/quill.js | 826 ++++++++++++++++++++++++++++++++++++++ ui/_/icons/logo.svg | 24 ++ ui/app/components/Home.js | 12 + ui/app/index.html | 12 + ui/app/index.js | 2 + ui/auth/auth.html | 59 +++ 13 files changed, 1253 insertions(+) create mode 100644 .gitignore create mode 100644 model/Node.js create mode 100644 package.json create mode 100644 server/auth.js create mode 100644 server/db.js create mode 100644 server/handlers.js create mode 100644 server/index.js create mode 100644 ui/_/code/quill.js create mode 100644 ui/_/icons/logo.svg create mode 100644 ui/app/components/Home.js create mode 100644 ui/app/index.html create mode 100644 ui/app/index.js create mode 100644 ui/auth/auth.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5887c64 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +db/db.json +.env +node_modules +package-lock.json \ No newline at end of file diff --git a/model/Node.js b/model/Node.js new file mode 100644 index 0000000..b43ef0a --- /dev/null +++ b/model/Node.js @@ -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)}`) + } + } +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..5f19068 --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/server/auth.js b/server/auth.js new file mode 100644 index 0000000..e707387 --- /dev/null +++ b/server/auth.js @@ -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) + } +} \ No newline at end of file diff --git a/server/db.js b/server/db.js new file mode 100644 index 0000000..a2f98e6 --- /dev/null +++ b/server/db.js @@ -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 } + } +} \ No newline at end of file diff --git a/server/handlers.js b/server/handlers.js new file mode 100644 index 0000000..84aff87 --- /dev/null +++ b/server/handlers.js @@ -0,0 +1,7 @@ +const handlers = { + updateLocation(req, res) { + console.log("req received") + } +} + +export default handlers; \ No newline at end of file diff --git a/server/index.js b/server/index.js new file mode 100644 index 0000000..f1afbbb --- /dev/null +++ b/server/index.js @@ -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() \ No newline at end of file diff --git a/ui/_/code/quill.js b/ui/_/code/quill.js new file mode 100644 index 0000000..7546c0f --- /dev/null +++ b/ui/_/code/quill.js @@ -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; +}; diff --git a/ui/_/icons/logo.svg b/ui/_/icons/logo.svg new file mode 100644 index 0000000..c7b523f --- /dev/null +++ b/ui/_/icons/logo.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ui/app/components/Home.js b/ui/app/components/Home.js new file mode 100644 index 0000000..e2b6494 --- /dev/null +++ b/ui/app/components/Home.js @@ -0,0 +1,12 @@ +class Home extends Shadow { + render() { + ZStack(() => { + + }) + .backgroundColor("#aebdff") + .display("block") + .width(100, vw).height(100, vh) + } +} + +registerShadow(Home) \ No newline at end of file diff --git a/ui/app/index.html b/ui/app/index.html new file mode 100644 index 0000000..f7b5f1d --- /dev/null +++ b/ui/app/index.html @@ -0,0 +1,12 @@ + + + + Blockcatcher + + + + + + + + \ No newline at end of file diff --git a/ui/app/index.js b/ui/app/index.js new file mode 100644 index 0000000..b14af88 --- /dev/null +++ b/ui/app/index.js @@ -0,0 +1,2 @@ +import "./components/Home.js" +Home() \ No newline at end of file diff --git a/ui/auth/auth.html b/ui/auth/auth.html new file mode 100644 index 0000000..e61e872 --- /dev/null +++ b/ui/auth/auth.html @@ -0,0 +1,59 @@ + + + + Login + + + + + + + \ No newline at end of file