diff --git a/.gitignore b/.gitignore index 5887c64..46397d7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ +node_modules +.DS_Store db/db.json .env -node_modules -package-lock.json \ No newline at end of file +package-lock.json +sample.octet-stream \ No newline at end of file diff --git a/package.json b/package.json index 17e9ee3..b9e698f 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "dependencies": { "chalk": "^5.6.2", "cors": "^2.8.5", + "csv-parser": "^3.2.0", "dotenv": "^17.2.3", "express": "^4.18.2", "http-proxy": "^1.18.1", diff --git a/server/db.js b/server/db.js index a2f98e6..50dec09 100644 --- a/server/db.js +++ b/server/db.js @@ -2,6 +2,7 @@ import chalk from 'chalk' import path from 'path'; import fs from 'fs/promises'; import { pathToFileURL } from 'url'; +import parse from "csv-parser" import Node from "../model/Node.js" export default class Database { @@ -74,4 +75,75 @@ export default class Database { async getAll() { return { nodes: this.#nodes } } + + async loadCSV(csvString) { + if (!csvString || typeof csvString !== 'string') { + throw new Error('loadCSVAsync: input must be a non-empty string'); + } + + return new Promise((resolve, reject) => { + const records = []; + + const parser = parse({ + columns: true, + skip_empty_lines: true, + trim: true, + relax_column_count: true, + bom: true + }); + + parser.on('readable', () => { + let row; + while ((row = parser.read()) !== null) { + records.push(this.cleanRow(row)); + } + }); + + parser.on('error', (err) => { + reject(err); + }); + + parser.on('end', () => { + resolve(records); + }); + + // Feed the CSV string + parser.write(csvString); + parser.end(); + }); + } + + cleanRow(row) { + const cleaned = {}; + + for (let [key, value] of Object.entries(row)) { + // 1. Clean header + key = key + .replace(/ -MyData/g, '') + .replace(/[^a-zA-Z0-9]/g, ' ') + .trim() + .replace(/\s+(.)/g, (_, c) => c.toUpperCase()) + .replace(/^(.)/, c => c.toLowerCase()); + + // 2. Empty → null + if (value === '' || value == null) { + cleaned[key] = null; + continue; + } + + // 3. Type conversion + if (key === 'registrationStatus') { + cleaned[key] = value === 'True' ? true : value === 'False' ? false : null; + } + else if (['confidences', 'personId', 'uid', 'stateVoterId'].includes(key)) { + const num = parseFloat(value); + cleaned[key] = isNaN(num) ? null : num; + } + else { + cleaned[key] = value; + } + } + + return cleaned; + } } \ No newline at end of file diff --git a/server/handlers.js b/server/handlers.js index 5adfd5f..51132c4 100644 --- a/server/handlers.js +++ b/server/handlers.js @@ -6,6 +6,13 @@ const handlers = { console.log(`Received location: (${name}, ${latitude}, ${longitude}) at ${timestamp}`); broadcast("update-location", { name, latitude, longitude, timestamp }); res.json({ success: true }); + }, + + async uploadCSV(req, res) { + const csvString = req.body; + console.log(csvString); + await global.db.uploadCSV(csvString) + res.json({ ok: true }); } } diff --git a/server/index.js b/server/index.js index eddfc2e..cdc9b1d 100644 --- a/server/index.js +++ b/server/index.js @@ -22,6 +22,7 @@ class Server { registerRoutes(router) { router.post('/api/location', handlers.updateLocation) + router.post('/csv', handlers.uploadCSV) router.post('/login', this.login) router.get('/*', this.get) return router @@ -115,7 +116,7 @@ class Server { } constructor() { - this.db = new Database() + global.db = new Database() this.auth = new AuthHandler() const app = express(); app.use(cors({ origin: '*' })); diff --git a/ui/_/code/quill.js b/ui/_/code/quill.js index 7546c0f..70b902b 100644 --- a/ui/_/code/quill.js +++ b/ui/_/code/quill.js @@ -1,826 +1 @@ -/* $ 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; -}; +let oldPushState = history.pushState; function extendHTMLElementWithStyleSetters() { ["accentColor", "additiveSymbols", "alignContent", "alignItems", "alignSelf", "alignmentBaseline", "all", "anchorName", "anchorScope", "animation", "animationComposition", "animationDelay", "animationDirection", "animationDuration", "animationFillMode", "animationIterationCount", "animationName", "animationPlayState", "animationRange", "animationRangeEnd", "animationRangeStart", "animationTimeline", "animationTimingFunction", "appRegion", "appearance", "ascentOverride", "aspectRatio", "backdropFilter", "backfaceVisibility", "background", "backgroundAttachment", "backgroundBlendMode", "backgroundClip", "backgroundColor", "backgroundImage", "backgroundOrigin", "backgroundPosition", "backgroundPositionX", "backgroundPositionY", "backgroundRepeat", "backgroundSize", "basePalette", "baselineShift", "baselineSource", "blockSize", "border", "borderBlock", "borderBlockColor", "borderBlockEnd", "borderBlockEndColor", "borderBlockEndStyle", "borderBlockEndWidth", "borderBlockStart", "borderBlockStartColor", "borderBlockStartStyle", "borderBlockStartWidth", "borderBlockStyle", "borderBlockWidth", "borderBottom", "borderBottomColor", "borderBottomLeftRadius", "borderBottomRightRadius", "borderBottomStyle", "borderBottomWidth", "borderCollapse", "borderColor", "borderEndEndRadius", "borderEndStartRadius", "borderImage", "borderImageOutset", "borderImageRepeat", "borderImageSlice", "borderImageSource", "borderImageWidth", "borderInline", "borderInlineColor", "borderInlineEnd", "borderInlineEndColor", "borderInlineEndStyle", "borderInlineEndWidth", "borderInlineStart", "borderInlineStartColor", "borderInlineStartStyle", "borderInlineStartWidth", "borderInlineStyle", "borderInlineWidth", "borderLeft", "borderLeftColor", "borderLeftStyle", "borderLeftWidth", "borderRadius", "borderRight", "borderRightColor", "borderRightStyle", "borderRightWidth", "borderSpacing", "borderStartEndRadius", "borderStartStartRadius", "borderStyle", "borderTop", "borderTopColor", "borderTopLeftRadius", "borderTopRightRadius", "borderTopStyle", "borderTopWidth", "borderWidth", "bottom", "boxDecorationBreak", "boxShadow", "boxSizing", "breakAfter", "breakBefore", "breakInside", "bufferedRendering", "captionSide", "caretAnimation", "caretColor", "clear", "clip", "clipPath", "clipRule", "color", "colorInterpolation", "colorInterpolationFilters", "colorRendering", "colorScheme", "columnCount", "columnFill", "columnGap", "columnRule", "columnRuleColor", "columnRuleStyle", "columnRuleWidth", "columnSpan", "columnWidth", "columns", "contain", "containIntrinsicBlockSize", "containIntrinsicHeight", "containIntrinsicInlineSize", "containIntrinsicSize", "containIntrinsicWidth", "container", "containerName", "containerType", "content", "contentVisibility", "cornerBlockEndShape", "cornerBlockStartShape", "cornerBottomLeftShape", "cornerBottomRightShape", "cornerBottomShape", "cornerEndEndShape", "cornerEndStartShape", "cornerInlineEndShape", "cornerInlineStartShape", "cornerLeftShape", "cornerRightShape", "cornerShape", "cornerStartEndShape", "cornerStartStartShape", "cornerTopLeftShape", "cornerTopRightShape", "cornerTopShape", "counterIncrement", "counterReset", "counterSet", "cursor", "cx", "cy", "d", "descentOverride", "direction", "display", "dominantBaseline", "dynamicRangeLimit", "emptyCells", "fallback", "fieldSizing", "fill", "fillOpacity", "fillRule", "filter", "flex", "flexBasis", "flexDirection", "flexFlow", "flexGrow", "flexShrink", "flexWrap", "float", "floodColor", "floodOpacity", "font", "fontDisplay", "fontFamily", "fontFeatureSettings", "fontKerning", "fontOpticalSizing", "fontPalette", "fontSize", "fontSizeAdjust", "fontStretch", "fontStyle", "fontSynthesis", "fontSynthesisSmallCaps", "fontSynthesisStyle", "fontSynthesisWeight", "fontVariant", "fontVariantAlternates", "fontVariantCaps", "fontVariantEastAsian", "fontVariantEmoji", "fontVariantLigatures", "fontVariantNumeric", "fontVariantPosition", "fontVariationSettings", "fontWeight", "forcedColorAdjust", "gap", "grid", "gridArea", "gridAutoColumns", "gridAutoFlow", "gridAutoRows", "gridColumn", "gridColumnEnd", "gridColumnGap", "gridColumnStart", "gridGap", "gridRow", "gridRowEnd", "gridRowGap", "gridRowStart", "gridTemplate", "gridTemplateAreas", "gridTemplateColumns", "gridTemplateRows", "height", "hyphenateCharacter", "hyphenateLimitChars", "hyphens", "imageOrientation", "imageRendering", "inherits", "initialLetter", "initialValue", "inlineSize", "inset", "insetBlock", "insetBlockEnd", "insetBlockStart", "insetInline", "insetInlineEnd", "insetInlineStart", "interactivity", "interpolateSize", "isolation", "justifyContent", "justifyItems", "justifySelf", "left", "letterSpacing", "lightingColor", "lineBreak", "lineGapOverride", "lineHeight", "listStyle", "listStyleImage", "listStylePosition", "listStyleType", "margin", "marginBlock", "marginBlockEnd", "marginBlockStart", "marginBottom", "marginInline", "marginInlineEnd", "marginInlineStart", "marginLeft", "marginRight", "marginTop", "marker", "markerEnd", "markerMid", "markerStart", "mask", "maskClip", "maskComposite", "maskImage", "maskMode", "maskOrigin", "maskPosition", "maskRepeat", "maskSize", "maskType", "mathDepth", "mathShift", "mathStyle", "maxBlockSize", "maxHeight", "maxInlineSize", "maxWidth", "minBlockSize", "minHeight", "minInlineSize", "minWidth", "mixBlendMode", "navigation", "negative", "objectFit", "objectPosition", "objectViewBox", "offset", "offsetAnchor", "offsetDistance", "offsetPath", "offsetPosition", "offsetRotate", "opacity", "order", "orphans", "outline", "outlineColor", "outlineOffset", "outlineStyle", "outlineWidth", "overflow", "overflowAnchor", "overflowBlock", "overflowClipMargin", "overflowInline", "overflowWrap", "overflowX", "overflowY", "overlay", "overrideColors", "overscrollBehavior", "overscrollBehaviorBlock", "overscrollBehaviorInline", "overscrollBehaviorX", "overscrollBehaviorY", "pad", "padding", "paddingBlock", "paddingBlockEnd", "paddingBlockStart", "paddingBottom", "paddingInline", "paddingInlineEnd", "paddingInlineStart", "paddingLeft", "paddingRight", "paddingTop", "page", "pageBreakAfter", "pageBreakBefore", "pageBreakInside", "pageOrientation", "paintOrder", "perspective", "perspectiveOrigin", "placeContent", "placeItems", "placeSelf", "pointerEvents", "position", "positionAnchor", "positionArea", "positionTry", "positionTryFallbacks", "positionTryOrder", "positionVisibility", "prefix", "printColorAdjust", "quotes", "r", "range", "readingFlow", "readingOrder", "resize", "result", "right", "rotate", "rowGap", "rubyAlign", "rubyPosition", "rx", "ry", "scale", "scrollBehavior", "scrollInitialTarget", "scrollMargin", "scrollMarginBlock", "scrollMarginBlockEnd", "scrollMarginBlockStart", "scrollMarginBottom", "scrollMarginInline", "scrollMarginInlineEnd", "scrollMarginInlineStart", "scrollMarginLeft", "scrollMarginRight", "scrollMarginTop", "scrollMarkerGroup", "scrollPadding", "scrollPaddingBlock", "scrollPaddingBlockEnd", "scrollPaddingBlockStart", "scrollPaddingBottom", "scrollPaddingInline", "scrollPaddingInlineEnd", "scrollPaddingInlineStart", "scrollPaddingLeft", "scrollPaddingRight", "scrollPaddingTop", "scrollSnapAlign", "scrollSnapStop", "scrollSnapType", "scrollTargetGroup", "scrollTimeline", "scrollTimelineAxis", "scrollTimelineName", "scrollbarColor", "scrollbarGutter", "scrollbarWidth", "shapeImageThreshold", "shapeMargin", "shapeOutside", "shapeRendering", "size", "sizeAdjust", "speak", "speakAs", "src", "stopColor", "stopOpacity", "stroke", "strokeDasharray", "strokeDashoffset", "strokeLinecap", "strokeLinejoin", "strokeMiterlimit", "strokeOpacity", "strokeWidth", "suffix", "symbols", "syntax", "system", "tabSize", "tableLayout", "textAlign", "textAlignLast", "textAnchor", "textAutospace", "textBox", "textBoxEdge", "textBoxTrim", "textCombineUpright", "textDecoration", "textDecorationColor", "textDecorationLine", "textDecorationSkipInk", "textDecorationStyle", "textDecorationThickness", "textEmphasis", "textEmphasisColor", "textEmphasisPosition", "textEmphasisStyle", "textIndent", "textOrientation", "textOverflow", "textRendering", "textShadow", "textSizeAdjust", "textSpacingTrim", "textTransform", "textUnderlineOffset", "textUnderlinePosition", "textWrap", "textWrapMode", "textWrapStyle", "timelineScope", "top", "touchAction", "transform", "transformBox", "transformOrigin", "transformStyle", "transition", "transitionBehavior", "transitionDelay", "transitionDuration", "transitionProperty", "transitionTimingFunction", "translate", "types", "unicodeBidi", "unicodeRange", "userSelect", "vectorEffect", "verticalAlign", "viewTimeline", "viewTimelineAxis", "viewTimelineInset", "viewTimelineName", "viewTransitionClass", "viewTransitionGroup", "viewTransitionName", "visibility", "webkitAlignContent", "webkitAlignItems", "webkitAlignSelf", "webkitAnimation", "webkitAnimationDelay", "webkitAnimationDirection", "webkitAnimationDuration", "webkitAnimationFillMode", "webkitAnimationIterationCount", "webkitAnimationName", "webkitAnimationPlayState", "webkitAnimationTimingFunction", "webkitAppRegion", "webkitAppearance", "webkitBackfaceVisibility", "webkitBackgroundClip", "webkitBackgroundOrigin", "webkitBackgroundSize", "webkitBorderAfter", "webkitBorderAfterColor", "webkitBorderAfterStyle", "webkitBorderAfterWidth", "webkitBorderBefore", "webkitBorderBeforeColor", "webkitBorderBeforeStyle", "webkitBorderBeforeWidth", "webkitBorderBottomLeftRadius", "webkitBorderBottomRightRadius", "webkitBorderEnd", "webkitBorderEndColor", "webkitBorderEndStyle", "webkitBorderEndWidth", "webkitBorderHorizontalSpacing", "webkitBorderImage", "webkitBorderRadius", "webkitBorderStart", "webkitBorderStartColor", "webkitBorderStartStyle", "webkitBorderStartWidth", "webkitBorderTopLeftRadius", "webkitBorderTopRightRadius", "webkitBorderVerticalSpacing", "webkitBoxAlign", "webkitBoxDecorationBreak", "webkitBoxDirection", "webkitBoxFlex", "webkitBoxOrdinalGroup", "webkitBoxOrient", "webkitBoxPack", "webkitBoxReflect", "webkitBoxShadow", "webkitBoxSizing", "webkitClipPath", "webkitColumnBreakAfter", "webkitColumnBreakBefore", "webkitColumnBreakInside", "webkitColumnCount", "webkitColumnGap", "webkitColumnRule", "webkitColumnRuleColor", "webkitColumnRuleStyle", "webkitColumnRuleWidth", "webkitColumnSpan", "webkitColumnWidth", "webkitColumns", "webkitFilter", "webkitFlex", "webkitFlexBasis", "webkitFlexDirection", "webkitFlexFlow", "webkitFlexGrow", "webkitFlexShrink", "webkitFlexWrap", "webkitFontFeatureSettings", "webkitFontSmoothing", "webkitHyphenateCharacter", "webkitJustifyContent", "webkitLineBreak", "webkitLineClamp", "webkitLocale", "webkitLogicalHeight", "webkitLogicalWidth", "webkitMarginAfter", "webkitMarginBefore", "webkitMarginEnd", "webkitMarginStart", "webkitMask", "webkitMaskBoxImage", "webkitMaskBoxImageOutset", "webkitMaskBoxImageRepeat", "webkitMaskBoxImageSlice", "webkitMaskBoxImageSource", "webkitMaskBoxImageWidth", "webkitMaskClip", "webkitMaskComposite", "webkitMaskImage", "webkitMaskOrigin", "webkitMaskPosition", "webkitMaskPositionX", "webkitMaskPositionY", "webkitMaskRepeat", "webkitMaskSize", "webkitMaxLogicalHeight", "webkitMaxLogicalWidth", "webkitMinLogicalHeight", "webkitMinLogicalWidth", "webkitOpacity", "webkitOrder", "webkitPaddingAfter", "webkitPaddingBefore", "webkitPaddingEnd", "webkitPaddingStart", "webkitPerspective", "webkitPerspectiveOrigin", "webkitPerspectiveOriginX", "webkitPerspectiveOriginY", "webkitPrintColorAdjust", "webkitRtlOrdering", "webkitRubyPosition", "webkitShapeImageThreshold", "webkitShapeMargin", "webkitShapeOutside", "webkitTapHighlightColor", "webkitTextCombine", "webkitTextDecorationsInEffect", "webkitTextEmphasis", "webkitTextEmphasisColor", "webkitTextEmphasisPosition", "webkitTextEmphasisStyle", "webkitTextFillColor", "webkitTextOrientation", "webkitTextSecurity", "webkitTextSizeAdjust", "webkitTextStroke", "webkitTextStrokeColor", "webkitTextStrokeWidth", "webkitTransform", "webkitTransformOrigin", "webkitTransformOriginX", "webkitTransformOriginY", "webkitTransformOriginZ", "webkitTransformStyle", "webkitTransition", "webkitTransitionDelay", "webkitTransitionDuration", "webkitTransitionProperty", "webkitTransitionTimingFunction", "webkitUserDrag", "webkitUserModify", "webkitUserSelect", "webkitWritingMode", "whiteSpace", "whiteSpaceCollapse", "widows", "width", "willChange", "wordBreak", "wordSpacing", "wordWrap", "writingMode", "x", "y", "zIndex", "zoom"].forEach(e => { if ("translate" === e) return; const t = function (e) { if (!(e in document.createElement("div").style)) return "invalid"; switch (e) { case "gap": case "borderRadius": case "width": case "height": case "maxWidth": case "maxHeight": case "minWidth": case "minHeight": case "left": case "top": case "bottom": case "right": case "padding": case "paddingLeft": case "paddingTop": case "paddingBottom": case "paddingRight": case "margin": case "marginLeft": case "marginTop": case "marginBottom": case "marginRight": case "textUnderlineOffset": case "letterSpacing": return "unit-number"; default: return "string" } }(e); switch (t) { case "unit-number": HTMLElement.prototype[e] = StyleFunction(function (t, n = "px") { if ("auto" === t) return this.style[e] = t, this; if (this.style[e] = t + n, "" !== t && "" === this.style[e]) throw new Error(`Invalid CSS value for ${e}: ` + t + n); return this }); break; case "string": HTMLElement.prototype[e] = StyleFunction(function (t) { if (this.style[e] = t, "" !== t && "" === this.style[e]) throw new Error(`Invalid CSS value for ${e}: ` + t); return this }) } }) } function StyleFunction(e) { let t = function (n, i = "px") { return "function" == typeof n ? (this.setUpState(t, n), this) : (e.call(this, n, i), this) }; return t } function checkPositionType(e) { let t = window.getComputedStyle(e).position; "absolute" !== t && "fixed" !== t && (e.style.position = "absolute") } function _storeListener(e, t, n, i) { e.__listeners || (e.__listeners = []); const r = JSON.stringify(i), o = e.__listeners.findIndex(e => e.type === t && e.handler.toString() === n.toString() && JSON.stringify(e.options) === r); if (-1 === o) e.addEventListener(t, n, i), e.__listeners.push({ type: t, handler: n, options: i }); else { const r = e.__listeners[o]; e.removeEventListener(r.type, r.handler, r.options), e.addEventListener(t, n, i), e.__listeners[o] = { type: t, handler: n, options: i } } } history.pushState = function () { let e = oldPushState.apply(this, arguments); return window.dispatchEvent(new Event("pushstate")), window.dispatchEvent(new Event("navigate")), e }, window.addEventListener("popstate", () => { window.dispatchEvent(new Event("navigate")) }), window.setQuery = function (e, t) { const n = new URL(window.location.href), i = n.searchParams; null == t ? i.delete(e) : i.set(e, t); const r = n.toString(); return history.replaceState(null, "", r), window.dispatchEvent(new Event("query-changed")), r }, window.navigateTo = function (e) { window.dispatchEvent(new Event("navigate")), window.history.pushState({}, "", e) }, HTMLElement.prototype.$ = function (e) { return window.$(e, this) }, DocumentFragment.prototype.$ = function (e) { return window.$(e, this) }, window.$ = function (e, t = document) { return t.querySelector(e) }, window.$$ = function (e, t = document) { return Array.from(t.querySelectorAll(e)) }, HTMLElement.prototype.$$ = function (e) { return window.$$(e, this) }, DocumentFragment.prototype.$$ = function (e) { return window.$$(e, this) }, console.red = function (e) { this.log(`%c${e}`, "color: rgb(254, 79, 42);") }, console.green = function (e) { this.log(`%c${e}`, "color: rgb(79, 254, 42);") }, window.darkMode = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches, document.documentElement.classList.add(darkMode ? "dark" : "light"), window.getColor = function (e) { const t = getComputedStyle(document.documentElement).getPropertyValue(`--${e}`).trim(); if (!t) throw new Error("Color not found"); return t }, window.isMobile = function () { return /Android|iPhone|iPad|iPod|Opera Mini|IEMobile|WPDesktop/i.test(navigator.userAgent) }, window.css = function (e) { let t = document.querySelector("style#pageStyle"); t || (t = document.createElement("style"), t.id = "pageStyle", document.head.appendChild(t)); let n = e.substring(0, e.indexOf("{")).trim(); n = n.replace(/\*/g, "all"), n = n.replace(/#/g, "id-"), n = n.replace(/,/g, ""); let i = t.querySelector(`:scope > style[id='${n}']`); i ? i.innerText = e : (i = document.createElement("style"), i.id = n, i.appendChild(document.createTextNode(e)), t.appendChild(i)) }, window.html = function (e) { return (new DOMParser).parseFromString(e, "text/html").body.firstChild }, window.util = {}, window.util.observeClassChange = (e, t) => { if (!(e && e instanceof Element)) throw new Error("observeClassChange requires a valid DOM element."); const n = new MutationObserver(n => { for (const i of n) "attributes" === i.type && "class" === i.attributeName && t(e.classList) }); return n.observe(e, { attributes: !0, attributeFilter: ["class"] }), n }, Object.defineProperty(Array.prototype, "last", { get() { return this[this.length - 1] }, enumerable: !1 }), window.quill = { rendering: [], lastState: null, render: e => { if (e instanceof Shadow) { let t = quill.rendering[quill.rendering.length - 1]; t || (t = document.body), t.appendChild(e) } else { e.render || (e.render = () => { }); let t = quill.rendering[quill.rendering.length - 1]; if (!t) throw new Error("Quill: no parent for element"); t.appendChild(e) } quill.rendering.push(e), e.render(), quill.rendering.pop(e) }, rerender: e => { Array.from(e.attributes).forEach(t => e.removeAttribute(t.name)), e.innerHTML = "", e.removeAllListeners(), quill.rendering.push(e), e.render(), quill.rendering.pop() }, isStack: e => e.classList.contains("HStack") || e.classList.contains("ZStack") || e.classList.contains("VStack") }, window.Shadow = class extends HTMLElement { constructor() { super() } }, window.register = (e, t) => { if ("function" != typeof e.prototype.render) throw new Error("Element must have a render: " + e.prototype.constructor.name); t || (t = e.prototype.constructor.name.toLowerCase() + "-"), customElements.define(t, e), e.css && css(e.css), window[e.prototype.constructor.name] = function (...t) { let n = new e(...t); if (n.state) { const o = new WeakMap; function i(e, t = []) { if (e && "object" == typeof e) { if (o.has(e)) return o.get(e); const a = new Proxy(e, function (e) { return { get(t, n, o) { if ("symbol" == typeof n) return Reflect.get(t, n, o); let a = Array.isArray(t) && !r(n) ? e : e.concat(n); quill.lastState = a.join("."); return i(Reflect.get(t, n, o), a) }, set(t, i, o, a) { const l = Array.isArray(t) ? t.length : void 0; if (t[i] === o) return !0; const s = Reflect.set(t, i, o, a); let c = !Array.isArray(t) || r(i) && t.length === l ? e.concat(i).join(".") : e; const d = n.stateWatchers[c]; return d && d.forEach(e => e()), s } } }(t)); return o.set(e, a), a } return e } function r(e) { return "string" == typeof e && "" !== e && String(+e) === e } let a = i(n.state); Object.defineProperty(n, "state", { value: a, writable: !1, configurable: !1, enumerable: !0 }); let l = {}; Object.keys(n.state).forEach(e => l[e] = []), Object.defineProperty(n, "stateWatchers", { value: l, writable: !1, configurable: !1, enumerable: !0 }) } return quill.render(n), n } }, HTMLElement.prototype.rerender = function () { quill.rerender(this) }, window.pct = "%", window.vmin = "vmin", window.vmax = "vmax", window.vh = "vh", window.vw = "vw", window.px = "px", window.em = "em", window.rem = "rem", window.inches = "in", HTMLElement.prototype.addStyle = function (e) { return e(this) }, window.css = function (e) { let t = document.querySelector("style#pageStyle"); t || (t = document.createElement("style"), t.id = "pageStyle", document.head.appendChild(t)); let n = e.substring(0, e.indexOf("{")).trim(); n = n.replace(/\*/g, "all"), n = n.replace(/#/g, "id-"), n = n.replace(/,/g, ""); let i = t.querySelector(`:scope > style[id='${n}']`); i ? i.innerText = e : (i = document.createElement("style"), i.id = n, i.appendChild(document.createTextNode(e)), t.appendChild(i)) }, extendHTMLElementWithStyleSetters(), HTMLElement.prototype.addStateWatcher = function (e, t) { let n = this; for (; !(n instanceof Shadow);)n = n.parentNode; n.stateWatchers[e].push(t) }, HTMLElement.prototype.setUpState = function (e, t) { let n = e => Array.isArray(e) ? e : [e], i = n(t()); if (!quill.lastState) throw new Error("Quill: style state function does not access valid state"); this.addStateWatcher(quill.lastState, () => { e.call(this, ...n(t())) }), quill.lastState = null, e.call(this, ...i) }, HTMLElement.prototype.styles = function (e) { return e.call(this, this), this }, HTMLElement.prototype.paddingVertical = StyleFunction(function (e, t = "px") { return this.style.paddingTop = e + t, this.style.paddingBottom = e + t, this }), HTMLElement.prototype.paddingHorizontal = StyleFunction(function (e, t = "px") { return this.style.paddingRight = e + t, this.style.paddingLeft = e + t, this }), HTMLElement.prototype.marginVertical = StyleFunction(function (e, t = "px") { return this.style.marginTop = e + t, this.style.marginBottom = e + t, this }), HTMLElement.prototype.marginHorizontal = StyleFunction(function (e, t = "px") { return this.style.marginRight = e + t, this.style.marginLeft = e + t, this }), HTMLElement.prototype.borderVertical = StyleFunction(function (e) { return this.style.borderTop = e, this.style.borderBottom = e, this }), HTMLElement.prototype.borderHorizontal = StyleFunction(function (e) { return this.style.borderRight = e, this.style.borderLeft = e, this }), HTMLElement.prototype.fontSize = StyleFunction(function (e, t = "px") { switch (e) { case "6xl": e = "3.75", t = "rem"; break; case "5xl": e = "3", t = "rem"; break; case "4xl": e = "2.25", t = "rem"; break; case "3xl": e = "1.875", t = "rem"; break; case "2xl": e = "1.5", t = "rem"; break; case "xl": e = "1.25", t = "rem"; break; case "l": e = "1.125", t = "rem"; break; case "s": e = "0.875", t = "rem"; break; case "xs": e = "0.75", t = "rem" }return this.style.fontSize = e + t, this }), HTMLElement.prototype.width = function (e, t = "px") { if ("number" != typeof e && "auto" !== e || Number.isNaN(e)) throw new Error(`Invalid value: ${e}. Expected a number.`); return this.style.width = e + t, "inline" === window.getComputedStyle(this).display && (this.style.display = "block"), this }, HTMLElement.prototype.height = function (e, t = "px") { if ("number" != typeof e && "auto" !== e || Number.isNaN(e)) throw new Error(`Invalid value: ${e}. Expected a number.`); return this.style.height = e + t, "inline" === window.getComputedStyle(this).display && (this.style.display = "block"), this }, HTMLElement.prototype.x = function (e, t = "px") { if ("number" != typeof e || isNaN(e)) throw new Error(`Invalid value: ${e}. Expected a number.`); return checkPositionType(this), this.style.left = e + t, this }, HTMLElement.prototype.y = function (e, t = "px") { if ("number" != typeof e || isNaN(e)) throw new Error(`Invalid value: ${e}. Expected a number.`); return checkPositionType(this), this.style.top = e + t, this }, HTMLElement.prototype.xRight = function (e, t = "px") { if ("number" != typeof e || isNaN(e)) throw new Error(`Invalid value: ${e}. Expected a number.`); return checkPositionType(this), this.style.right = e + t, this }, HTMLElement.prototype.yBottom = function (e, t = "px") { if ("number" != typeof e || isNaN(e)) throw new Error(`Invalid value: ${e}. Expected a number.`); return checkPositionType(this), this.style.bottom = e + t, this }, HTMLElement.prototype.backgroundImage = function (...e) { const t = e.map(e => (e.includes("/") && !e.includes("gradient") && (e = "url(" + e + ")"), String(e).trim())).join(", "); return this.style.backgroundImage = t, this }, HTMLElement.prototype.center = function () { return this.style.transform = "translate(-50%, -50%)", this }, HTMLElement.prototype.centerX = function () { return this.style.transform = "translateX(-50%)", this }, HTMLElement.prototype.centerY = function () { return this.style.transform = "translateY(-50%)", this }, HTMLElement.prototype.verticalAlign = function (e) { const t = getComputedStyle(this).flexDirection; if (!t) throw new Error("verticalAlign can be only be used on HStacks or VStacks!"); return "column" === t || "column-reverse" === t ? this.style.justifyContent = e : this.style.alignItems = e, this }, HTMLElement.prototype.horizontalAlign = function (e) { const t = getComputedStyle(this).flexDirection; if (!t) throw new Error("horizontalAlign can be only be used on HStacks or VStacks!"); return "column" === t || "column-reverse" === t ? this.style.alignItems = e : this.style.justifyContent = e, this }, quill.setChildren = function (e, t) { if ("string" == typeof t) e.innerText = t; else { if ("function" != typeof t) throw new Error("Children of unknown type"); e.render = t } }, window.a = function (e, t = e) { if (!e) throw new Error("quill a: missing href argument. Function: a( href, inner=href )"); let n = document.createElement("a"); return n.setAttribute("href", e), quill.setChildren(n, t), quill.render(n), n }, window.img = function (e, t = "", n = "") { let i = document.createElement("img"); if (!e || "string" != typeof e) throw new Error("img: missing first argument: src | String"); return i.src = e, t && "string" == typeof t ? i.style.width = t : t && (i.style.width = t + "px"), n && "string" == typeof n ? i.style.height = n : n && (i.style.height = n + "px"), quill.render(i), i }, HTMLImageElement.prototype.backgroundColor = function (e) { return this.src.endsWith(".svg") || this.src.startsWith("data:image/svg+xml") ? fetch(this.src).then(e => e.text()).then(t => { const n = t.replace(/\bfill="[^"]*"/g, `fill="${e}"`), i = new Blob([n], { type: "image/svg+xml" }); this.src = URL.createObjectURL(i) }).catch(e => { console.error("Error updating SVG fill:", e) }) : this.style.backgroundColor = e, this }, window.p = function (e) { let t = document.createElement("p"); return "function" == typeof innerText ? t.render = e : t.innerHTML = e, t.style.margin = "0", quill.render(t), t }, window.h1 = function (e) { let t = document.createElement("h1"); return t.innerText = e, quill.render(t), t }, window.h2 = function (e) { let t = document.createElement("h2"); return t.innerText = e, quill.render(t), t }, window.h3 = function (e) { let t = document.createElement("h3"); return t.innerText = e, quill.render(t), t }, window.div = function (e) { let t = document.createElement("div"); return t.innerText = e ?? "", quill.render(t), t }, window.span = function (e) { let t = document.createElement("span"); return t.innerText = e, quill.render(t), t }, window.button = function (e = "") { let t = document.createElement("button"); return quill.setChildren(t, e), quill.render(t), t }, window.form = function (e) { let t = document.createElement("form"); return t.render = e, quill.render(t), t }, window.input = function (e = "", t, n) { let i = document.createElement("input"); return i.placeholder = e, i.style.width = t, i.style.height = n, quill.render(i), i }, window.label = function (e) { let t = document.createElement("label"); return "string" == typeof e ? t.innerText = e : t.render = e, quill.render(t), t }, window.textarea = function (e = "") { let t = document.createElement("textarea"); return t.placeholder = e, quill.render(t), t }, handleStack = function (e, t, n = "") { let i = quill.rendering[quill.rendering.length - 1]; if ("" !== i.innerHTML.trim() || quill.isStack(i)) { let i = document.createElement("div"); return i.classList.add(t), i.style.cssText += n, i.render = e, quill.render(i), i } return i.style.cssText += n, i.classList.add(t), e(), quill.lastState && i.addStateWatcher(quill.lastState, () => { i.innerHTML = "", e() }), i }, window.VStack = function (e = () => { }) { return handleStack(e, "VStack", "\n display: flex;\n flex-direction: column;\n ") }, window.HStack = function (e = () => { }) { return handleStack(e, "HStack", "\n display: flex;\n flex-direction: row;\n ") }, window.ZStack = function (e = () => { }) { return handleStack(e, "ZStack") }, window.svgMethods = function (e) { e.pulse = function (e = 600) { return this.style.transition = `transform ${e}ms ease-in-out`, this.style.transform = "scale(1.2)", setTimeout(() => { this.style.transform = "scale(1)" }, e / 2), this }, e.rotate = function (e = 360, t = 1e3) { return this.style.transition = `transform ${t}ms linear`, this.style.transform = `rotate(${e}deg)`, this }, e.fill = function (e) { return this.setAttribute("fill", e), this }, e.height = function (e) { return this.setAttribute("height", e), this }, e.width = function (e) { return this.setAttribute("width", e), this }, e.stroke = function (e, t) { return this.setAttribute("stroke", t), this.setAttribute("stroke-width", e), this }, e.toggle = function () { return this.style.display = "none" === this.style.display ? "" : "none", this } }, window.Rectangle = function (e = "40px", t = "40px") { const n = "http://www.w3.org/2000/svg", i = document.createElementNS(n, "svg"), r = document.createElementNS(n, "rect"); return i.setAttribute("width", e), i.setAttribute("height", t), i.setAttribute("viewBox", "0 0 100 100"), i.setAttribute("preserveAspectRatio", "xMidYMid meet"), r.setAttribute("x", "15"), r.setAttribute("y", "15"), r.setAttribute("width", "70"), r.setAttribute("height", "70"), i.appendChild(r), svgMethods(i), quill.render(i), i }, window.Triangle = function (e = "40px", t = "40px") { const n = "http://www.w3.org/2000/svg", i = document.createElementNS(n, "svg"), r = document.createElementNS(n, "path"); return i.setAttribute("width", e), i.setAttribute("height", t), i.setAttribute("viewBox", "0 0 100 100"), i.setAttribute("preserveAspectRatio", "xMidYMid meet"), r.setAttribute("d", "M 25 15 L 90 50 L 25 85 Z"), i.appendChild(r), svgMethods(i), quill.render(i), i }, HTMLElement.prototype.onAppear = function (e) { return e.call(this), this }, HTMLElement.prototype.onClick = function (e) { return this._storeListener("mousedown", t => e.call(this, !1, t)), this._storeListener("mouseup", t => e.call(this, !0, t)), this }, HTMLElement.prototype.onMouseDown = function (e) { return this._storeListener("mousedown", e), this }, HTMLElement.prototype.onMouseUp = function (e) { return this._storeListener("mouseup", e), this }, HTMLElement.prototype.onRightClick = function (e) { return this._storeListener("contextmenu", e), this }, HTMLElement.prototype.onHover = function (e) { return this._storeListener("mouseover", t => e.call(this, !0, t)), this._storeListener("mouseleave", t => e.call(this, !1, t)), this }, HTMLElement.prototype.onFocus = function (e) { if (!this.matches("input, textarea, select, button")) throw new Error("Can't put focus event on non-form element!"); return this._storeListener("focus", () => e.call(this, !0)), this._storeListener("blur", () => e.call(this, !1)), this }, HTMLElement.prototype.onKeyDown = function (e) { return this._storeListener("keydown", e), this }, HTMLElement.prototype.onInput = function (e) { if (!this.matches('input, textarea, [contenteditable=""], [contenteditable="true"]')) throw new Error("Can't put input event on non-input element!"); return this._storeListener("input", e), this }, HTMLElement.prototype.onChange = function (e) { if (!this.matches('input, textarea, [contenteditable=""], [contenteditable="true"]')) throw new Error("Can't put input event on non-input element!"); return this._storeListener("change", e), this }, HTMLElement.prototype.onSubmit = function (e) { if (!this.matches("form")) throw new Error("Can't put form event on non-form element!"); return this._storeListener("submit", e), this }, HTMLElement.prototype.onTouch = function (e) { return this._storeListener("touchstart", () => e.call(this, !0)), this._storeListener("touchend", () => e.call(this, !1)), this._storeListener("touchcancel", () => e.call(this, null)), this }, HTMLElement.prototype.onTap = function (e) { return this._storeListener("touchend", e), this }, navigateListeners = [], window.addEventListener("navigate", () => { for (entry of navigateListeners) entry.el.dispatchEvent(new CustomEvent("navigate")) }), HTMLElement.prototype.onNavigate = function (e) { this._storeListener("navigate", e); let t = !1, n = Array.from(this.parentNode.children).indexOf(this); for (entry of navigateListeners) if (entry.cb.toString() === e.toString() && entry.index === n && this.nodeName === entry.el.nodeName) { t = !0; break } return !1 === t && navigateListeners.push({ el: this, cb: e, index: n }), this }, queryListeners = [], HTMLElement.prototype.onQueryChanged = function (e) { this._storeListener("query-changed", e); let t = !1; for (entry of queryListeners) if (entry.cb.toString() === e.toString() && this.nodeName === entry.el.nodeName) { t = !0; break } return !1 === t && queryListeners.push({ el: this, cb: e }), this }, window.addEventListener("query-changed", () => { for (entry of queryListeners) entry.el.dispatchEvent(new CustomEvent("query-changed")) }), HTMLElement.prototype.onEvent = function (e, t) { return window._storeListener(window, e, t), this }, HTMLElement.prototype._storeListener = function (e, t, n) { window._storeListener(this, e, t, n) }, window.__listeners = [], HTMLElement.prototype.removeAllListeners = function () { if (this.__listeners) { for (const { type: e, handler: t, options: n } of this.__listeners) this.removeEventListener(e, t, n); return this.__listeners = [], this } }, HTMLElement.prototype.attr = function (e) { if ("object" != typeof e || null === e || Array.isArray(e)) throw new TypeError("attr() expects an object with key-value pairs"); for (const [t, n] of Object.entries(e)) this.setAttribute(t, n); return this }; \ No newline at end of file diff --git a/ui/_/code/styles.css b/ui/_/code/styles.css new file mode 100644 index 0000000..882bd15 --- /dev/null +++ b/ui/_/code/styles.css @@ -0,0 +1,6 @@ +@font-face { + font-family: 'Inknut Antiqua'; + src: url('/_/fonts/Inknut_Antiqua/InknutAntiqua-Regular.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} \ No newline at end of file diff --git a/ui/_/fonts/Inknut_Antiqua/InknutAntiqua-Black.ttf b/ui/_/fonts/Inknut_Antiqua/InknutAntiqua-Black.ttf new file mode 100755 index 0000000..e4e1b09 Binary files /dev/null and b/ui/_/fonts/Inknut_Antiqua/InknutAntiqua-Black.ttf differ diff --git a/ui/_/fonts/Inknut_Antiqua/InknutAntiqua-Bold.ttf b/ui/_/fonts/Inknut_Antiqua/InknutAntiqua-Bold.ttf new file mode 100755 index 0000000..a926d32 Binary files /dev/null and b/ui/_/fonts/Inknut_Antiqua/InknutAntiqua-Bold.ttf differ diff --git a/ui/_/fonts/Inknut_Antiqua/InknutAntiqua-ExtraBold.ttf b/ui/_/fonts/Inknut_Antiqua/InknutAntiqua-ExtraBold.ttf new file mode 100755 index 0000000..62340b2 Binary files /dev/null and b/ui/_/fonts/Inknut_Antiqua/InknutAntiqua-ExtraBold.ttf differ diff --git a/ui/_/fonts/Inknut_Antiqua/InknutAntiqua-Light.ttf b/ui/_/fonts/Inknut_Antiqua/InknutAntiqua-Light.ttf new file mode 100755 index 0000000..b986946 Binary files /dev/null and b/ui/_/fonts/Inknut_Antiqua/InknutAntiqua-Light.ttf differ diff --git a/ui/_/fonts/Inknut_Antiqua/InknutAntiqua-Medium.ttf b/ui/_/fonts/Inknut_Antiqua/InknutAntiqua-Medium.ttf new file mode 100755 index 0000000..7c452a2 Binary files /dev/null and b/ui/_/fonts/Inknut_Antiqua/InknutAntiqua-Medium.ttf differ diff --git a/ui/_/fonts/Inknut_Antiqua/InknutAntiqua-Regular.ttf b/ui/_/fonts/Inknut_Antiqua/InknutAntiqua-Regular.ttf new file mode 100755 index 0000000..241f222 Binary files /dev/null and b/ui/_/fonts/Inknut_Antiqua/InknutAntiqua-Regular.ttf differ diff --git a/ui/_/fonts/Inknut_Antiqua/InknutAntiqua-SemiBold.ttf b/ui/_/fonts/Inknut_Antiqua/InknutAntiqua-SemiBold.ttf new file mode 100755 index 0000000..d169a5c Binary files /dev/null and b/ui/_/fonts/Inknut_Antiqua/InknutAntiqua-SemiBold.ttf differ diff --git a/ui/_/fonts/Inknut_Antiqua/OFL.txt b/ui/_/fonts/Inknut_Antiqua/OFL.txt new file mode 100755 index 0000000..0dccedd --- /dev/null +++ b/ui/_/fonts/Inknut_Antiqua/OFL.txt @@ -0,0 +1,93 @@ +Copyright (c) 2014 Claus Eggers Sørensen (es@forthehearts.net) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/ui/_/icons/runner.svg b/ui/_/icons/runner.svg new file mode 100644 index 0000000..1e279a8 --- /dev/null +++ b/ui/_/icons/runner.svg @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ui/app/components/Home.js b/ui/app/components/Home.js index eac3ae6..1234f8c 100644 --- a/ui/app/components/Home.js +++ b/ui/app/components/Home.js @@ -1,20 +1,20 @@ import "./LocationList.js" +import "./Sidebar.js" +import "./MainPanel.js" class Home extends Shadow { render() { ZStack(() => { - p("BLOCKCATCHER") - .x(2, em).y(1, em) - .fontSize(1.2, em) - .fontFamily("Arial") - - LocationList() + HStack(() => { + Sidebar() + MainPanel() + }) }) - .backgroundColor("#aebdff") + .backgroundColor("rgb(146, 160, 219)") .display("block") .width(100, vw).height(100, vh) - .color("#60759f") + .color("#203051ff") } } -registerShadow(Home) \ No newline at end of file +register(Home) \ No newline at end of file diff --git a/ui/app/components/LocationList.js b/ui/app/components/LocationList.js index bb25460..6710430 100644 --- a/ui/app/components/LocationList.js +++ b/ui/app/components/LocationList.js @@ -14,13 +14,15 @@ class LocationList extends Shadow { .gap(1, em) }) }) + .gap(0.5, em) + .paddingLeft(5, em) + .paddingTop(2, em) .onEvent("update-location", (e) => { console.log("location received: ", e.detail) this.list.unshift(e.detail) this.rerender() }) - .x(2, em).y(5, em) } } -window.registerShadow(LocationList) \ No newline at end of file +window.register(LocationList) \ No newline at end of file diff --git a/ui/app/components/MainPanel.js b/ui/app/components/MainPanel.js new file mode 100644 index 0000000..62fe8a7 --- /dev/null +++ b/ui/app/components/MainPanel.js @@ -0,0 +1,25 @@ +class MainPanel extends Shadow { + render() { + ZStack(() => { + input("Search", "70vw") + .marginLeft(5, em) + .marginTop(2, em) + .paddingVertical(0.5, em) + .paddingHorizontal(1, em) + .border("none") + .background("rgb(146, 160, 219)") + .outline("none") + + switch(window.location.pathname) { + case "/": + LocationList() + break; + } + }) + .width(100, "%") + .height(100, vh) + .backgroundColor("rgb(174, 189, 255)") + } +} + +register(MainPanel) \ No newline at end of file diff --git a/ui/app/components/Sidebar.js b/ui/app/components/Sidebar.js new file mode 100644 index 0000000..2495cea --- /dev/null +++ b/ui/app/components/Sidebar.js @@ -0,0 +1,61 @@ +class Sidebar extends Shadow { + render() { + VStack(() => { + HStack(() => { + img("./_/icons/runner.svg", "40px", "40px") + p("Blockcatcher") + .fontSize(1.2, em) + .fontFamily("Inknut Antiqua") + .marginTop(0.5, em) + }) + .marginTop(2, em) + .paddingLeft(6, em) + .width(100, "%") + .borderBottom("2px solid rgb(100 115 177)") + + button("Walkers") + .background("rgb(100 115 177)") + .paddingVertical(0.5, em) + .border("none") + .borderRadius(6, px) + .marginTop(3, em) + .width(80, "%") + + button("Voters") + .background("transparent") + .paddingVertical(0.5, em) + .border("none") + .borderRadius(6, px) + .marginTop(1, em) + .width(80, "%") + .onHover(function (hovering) { + if(hovering) { + this.style.outline = "1px solid rgb(100 115 177)" + } else { + this.style.outline = "" + } + }) + + button("Data") + .background("transparent") + .paddingVertical(0.5, em) + .border("none") + .borderRadius(6, px) + .marginTop(1, em) + .width(80, "%") + .onHover(function (hovering) { + if(hovering) { + this.style.outline = "1px solid rgb(100 115 177)" + } else { + this.style.outline = "" + } + }) + }) + .width(20, vw) + .height(100, vh) + .borderRight("2px solid rgb(100 115 177)") + .alignItems("center") + } +} + +register(Sidebar) \ No newline at end of file diff --git a/ui/app/index.html b/ui/app/index.html index f7b5f1d..6177617 100644 --- a/ui/app/index.html +++ b/ui/app/index.html @@ -4,6 +4,7 @@ Blockcatcher +