diff --git a/.gitignore b/.gitignore index 89d431a..095eb04 100644 --- a/.gitignore +++ b/.gitignore @@ -1,19 +1 @@ -# ---> Erlang -.eunit -*.o -*.beam -*.plt -erl_crash.dump -.concrete/DEV_MODE - -# rebar 2.x -.rebar -rel/example_project -ebin/*.beam -deps - -# rebar 3 -.rebar3 -_build/ -_checkouts/ - +localbuild.sh diff --git a/Dockerfile b/Dockerfile index 73c380f..a72311e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,23 +1,23 @@ -ARG GLEAM_VERSION=v1.12.0 +ARG GLEAM_VERSION=v1.15.0 # Build stage - compile the application FROM ghcr.io/gleam-lang/gleam:${GLEAM_VERSION}-erlang-alpine AS builder # Add project code -COPY ./priv /quizterm/priv -COPY ./src /quizterm/src -COPY ./gleam.toml /quizterm/ +COPY ./server/priv /quizterm/server/priv +COPY ./server/src /quizterm/server/src +COPY ./server/gleam.toml /quizterm/server/ -RUN cd /quizterm && gleam deps download +RUN cd /quizterm/server && gleam deps download # Compile the server code -RUN cd /quizterm \ +RUN cd /quizterm/server \ && gleam export erlang-shipment # Runtime stage - slim image with only what's needed to run FROM ghcr.io/gleam-lang/gleam:${GLEAM_VERSION}-erlang-alpine # Copy the compiled server code from the builder stage -COPY --from=builder /quizterm/build/erlang-shipment /app +COPY --from=builder /quizterm/server/build/erlang-shipment /app # Set up the entrypoint WORKDIR /app diff --git a/README.md b/README.md index 74b3c3c..4b8fe41 100644 --- a/README.md +++ b/README.md @@ -8,17 +8,39 @@ was, will show up on everyones screen. Not quite finished yet, it is at a point where it is "usable" enough. -There are two endpoints to use: -| / |endpoint for "regular" players. -| /control | endpoint for person controlling the quiz. Same interface as for regular players, but with possiblity to control when to reveal answers and when to move on to next question. This gives the possiblity for the person asking the question to also provide answers, but the controls will work even if there is no player joined from this page. +Endpoints explained -![Screenshot of the game](screenshot.png) +| Endpoint | Usage | +|--------------------------|--------------------------------------------------------------| +| /room/ | Create room with given room_id (max 200 rooms) | +| /board/ | Join a game with the given room_id | +| /board//control | Join a game with the given room_id with more control options | -Next steps are: -- Display questions. Currently, Quizterm only asks user to provide answer, the actual question needs to be asked -elsewhere. This is often not a problem, since questions are asked on site, or in streamed meetings. -- Make it a little harder to join a quiz. So far, a quiz is open, and anyone that knows the URL can easily join. -A good idea to deploy to a disposable URL. -- Bad handling of players with the same name. If a player register with a name that is already in used, two players -will "compete" about being this player. You need to make sure to register with a different name than those already in -use. As all "in use" names are displayed on your screen, this is somewhat doable. + +| Ingame example | Idle player | +|--------------------------|--------------------------| +| ![Screenshot](game1.png) | ![Screenshot](game2.png) | + +### Building and running + +Docker, or a compatible container manager, like podman, is required to build and run +quizterm. The alternative is to install Gleam and Erlang/BEAM and run it dockerless. +Unless you plan to do Gleam development, using Docker will save a lot of hassle. + +To compile project and build docker image, write: +``` +docker build . -t quizterm:1 +``` +quizterm can be whatever name you want to give the container, 1 can be +changed to whatever you want the version of the container to be. + +Start server on port 4321: +``` +docker run -p 4321:1234 quizterm:1 +``` + +Port 1234 is the port used internally in the docker container, while 4321 +is the port exposed outside the container. The latter can be set to whatever +port you want to use. + +Open web browser and access http://localhost:4321 diff --git a/api-test/api-test.sh b/api-test/api-test.sh new file mode 100644 index 0000000..3f1cb68 --- /dev/null +++ b/api-test/api-test.sh @@ -0,0 +1,5 @@ +cat test.json | curl --json @- http://localhost:1234/api/info + +echo "" +echo "" + diff --git a/api-test/docker-down.sh b/api-test/docker-down.sh new file mode 100644 index 0000000..2fb0cd5 --- /dev/null +++ b/api-test/docker-down.sh @@ -0,0 +1,2 @@ +docker stop do_integration_test +docker rm do_integration_test diff --git a/api-test/docker-up.sh b/api-test/docker-up.sh new file mode 100644 index 0000000..3728472 --- /dev/null +++ b/api-test/docker-up.sh @@ -0,0 +1,4 @@ +docker build . -t quizterm:1 +docker run --name do_integration_test -d -p 4321:1234 quizterm:1 + +sleep 2 diff --git a/api-test/run.sh b/api-test/run.sh new file mode 100644 index 0000000..a22777e --- /dev/null +++ b/api-test/run.sh @@ -0,0 +1,3 @@ +sh docker-up +sh api-test +sh docker-down diff --git a/api-test/test.json b/api-test/test.json new file mode 100644 index 0000000..8e13b56 --- /dev/null +++ b/api-test/test.json @@ -0,0 +1,8 @@ +{ + "answers": + [ + {"question" : 14, "answer": "what is the meaning of the question that is number what is the meaning of the question that is numberwhat is the meaning of the question that is numberwhat is the meaning of the question that is number"} + ] +} + + diff --git a/client/dist/client.js b/client/dist/client.js new file mode 100644 index 0000000..18c3006 --- /dev/null +++ b/client/dist/client.js @@ -0,0 +1,6344 @@ +// build/dev/javascript/prelude.mjs +class CustomType { + withFields(fields) { + let properties = Object.keys(this).map((label) => (label in fields) ? fields[label] : this[label]); + return new this.constructor(...properties); + } +} + +class List { + static fromArray(array, tail) { + let t = tail || new Empty; + for (let i = array.length - 1;i >= 0; --i) { + t = new NonEmpty(array[i], t); + } + return t; + } + [Symbol.iterator]() { + return new ListIterator(this); + } + toArray() { + return [...this]; + } + atLeastLength(desired) { + let current = this; + while (desired-- > 0 && current) + current = current.tail; + return current !== undefined; + } + hasLength(desired) { + let current = this; + while (desired-- > 0 && current) + current = current.tail; + return desired === -1 && current instanceof Empty; + } + countLength() { + let current = this; + let length = 0; + while (current) { + current = current.tail; + length++; + } + return length - 1; + } +} +function prepend(element, tail) { + return new NonEmpty(element, tail); +} +function toList(elements, tail) { + return List.fromArray(elements, tail); +} + +class ListIterator { + #current; + constructor(current) { + this.#current = current; + } + next() { + if (this.#current instanceof Empty) { + return { done: true }; + } else { + let { head, tail } = this.#current; + this.#current = tail; + return { value: head, done: false }; + } + } +} + +class Empty extends List { +} +var List$Empty = () => new Empty; +var List$isEmpty = (value) => value instanceof Empty; + +class NonEmpty extends List { + constructor(head, tail) { + super(); + this.head = head; + this.tail = tail; + } +} +var List$NonEmpty = (head, tail) => new NonEmpty(head, tail); +var List$isNonEmpty = (value) => value instanceof NonEmpty; +var List$NonEmpty$first = (value) => value.head; +var List$NonEmpty$rest = (value) => value.tail; + +class BitArray { + bitSize; + byteSize; + bitOffset; + rawBuffer; + constructor(buffer, bitSize, bitOffset) { + if (!(buffer instanceof Uint8Array)) { + throw globalThis.Error("BitArray can only be constructed from a Uint8Array"); + } + this.bitSize = bitSize ?? buffer.length * 8; + this.byteSize = Math.trunc((this.bitSize + 7) / 8); + this.bitOffset = bitOffset ?? 0; + if (this.bitSize < 0) { + throw globalThis.Error(`BitArray bit size is invalid: ${this.bitSize}`); + } + if (this.bitOffset < 0 || this.bitOffset > 7) { + throw globalThis.Error(`BitArray bit offset is invalid: ${this.bitOffset}`); + } + if (buffer.length !== Math.trunc((this.bitOffset + this.bitSize + 7) / 8)) { + throw globalThis.Error("BitArray buffer length is invalid"); + } + this.rawBuffer = buffer; + } + byteAt(index) { + if (index < 0 || index >= this.byteSize) { + return; + } + return bitArrayByteAt(this.rawBuffer, this.bitOffset, index); + } + equals(other) { + if (this.bitSize !== other.bitSize) { + return false; + } + const wholeByteCount = Math.trunc(this.bitSize / 8); + if (this.bitOffset === 0 && other.bitOffset === 0) { + for (let i = 0;i < wholeByteCount; i++) { + if (this.rawBuffer[i] !== other.rawBuffer[i]) { + return false; + } + } + const trailingBitsCount = this.bitSize % 8; + if (trailingBitsCount) { + const unusedLowBitCount = 8 - trailingBitsCount; + if (this.rawBuffer[wholeByteCount] >> unusedLowBitCount !== other.rawBuffer[wholeByteCount] >> unusedLowBitCount) { + return false; + } + } + } else { + for (let i = 0;i < wholeByteCount; i++) { + const a = bitArrayByteAt(this.rawBuffer, this.bitOffset, i); + const b = bitArrayByteAt(other.rawBuffer, other.bitOffset, i); + if (a !== b) { + return false; + } + } + const trailingBitsCount = this.bitSize % 8; + if (trailingBitsCount) { + const a = bitArrayByteAt(this.rawBuffer, this.bitOffset, wholeByteCount); + const b = bitArrayByteAt(other.rawBuffer, other.bitOffset, wholeByteCount); + const unusedLowBitCount = 8 - trailingBitsCount; + if (a >> unusedLowBitCount !== b >> unusedLowBitCount) { + return false; + } + } + } + return true; + } + get buffer() { + if (this.bitOffset !== 0 || this.bitSize % 8 !== 0) { + throw new globalThis.Error("BitArray.buffer does not support unaligned bit arrays"); + } + return this.rawBuffer; + } + get length() { + if (this.bitOffset !== 0 || this.bitSize % 8 !== 0) { + throw new globalThis.Error("BitArray.length does not support unaligned bit arrays"); + } + return this.rawBuffer.length; + } +} +function bitArrayByteAt(buffer, bitOffset, index) { + if (bitOffset === 0) { + return buffer[index] ?? 0; + } else { + const a = buffer[index] << bitOffset & 255; + const b = buffer[index + 1] >> 8 - bitOffset; + return a | b; + } +} + +class UtfCodepoint { + constructor(value) { + this.value = value; + } +} +class Result extends CustomType { + static isResult(data2) { + return data2 instanceof Result; + } +} + +class Ok extends Result { + constructor(value) { + super(); + this[0] = value; + } + isOk() { + return true; + } +} +var Result$Ok = (value) => new Ok(value); +var Result$isOk = (value) => value instanceof Ok; +var Result$Ok$0 = (value) => value[0]; + +class Error extends Result { + constructor(detail) { + super(); + this[0] = detail; + } + isOk() { + return false; + } +} +var Result$Error = (detail) => new Error(detail); +var Result$isError = (value) => value instanceof Error; +function isEqual(x, y) { + let values = [x, y]; + while (values.length) { + let a = values.pop(); + let b = values.pop(); + if (a === b) + continue; + if (!isObject(a) || !isObject(b)) + return false; + let unequal = !structurallyCompatibleObjects(a, b) || unequalDates(a, b) || unequalBuffers(a, b) || unequalArrays(a, b) || unequalMaps(a, b) || unequalSets(a, b) || unequalRegExps(a, b); + if (unequal) + return false; + const proto = Object.getPrototypeOf(a); + if (proto !== null && typeof proto.equals === "function") { + try { + if (a.equals(b)) + continue; + else + return false; + } catch {} + } + let [keys, get] = getters(a); + const ka = keys(a); + const kb = keys(b); + if (ka.length !== kb.length) + return false; + for (let k of ka) { + values.push(get(a, k), get(b, k)); + } + } + return true; +} +function getters(object) { + if (object instanceof Map) { + return [(x) => x.keys(), (x, y) => x.get(y)]; + } else { + let extra = object instanceof globalThis.Error ? ["message"] : []; + return [(x) => [...extra, ...Object.keys(x)], (x, y) => x[y]]; + } +} +function unequalDates(a, b) { + return a instanceof Date && (a > b || a < b); +} +function unequalBuffers(a, b) { + return !(a instanceof BitArray) && a.buffer instanceof ArrayBuffer && a.BYTES_PER_ELEMENT && !(a.byteLength === b.byteLength && a.every((n, i) => n === b[i])); +} +function unequalArrays(a, b) { + return Array.isArray(a) && a.length !== b.length; +} +function unequalMaps(a, b) { + return a instanceof Map && a.size !== b.size; +} +function unequalSets(a, b) { + return a instanceof Set && (a.size != b.size || [...a].some((e) => !b.has(e))); +} +function unequalRegExps(a, b) { + return a instanceof RegExp && (a.source !== b.source || a.flags !== b.flags); +} +function isObject(a) { + return typeof a === "object" && a !== null; +} +function structurallyCompatibleObjects(a, b) { + if (typeof a !== "object" && typeof b !== "object" && (!a || !b)) + return false; + let nonstructural = [Promise, WeakSet, WeakMap, Function]; + if (nonstructural.some((c) => a instanceof c)) + return false; + return a.constructor === b.constructor; +} +function makeError(variant, file, module, line, fn, message, extra) { + let error = new globalThis.Error(message); + error.gleam_error = variant; + error.file = file; + error.module = module; + error.line = line; + error.function = fn; + error.fn = fn; + for (let k in extra) + error[k] = extra[k]; + return error; +} +// build/dev/javascript/gleam_stdlib/dict.mjs +var referenceMap = /* @__PURE__ */ new WeakMap; +var tempDataView = /* @__PURE__ */ new DataView(/* @__PURE__ */ new ArrayBuffer(8)); +var referenceUID = 0; +function hashByReference(o) { + const known = referenceMap.get(o); + if (known !== undefined) { + return known; + } + const hash = referenceUID++; + if (referenceUID === 2147483647) { + referenceUID = 0; + } + referenceMap.set(o, hash); + return hash; +} +function hashMerge(a, b) { + return a ^ b + 2654435769 + (a << 6) + (a >> 2) | 0; +} +function hashString(s) { + let hash = 0; + const len = s.length; + for (let i = 0;i < len; i++) { + hash = Math.imul(31, hash) + s.charCodeAt(i) | 0; + } + return hash; +} +function hashNumber(n) { + tempDataView.setFloat64(0, n); + const i = tempDataView.getInt32(0); + const j = tempDataView.getInt32(4); + return Math.imul(73244475, i >> 16 ^ i) ^ j; +} +function hashBigInt(n) { + return hashString(n.toString()); +} +function hashObject(o) { + const proto = Object.getPrototypeOf(o); + if (proto !== null && typeof proto.hashCode === "function") { + try { + const code = o.hashCode(o); + if (typeof code === "number") { + return code; + } + } catch {} + } + if (o instanceof Promise || o instanceof WeakSet || o instanceof WeakMap) { + return hashByReference(o); + } + if (o instanceof Date) { + return hashNumber(o.getTime()); + } + let h = 0; + if (o instanceof ArrayBuffer) { + o = new Uint8Array(o); + } + if (Array.isArray(o) || o instanceof Uint8Array) { + for (let i = 0;i < o.length; i++) { + h = Math.imul(31, h) + getHash(o[i]) | 0; + } + } else if (o instanceof Set) { + o.forEach((v) => { + h = h + getHash(v) | 0; + }); + } else if (o instanceof Map) { + o.forEach((v, k) => { + h = h + hashMerge(getHash(v), getHash(k)) | 0; + }); + } else { + const keys = Object.keys(o); + for (let i = 0;i < keys.length; i++) { + const k = keys[i]; + const v = o[k]; + h = h + hashMerge(getHash(v), hashString(k)) | 0; + } + } + return h; +} +function getHash(u) { + if (u === null) + return 1108378658; + if (u === undefined) + return 1108378659; + if (u === true) + return 1108378657; + if (u === false) + return 1108378656; + switch (typeof u) { + case "number": + return hashNumber(u); + case "string": + return hashString(u); + case "bigint": + return hashBigInt(u); + case "object": + return hashObject(u); + case "symbol": + return hashByReference(u); + case "function": + return hashByReference(u); + default: + return 0; + } +} + +class Dict { + constructor(size, root) { + this.size = size; + this.root = root; + } +} +var bits = 5; +var mask = (1 << bits) - 1; +var noElementMarker = Symbol(); +var generationKey = Symbol(); +var emptyNode = /* @__PURE__ */ newNode(0); +var emptyDict = /* @__PURE__ */ new Dict(0, emptyNode); +var errorNil = /* @__PURE__ */ Result$Error(undefined); +function makeNode(generation, datamap, nodemap, data2) { + return { + datamap, + nodemap, + data: data2, + [generationKey]: generation + }; +} +function newNode(generation) { + return makeNode(generation, 0, 0, []); +} +function copyNode(node, generation) { + if (node[generationKey] === generation) { + return node; + } + const newData = node.data.slice(0); + return makeNode(generation, node.datamap, node.nodemap, newData); +} +function copyAndSet(node, generation, idx, val) { + if (node.data[idx] === val) { + return node; + } + node = copyNode(node, generation); + node.data[idx] = val; + return node; +} +function copyAndInsertPair(node, generation, bit, idx, key, val) { + const data2 = node.data; + const length = data2.length; + const newData = new Array(length + 2); + let readIndex = 0; + let writeIndex = 0; + while (readIndex < idx) + newData[writeIndex++] = data2[readIndex++]; + newData[writeIndex++] = key; + newData[writeIndex++] = val; + while (readIndex < length) + newData[writeIndex++] = data2[readIndex++]; + return makeNode(generation, node.datamap | bit, node.nodemap, newData); +} +function make() { + return emptyDict; +} +function get(dict, key) { + const result = lookup(dict.root, key, getHash(key)); + return result !== noElementMarker ? Result$Ok(result) : errorNil; +} +function lookup(node, key, hash) { + for (let shift = 0;shift < 32; shift += bits) { + const data2 = node.data; + const bit = hashbit(hash, shift); + if (node.nodemap & bit) { + node = data2[data2.length - 1 - index(node.nodemap, bit)]; + } else if (node.datamap & bit) { + const dataidx = Math.imul(index(node.datamap, bit), 2); + return isEqual(key, data2[dataidx]) ? data2[dataidx + 1] : noElementMarker; + } else { + return noElementMarker; + } + } + const overflow = node.data; + for (let i = 0;i < overflow.length; i += 2) { + if (isEqual(key, overflow[i])) { + return overflow[i + 1]; + } + } + return noElementMarker; +} +function toTransient(dict) { + return { + generation: nextGeneration(dict), + root: dict.root, + size: dict.size, + dict + }; +} +function nextGeneration(dict) { + const root = dict.root; + if (root[generationKey] < Number.MAX_SAFE_INTEGER) { + return root[generationKey] + 1; + } + const queue = [root]; + while (queue.length) { + const node = queue.pop(); + node[generationKey] = 0; + const nodeStart = data.length - popcount(node.nodemap); + for (let i = nodeStart;i < node.data.length; ++i) { + queue.push(node.data[i]); + } + } + return 1; +} +var globalTransient = /* @__PURE__ */ toTransient(emptyDict); +function insert(dict, key, value) { + globalTransient.generation = nextGeneration(dict); + globalTransient.size = dict.size; + const hash = getHash(key); + const root = insertIntoNode(globalTransient, dict.root, key, value, hash, 0); + if (root === dict.root) { + return dict; + } + return new Dict(globalTransient.size, root); +} +function insertIntoNode(transient, node, key, value, hash, shift) { + const data2 = node.data; + const generation = transient.generation; + if (shift > 32) { + for (let i = 0;i < data2.length; i += 2) { + if (isEqual(key, data2[i])) { + return copyAndSet(node, generation, i + 1, value); + } + } + transient.size += 1; + return copyAndInsertPair(node, generation, 0, data2.length, key, value); + } + const bit = hashbit(hash, shift); + if (node.nodemap & bit) { + const nodeidx2 = data2.length - 1 - index(node.nodemap, bit); + let child2 = data2[nodeidx2]; + child2 = insertIntoNode(transient, child2, key, value, hash, shift + bits); + return copyAndSet(node, generation, nodeidx2, child2); + } + const dataidx = Math.imul(index(node.datamap, bit), 2); + if ((node.datamap & bit) === 0) { + transient.size += 1; + return copyAndInsertPair(node, generation, bit, dataidx, key, value); + } + if (isEqual(key, data2[dataidx])) { + return copyAndSet(node, generation, dataidx + 1, value); + } + const childShift = shift + bits; + let child = emptyNode; + child = insertIntoNode(transient, child, key, value, hash, childShift); + const key2 = data2[dataidx]; + const value2 = data2[dataidx + 1]; + const hash2 = getHash(key2); + child = insertIntoNode(transient, child, key2, value2, hash2, childShift); + transient.size -= 1; + const length = data2.length; + const nodeidx = length - 1 - index(node.nodemap, bit); + const newData = new Array(length - 1); + let readIndex = 0; + let writeIndex = 0; + while (readIndex < dataidx) + newData[writeIndex++] = data2[readIndex++]; + readIndex += 2; + while (readIndex <= nodeidx) + newData[writeIndex++] = data2[readIndex++]; + newData[writeIndex++] = child; + while (readIndex < length) + newData[writeIndex++] = data2[readIndex++]; + return makeNode(generation, node.datamap ^ bit, node.nodemap | bit, newData); +} +function fold(dict, state, fun) { + const queue = [dict.root]; + while (queue.length) { + const node = queue.pop(); + const data2 = node.data; + const edgesStart = data2.length - popcount(node.nodemap); + for (let i = 0;i < edgesStart; i += 2) { + state = fun(state, data2[i], data2[i + 1]); + } + for (let i = edgesStart;i < data2.length; ++i) { + queue.push(data2[i]); + } + } + return state; +} +function popcount(n) { + n -= n >>> 1 & 1431655765; + n = (n & 858993459) + (n >>> 2 & 858993459); + return Math.imul(n + (n >>> 4) & 252645135, 16843009) >>> 24; +} +function index(bitmap, bit) { + return popcount(bitmap & bit - 1); +} +function hashbit(hash, shift) { + return 1 << (hash >>> shift & mask); +} + +// build/dev/javascript/gleam_stdlib/gleam/option.mjs +class Some extends CustomType { + constructor($0) { + super(); + this[0] = $0; + } +} +var Option$isSome = (value) => value instanceof Some; +var Option$Some$0 = (value) => value[0]; + +class None extends CustomType { +} +function to_result(option, e) { + if (option instanceof Some) { + let a = option[0]; + return new Ok(a); + } else { + return new Error(e); + } +} +function unwrap(option, default$) { + if (option instanceof Some) { + let x = option[0]; + return x; + } else { + return default$; + } +} + +// build/dev/javascript/gleam_stdlib/gleam/dict.mjs +function keys(dict) { + return fold(dict, toList([]), (acc, key, _) => { + return prepend(key, acc); + }); +} + +// build/dev/javascript/gleam_stdlib/gleam/order.mjs +class Lt extends CustomType { +} +var Order$Lt = () => new Lt; +class Eq extends CustomType { +} +var Order$Eq = () => new Eq; +class Gt extends CustomType { +} +var Order$Gt = () => new Gt; + +// build/dev/javascript/gleam_stdlib/gleam/string.mjs +function concat_loop(loop$strings, loop$accumulator) { + while (true) { + let strings = loop$strings; + let accumulator = loop$accumulator; + if (strings instanceof Empty) { + return accumulator; + } else { + let string = strings.head; + let strings$1 = strings.tail; + loop$strings = strings$1; + loop$accumulator = accumulator + string; + } + } +} +function concat2(strings) { + return concat_loop(strings, ""); +} +function split2(x, substring) { + if (substring === "") { + return graphemes(x); + } else { + let _pipe = x; + let _pipe$1 = identity(_pipe); + let _pipe$2 = split(_pipe$1, substring); + return map2(_pipe$2, identity); + } +} + +// build/dev/javascript/gleam_stdlib/gleam/dynamic/decode.mjs +class DecodeError extends CustomType { + constructor(expected, found, path) { + super(); + this.expected = expected; + this.found = found; + this.path = path; + } +} +class Decoder extends CustomType { + constructor(function$) { + super(); + this.function = function$; + } +} +var int2 = /* @__PURE__ */ new Decoder(decode_int); +var float2 = /* @__PURE__ */ new Decoder(decode_float); +var string2 = /* @__PURE__ */ new Decoder(decode_string); +function run(data2, decoder) { + let $ = decoder.function(data2); + let maybe_invalid_data; + let errors; + maybe_invalid_data = $[0]; + errors = $[1]; + if (errors instanceof Empty) { + return new Ok(maybe_invalid_data); + } else { + return new Error(errors); + } +} +function success(data2) { + return new Decoder((_) => { + return [data2, toList([])]; + }); +} +function map3(decoder, transformer) { + return new Decoder((d) => { + let $ = decoder.function(d); + let data2; + let errors; + data2 = $[0]; + errors = $[1]; + return [transformer(data2), errors]; + }); +} +function run_decoders(loop$data, loop$failure, loop$decoders) { + while (true) { + let data2 = loop$data; + let failure = loop$failure; + let decoders = loop$decoders; + if (decoders instanceof Empty) { + return failure; + } else { + let decoder = decoders.head; + let decoders$1 = decoders.tail; + let $ = decoder.function(data2); + let layer; + let errors; + layer = $; + errors = $[1]; + if (errors instanceof Empty) { + return layer; + } else { + loop$data = data2; + loop$failure = failure; + loop$decoders = decoders$1; + } + } + } +} +function one_of(first, alternatives) { + return new Decoder((dynamic_data) => { + let $ = first.function(dynamic_data); + let layer; + let errors; + layer = $; + errors = $[1]; + if (errors instanceof Empty) { + return layer; + } else { + return run_decoders(dynamic_data, layer, alternatives); + } + }); +} +function run_dynamic_function(data2, name, f) { + let $ = f(data2); + if ($ instanceof Ok) { + let data$1 = $[0]; + return [data$1, toList([])]; + } else { + let placeholder = $[0]; + return [ + placeholder, + toList([new DecodeError(name, classify_dynamic(data2), toList([]))]) + ]; + } +} +function decode_int(data2) { + return run_dynamic_function(data2, "Int", int); +} +function decode_float(data2) { + return run_dynamic_function(data2, "Float", float); +} +function decode_string(data2) { + return run_dynamic_function(data2, "String", string); +} +function path_segment_to_string(key) { + let decoder = one_of(string2, toList([ + (() => { + let _pipe = int2; + return map3(_pipe, to_string); + })(), + (() => { + let _pipe = float2; + return map3(_pipe, float_to_string); + })() + ])); + let $ = run(key, decoder); + if ($ instanceof Ok) { + let key$1 = $[0]; + return key$1; + } else { + return "<" + classify_dynamic(key) + ">"; + } +} +function push_path(layer, path) { + let path$1 = map2(path, (key) => { + let _pipe = key; + let _pipe$1 = identity(_pipe); + return path_segment_to_string(_pipe$1); + }); + let errors = map2(layer[1], (error) => { + return new DecodeError(error.expected, error.found, append2(path$1, error.path)); + }); + return [layer[0], errors]; +} +function index3(loop$path, loop$position, loop$inner, loop$data, loop$handle_miss) { + while (true) { + let path = loop$path; + let position = loop$position; + let inner = loop$inner; + let data2 = loop$data; + let handle_miss = loop$handle_miss; + if (path instanceof Empty) { + let _pipe = data2; + let _pipe$1 = inner(_pipe); + return push_path(_pipe$1, reverse(position)); + } else { + let key = path.head; + let path$1 = path.tail; + let $ = index2(data2, key); + if ($ instanceof Ok) { + let $1 = $[0]; + if ($1 instanceof Some) { + let data$1 = $1[0]; + loop$path = path$1; + loop$position = prepend(key, position); + loop$inner = inner; + loop$data = data$1; + loop$handle_miss = handle_miss; + } else { + return handle_miss(data2, prepend(key, position)); + } + } else { + let kind = $[0]; + let $1 = inner(data2); + let default$; + default$ = $1[0]; + let _pipe = [ + default$, + toList([new DecodeError(kind, classify_dynamic(data2), toList([]))]) + ]; + return push_path(_pipe, reverse(position)); + } + } + } +} +function subfield(field_path, field_decoder, next) { + return new Decoder((data2) => { + let $ = index3(field_path, toList([]), field_decoder.function, data2, (data3, position) => { + let $12 = field_decoder.function(data3); + let default$; + default$ = $12[0]; + let _pipe = [ + default$, + toList([new DecodeError("Field", "Nothing", toList([]))]) + ]; + return push_path(_pipe, reverse(position)); + }); + let out; + let errors1; + out = $[0]; + errors1 = $[1]; + let $1 = next(out).function(data2); + let out$1; + let errors2; + out$1 = $1[0]; + errors2 = $1[1]; + return [out$1, append2(errors1, errors2)]; + }); +} + +// build/dev/javascript/gleam_stdlib/gleam_stdlib.mjs +var Nil = undefined; +function identity(x) { + return x; +} +function parse_int(value) { + if (/^[-+]?(\d+)$/.test(value)) { + return Result$Ok(parseInt(value)); + } else { + return Result$Error(Nil); + } +} +function to_string(term) { + return term.toString(); +} +function graphemes(string3) { + const iterator = graphemes_iterator(string3); + if (iterator) { + return arrayToList(Array.from(iterator).map((item) => item.segment)); + } else { + return arrayToList(string3.match(/./gsu)); + } +} +var segmenter = undefined; +function graphemes_iterator(string3) { + if (globalThis.Intl && Intl.Segmenter) { + segmenter ||= new Intl.Segmenter; + return segmenter.segment(string3)[Symbol.iterator](); + } +} +function pop_codeunit(str) { + return [str.charCodeAt(0) | 0, str.slice(1)]; +} +function lowercase(string3) { + return string3.toLowerCase(); +} +function split(xs, pattern) { + return arrayToList(xs.split(pattern)); +} +function string_codeunit_slice(str, from2, length2) { + return str.slice(from2, from2 + length2); +} +function starts_with(haystack, needle) { + return haystack.startsWith(needle); +} +var unicode_whitespaces = [ + " ", + "\t", + ` +`, + "\v", + "\f", + "\r", + "…", + "\u2028", + "\u2029" +].join(""); +var trim_start_regex = /* @__PURE__ */ new RegExp(`^[${unicode_whitespaces}]*`); +var trim_end_regex = /* @__PURE__ */ new RegExp(`[${unicode_whitespaces}]*$`); +function classify_dynamic(data2) { + if (typeof data2 === "string") { + return "String"; + } else if (typeof data2 === "boolean") { + return "Bool"; + } else if (isResult(data2)) { + return "Result"; + } else if (isList(data2)) { + return "List"; + } else if (data2 instanceof BitArray) { + return "BitArray"; + } else if (data2 instanceof Dict) { + return "Dict"; + } else if (Number.isInteger(data2)) { + return "Int"; + } else if (Array.isArray(data2)) { + return `Array`; + } else if (typeof data2 === "number") { + return "Float"; + } else if (data2 === null) { + return "Nil"; + } else if (data2 === undefined) { + return "Nil"; + } else { + const type = typeof data2; + return type.charAt(0).toUpperCase() + type.slice(1); + } +} +var MIN_I32 = -(2 ** 31); +var MAX_I32 = 2 ** 31 - 1; +var U32 = 2 ** 32; +var MAX_SAFE = Number.MAX_SAFE_INTEGER; +var MIN_SAFE = Number.MIN_SAFE_INTEGER; +function float_to_string(float3) { + const string3 = float3.toString().replace("+", ""); + if (string3.indexOf(".") >= 0) { + return string3; + } else { + const index4 = string3.indexOf("e"); + if (index4 >= 0) { + return string3.slice(0, index4) + ".0" + string3.slice(index4); + } else { + return string3 + ".0"; + } + } +} + +class Inspector { + #references = new Set; + inspect(v) { + const t = typeof v; + if (v === true) + return "True"; + if (v === false) + return "False"; + if (v === null) + return "//js(null)"; + if (v === undefined) + return "Nil"; + if (t === "string") + return this.#string(v); + if (t === "bigint" || Number.isInteger(v)) + return v.toString(); + if (t === "number") + return float_to_string(v); + if (v instanceof UtfCodepoint) + return this.#utfCodepoint(v); + if (v instanceof BitArray) + return this.#bit_array(v); + if (v instanceof RegExp) + return `//js(${v})`; + if (v instanceof Date) + return `//js(Date("${v.toISOString()}"))`; + if (v instanceof globalThis.Error) + return `//js(${v.toString()})`; + if (v instanceof Function) { + const args = []; + for (const i of Array(v.length).keys()) + args.push(String.fromCharCode(i + 97)); + return `//fn(${args.join(", ")}) { ... }`; + } + if (this.#references.size === this.#references.add(v).size) { + return "//js(circular reference)"; + } + let printed; + if (Array.isArray(v)) { + printed = `#(${v.map((v2) => this.inspect(v2)).join(", ")})`; + } else if (isList(v)) { + printed = this.#list(v); + } else if (v instanceof CustomType) { + printed = this.#customType(v); + } else if (v instanceof Dict) { + printed = this.#dict(v); + } else if (v instanceof Set) { + return `//js(Set(${[...v].map((v2) => this.inspect(v2)).join(", ")}))`; + } else { + printed = this.#object(v); + } + this.#references.delete(v); + return printed; + } + #object(v) { + const name = Object.getPrototypeOf(v)?.constructor?.name || "Object"; + const props = []; + for (const k of Object.keys(v)) { + props.push(`${this.inspect(k)}: ${this.inspect(v[k])}`); + } + const body = props.length ? " " + props.join(", ") + " " : ""; + const head = name === "Object" ? "" : name + " "; + return `//js(${head}{${body}})`; + } + #dict(map4) { + let body = "dict.from_list(["; + let first = true; + body = fold(map4, body, (body2, key, value) => { + if (!first) + body2 = body2 + ", "; + first = false; + return body2 + "#(" + this.inspect(key) + ", " + this.inspect(value) + ")"; + }); + return body + "])"; + } + #customType(record) { + const props = Object.keys(record).map((label) => { + const value = this.inspect(record[label]); + return isNaN(parseInt(label)) ? `${label}: ${value}` : value; + }).join(", "); + return props ? `${record.constructor.name}(${props})` : record.constructor.name; + } + #list(list2) { + if (List$isEmpty(list2)) { + return "[]"; + } + let char_out = 'charlist.from_string("'; + let list_out = "["; + let current = list2; + while (List$isNonEmpty(current)) { + let element = current.head; + current = current.tail; + if (list_out !== "[") { + list_out += ", "; + } + list_out += this.inspect(element); + if (char_out) { + if (Number.isInteger(element) && element >= 32 && element <= 126) { + char_out += String.fromCharCode(element); + } else { + char_out = null; + } + } + } + if (char_out) { + return char_out + '")'; + } else { + return list_out + "]"; + } + } + #string(str) { + let new_str = '"'; + for (let i = 0;i < str.length; i++) { + const char = str[i]; + switch (char) { + case ` +`: + new_str += "\\n"; + break; + case "\r": + new_str += "\\r"; + break; + case "\t": + new_str += "\\t"; + break; + case "\f": + new_str += "\\f"; + break; + case "\\": + new_str += "\\\\"; + break; + case '"': + new_str += "\\\""; + break; + default: + if (char < " " || char > "~" && char < " ") { + new_str += "\\u{" + char.charCodeAt(0).toString(16).toUpperCase().padStart(4, "0") + "}"; + } else { + new_str += char; + } + } + } + new_str += '"'; + return new_str; + } + #utfCodepoint(codepoint2) { + return `//utfcodepoint(${String.fromCodePoint(codepoint2.value)})`; + } + #bit_array(bits2) { + if (bits2.bitSize === 0) { + return "<<>>"; + } + let acc = "<<"; + for (let i = 0;i < bits2.byteSize - 1; i++) { + acc += bits2.byteAt(i).toString(); + acc += ", "; + } + if (bits2.byteSize * 8 === bits2.bitSize) { + acc += bits2.byteAt(bits2.byteSize - 1).toString(); + } else { + const trailingBitsCount = bits2.bitSize % 8; + acc += bits2.byteAt(bits2.byteSize - 1) >> 8 - trailingBitsCount; + acc += `:size(${trailingBitsCount})`; + } + acc += ">>"; + return acc; + } +} +function index2(data2, key) { + if (data2 instanceof Dict) { + const result = get(data2, key); + return Result$Ok(result.isOk() ? new Some(result[0]) : new None); + } + if (data2 instanceof WeakMap || data2 instanceof Map) { + const token = {}; + const entry = data2.get(key, token); + if (entry === token) + return Result$Ok(new None); + return Result$Ok(new Some(entry)); + } + const key_is_int = Number.isInteger(key); + if (key_is_int && key >= 0 && key < 8 && isList(data2)) { + let i = 0; + for (const value of data2) { + if (i === key) + return Result$Ok(new Some(value)); + i++; + } + return Result$Error("Indexable"); + } + if (key_is_int && Array.isArray(data2) || data2 && typeof data2 === "object" || data2 && Object.getPrototypeOf(data2) === Object.prototype) { + if (key in data2) + return Result$Ok(new Some(data2[key])); + return Result$Ok(new None); + } + return Result$Error(key_is_int ? "Indexable" : "Dict"); +} +function float(data2) { + if (typeof data2 === "number") + return Result$Ok(data2); + return Result$Error(0); +} +function int(data2) { + if (Number.isInteger(data2)) + return Result$Ok(data2); + return Result$Error(0); +} +function string(data2) { + if (typeof data2 === "string") + return Result$Ok(data2); + return Result$Error(""); +} +function arrayToList(array) { + let list2 = List$Empty(); + let i = array.length; + while (i--) { + list2 = List$NonEmpty(array[i], list2); + } + return list2; +} +function isList(data2) { + return List$isEmpty(data2) || List$isNonEmpty(data2); +} +function isResult(data2) { + return Result$isOk(data2) || Result$isError(data2); +} + +// build/dev/javascript/gleam_stdlib/gleam/list.mjs +class Ascending extends CustomType { +} + +class Descending extends CustomType { +} +function reverse_and_prepend(loop$prefix, loop$suffix) { + while (true) { + let prefix = loop$prefix; + let suffix = loop$suffix; + if (prefix instanceof Empty) { + return suffix; + } else { + let first$1 = prefix.head; + let rest$1 = prefix.tail; + loop$prefix = rest$1; + loop$suffix = prepend(first$1, suffix); + } + } +} +function reverse(list2) { + return reverse_and_prepend(list2, toList([])); +} +function map_loop(loop$list, loop$fun, loop$acc) { + while (true) { + let list2 = loop$list; + let fun = loop$fun; + let acc = loop$acc; + if (list2 instanceof Empty) { + return reverse(acc); + } else { + let first$1 = list2.head; + let rest$1 = list2.tail; + loop$list = rest$1; + loop$fun = fun; + loop$acc = prepend(fun(first$1), acc); + } + } +} +function map2(list2, fun) { + return map_loop(list2, fun, toList([])); +} +function index_map_loop(loop$list, loop$fun, loop$index, loop$acc) { + while (true) { + let list2 = loop$list; + let fun = loop$fun; + let index4 = loop$index; + let acc = loop$acc; + if (list2 instanceof Empty) { + return reverse(acc); + } else { + let first$1 = list2.head; + let rest$1 = list2.tail; + let acc$1 = prepend(fun(first$1, index4), acc); + loop$list = rest$1; + loop$fun = fun; + loop$index = index4 + 1; + loop$acc = acc$1; + } + } +} +function index_map(list2, fun) { + return index_map_loop(list2, fun, 0, toList([])); +} +function append_loop(loop$first, loop$second) { + while (true) { + let first = loop$first; + let second = loop$second; + if (first instanceof Empty) { + return second; + } else { + let first$1 = first.head; + let rest$1 = first.tail; + loop$first = rest$1; + loop$second = prepend(first$1, second); + } + } +} +function append2(first, second) { + return append_loop(reverse(first), second); +} +function prepend2(list2, item) { + return prepend(item, list2); +} +function fold2(loop$list, loop$initial, loop$fun) { + while (true) { + let list2 = loop$list; + let initial = loop$initial; + let fun = loop$fun; + if (list2 instanceof Empty) { + return initial; + } else { + let first$1 = list2.head; + let rest$1 = list2.tail; + loop$list = rest$1; + loop$initial = fun(initial, first$1); + loop$fun = fun; + } + } +} +function sequences(loop$list, loop$compare, loop$growing, loop$direction, loop$prev, loop$acc) { + while (true) { + let list2 = loop$list; + let compare3 = loop$compare; + let growing = loop$growing; + let direction = loop$direction; + let prev = loop$prev; + let acc = loop$acc; + let growing$1 = prepend(prev, growing); + if (list2 instanceof Empty) { + if (direction instanceof Ascending) { + return prepend(reverse(growing$1), acc); + } else { + return prepend(growing$1, acc); + } + } else { + let new$1 = list2.head; + let rest$1 = list2.tail; + let $ = compare3(prev, new$1); + if (direction instanceof Ascending) { + if ($ instanceof Lt) { + loop$list = rest$1; + loop$compare = compare3; + loop$growing = growing$1; + loop$direction = direction; + loop$prev = new$1; + loop$acc = acc; + } else if ($ instanceof Eq) { + loop$list = rest$1; + loop$compare = compare3; + loop$growing = growing$1; + loop$direction = direction; + loop$prev = new$1; + loop$acc = acc; + } else { + let _block; + if (direction instanceof Ascending) { + _block = prepend(reverse(growing$1), acc); + } else { + _block = prepend(growing$1, acc); + } + let acc$1 = _block; + if (rest$1 instanceof Empty) { + return prepend(toList([new$1]), acc$1); + } else { + let next = rest$1.head; + let rest$2 = rest$1.tail; + let _block$1; + let $1 = compare3(new$1, next); + if ($1 instanceof Lt) { + _block$1 = new Ascending; + } else if ($1 instanceof Eq) { + _block$1 = new Ascending; + } else { + _block$1 = new Descending; + } + let direction$1 = _block$1; + loop$list = rest$2; + loop$compare = compare3; + loop$growing = toList([new$1]); + loop$direction = direction$1; + loop$prev = next; + loop$acc = acc$1; + } + } + } else if ($ instanceof Lt) { + let _block; + if (direction instanceof Ascending) { + _block = prepend(reverse(growing$1), acc); + } else { + _block = prepend(growing$1, acc); + } + let acc$1 = _block; + if (rest$1 instanceof Empty) { + return prepend(toList([new$1]), acc$1); + } else { + let next = rest$1.head; + let rest$2 = rest$1.tail; + let _block$1; + let $1 = compare3(new$1, next); + if ($1 instanceof Lt) { + _block$1 = new Ascending; + } else if ($1 instanceof Eq) { + _block$1 = new Ascending; + } else { + _block$1 = new Descending; + } + let direction$1 = _block$1; + loop$list = rest$2; + loop$compare = compare3; + loop$growing = toList([new$1]); + loop$direction = direction$1; + loop$prev = next; + loop$acc = acc$1; + } + } else if ($ instanceof Eq) { + let _block; + if (direction instanceof Ascending) { + _block = prepend(reverse(growing$1), acc); + } else { + _block = prepend(growing$1, acc); + } + let acc$1 = _block; + if (rest$1 instanceof Empty) { + return prepend(toList([new$1]), acc$1); + } else { + let next = rest$1.head; + let rest$2 = rest$1.tail; + let _block$1; + let $1 = compare3(new$1, next); + if ($1 instanceof Lt) { + _block$1 = new Ascending; + } else if ($1 instanceof Eq) { + _block$1 = new Ascending; + } else { + _block$1 = new Descending; + } + let direction$1 = _block$1; + loop$list = rest$2; + loop$compare = compare3; + loop$growing = toList([new$1]); + loop$direction = direction$1; + loop$prev = next; + loop$acc = acc$1; + } + } else { + loop$list = rest$1; + loop$compare = compare3; + loop$growing = growing$1; + loop$direction = direction; + loop$prev = new$1; + loop$acc = acc; + } + } + } +} +function merge_ascendings(loop$list1, loop$list2, loop$compare, loop$acc) { + while (true) { + let list1 = loop$list1; + let list2 = loop$list2; + let compare3 = loop$compare; + let acc = loop$acc; + if (list1 instanceof Empty) { + let list3 = list2; + return reverse_and_prepend(list3, acc); + } else if (list2 instanceof Empty) { + let list3 = list1; + return reverse_and_prepend(list3, acc); + } else { + let first1 = list1.head; + let rest1 = list1.tail; + let first2 = list2.head; + let rest2 = list2.tail; + let $ = compare3(first1, first2); + if ($ instanceof Lt) { + loop$list1 = rest1; + loop$list2 = list2; + loop$compare = compare3; + loop$acc = prepend(first1, acc); + } else if ($ instanceof Eq) { + loop$list1 = list1; + loop$list2 = rest2; + loop$compare = compare3; + loop$acc = prepend(first2, acc); + } else { + loop$list1 = list1; + loop$list2 = rest2; + loop$compare = compare3; + loop$acc = prepend(first2, acc); + } + } + } +} +function merge_ascending_pairs(loop$sequences, loop$compare, loop$acc) { + while (true) { + let sequences2 = loop$sequences; + let compare3 = loop$compare; + let acc = loop$acc; + if (sequences2 instanceof Empty) { + return reverse(acc); + } else { + let $ = sequences2.tail; + if ($ instanceof Empty) { + let sequence = sequences2.head; + return reverse(prepend(reverse(sequence), acc)); + } else { + let ascending1 = sequences2.head; + let ascending2 = $.head; + let rest$1 = $.tail; + let descending = merge_ascendings(ascending1, ascending2, compare3, toList([])); + loop$sequences = rest$1; + loop$compare = compare3; + loop$acc = prepend(descending, acc); + } + } + } +} +function merge_descendings(loop$list1, loop$list2, loop$compare, loop$acc) { + while (true) { + let list1 = loop$list1; + let list2 = loop$list2; + let compare3 = loop$compare; + let acc = loop$acc; + if (list1 instanceof Empty) { + let list3 = list2; + return reverse_and_prepend(list3, acc); + } else if (list2 instanceof Empty) { + let list3 = list1; + return reverse_and_prepend(list3, acc); + } else { + let first1 = list1.head; + let rest1 = list1.tail; + let first2 = list2.head; + let rest2 = list2.tail; + let $ = compare3(first1, first2); + if ($ instanceof Lt) { + loop$list1 = list1; + loop$list2 = rest2; + loop$compare = compare3; + loop$acc = prepend(first2, acc); + } else if ($ instanceof Eq) { + loop$list1 = rest1; + loop$list2 = list2; + loop$compare = compare3; + loop$acc = prepend(first1, acc); + } else { + loop$list1 = rest1; + loop$list2 = list2; + loop$compare = compare3; + loop$acc = prepend(first1, acc); + } + } + } +} +function merge_descending_pairs(loop$sequences, loop$compare, loop$acc) { + while (true) { + let sequences2 = loop$sequences; + let compare3 = loop$compare; + let acc = loop$acc; + if (sequences2 instanceof Empty) { + return reverse(acc); + } else { + let $ = sequences2.tail; + if ($ instanceof Empty) { + let sequence = sequences2.head; + return reverse(prepend(reverse(sequence), acc)); + } else { + let descending1 = sequences2.head; + let descending2 = $.head; + let rest$1 = $.tail; + let ascending = merge_descendings(descending1, descending2, compare3, toList([])); + loop$sequences = rest$1; + loop$compare = compare3; + loop$acc = prepend(ascending, acc); + } + } + } +} +function merge_all(loop$sequences, loop$direction, loop$compare) { + while (true) { + let sequences2 = loop$sequences; + let direction = loop$direction; + let compare3 = loop$compare; + if (sequences2 instanceof Empty) { + return sequences2; + } else if (direction instanceof Ascending) { + let $ = sequences2.tail; + if ($ instanceof Empty) { + let sequence = sequences2.head; + return sequence; + } else { + let sequences$1 = merge_ascending_pairs(sequences2, compare3, toList([])); + loop$sequences = sequences$1; + loop$direction = new Descending; + loop$compare = compare3; + } + } else { + let $ = sequences2.tail; + if ($ instanceof Empty) { + let sequence = sequences2.head; + return reverse(sequence); + } else { + let sequences$1 = merge_descending_pairs(sequences2, compare3, toList([])); + loop$sequences = sequences$1; + loop$direction = new Ascending; + loop$compare = compare3; + } + } + } +} +function sort(list2, compare3) { + if (list2 instanceof Empty) { + return list2; + } else { + let $ = list2.tail; + if ($ instanceof Empty) { + return list2; + } else { + let x = list2.head; + let y = $.head; + let rest$1 = $.tail; + let _block; + let $1 = compare3(x, y); + if ($1 instanceof Lt) { + _block = new Ascending; + } else if ($1 instanceof Eq) { + _block = new Ascending; + } else { + _block = new Descending; + } + let direction = _block; + let sequences$1 = sequences(rest$1, compare3, toList([x]), direction, y, toList([])); + return merge_all(sequences$1, new Ascending, compare3); + } + } +} +function key_set_loop(loop$list, loop$key, loop$value, loop$inspected) { + while (true) { + let list2 = loop$list; + let key = loop$key; + let value = loop$value; + let inspected = loop$inspected; + if (list2 instanceof Empty) { + return reverse(prepend([key, value], inspected)); + } else { + let k = list2.head[0]; + if (isEqual(k, key)) { + let rest$1 = list2.tail; + return reverse_and_prepend(inspected, prepend([k, value], rest$1)); + } else { + let first$1 = list2.head; + let rest$1 = list2.tail; + loop$list = rest$1; + loop$key = key; + loop$value = value; + loop$inspected = prepend(first$1, inspected); + } + } + } +} +function key_set(list2, key, value) { + return key_set_loop(list2, key, value, toList([])); +} +function each(loop$list, loop$f) { + while (true) { + let list2 = loop$list; + let f = loop$f; + if (list2 instanceof Empty) { + return; + } else { + let first$1 = list2.head; + let rest$1 = list2.tail; + f(first$1); + loop$list = rest$1; + loop$f = f; + } + } +} + +// build/dev/javascript/gleam_stdlib/gleam/result.mjs +function map_error(result, fun) { + if (result instanceof Ok) { + return result; + } else { + let error = result[0]; + return new Error(fun(error)); + } +} +function try$(result, fun) { + if (result instanceof Ok) { + let x = result[0]; + return fun(x); + } else { + return result; + } +} +function unwrap2(result, default$) { + if (result instanceof Ok) { + let v = result[0]; + return v; + } else { + return default$; + } +} +function replace_error(result, error) { + if (result instanceof Ok) { + return result; + } else { + return new Error(error); + } +} +// build/dev/javascript/gleam_stdlib/gleam/bool.mjs +function guard(requirement, consequence, alternative) { + if (requirement) { + return consequence; + } else { + return alternative(); + } +} + +// build/dev/javascript/gleam_http/gleam/http.mjs +class Get extends CustomType { +} +class Post extends CustomType { +} +class Head extends CustomType { +} +class Put extends CustomType { +} +class Delete extends CustomType { +} +class Trace extends CustomType { +} +class Connect extends CustomType { +} +class Options extends CustomType { +} +class Patch extends CustomType { +} +class Http extends CustomType { +} +class Https extends CustomType { +} +function method_to_string(method) { + if (method instanceof Get) { + return "GET"; + } else if (method instanceof Post) { + return "POST"; + } else if (method instanceof Head) { + return "HEAD"; + } else if (method instanceof Put) { + return "PUT"; + } else if (method instanceof Delete) { + return "DELETE"; + } else if (method instanceof Trace) { + return "TRACE"; + } else if (method instanceof Connect) { + return "CONNECT"; + } else if (method instanceof Options) { + return "OPTIONS"; + } else if (method instanceof Patch) { + return "PATCH"; + } else { + let method$1 = method[0]; + return method$1; + } +} +function scheme_to_string(scheme) { + if (scheme instanceof Http) { + return "http"; + } else { + return "https"; + } +} +function scheme_from_string(scheme) { + let $ = lowercase(scheme); + if ($ === "http") { + return new Ok(new Http); + } else if ($ === "https") { + return new Ok(new Https); + } else { + return new Error(undefined); + } +} + +// build/dev/javascript/gleam_http/gleam/http/response.mjs +class Response extends CustomType { + constructor(status, headers, body) { + super(); + this.status = status; + this.headers = headers; + this.body = body; + } +} +// build/dev/javascript/gleam_stdlib/gleam/function.mjs +function identity2(x) { + return x; +} +// build/dev/javascript/gleam_json/gleam_json_ffi.mjs +function json_to_string(json) { + return JSON.stringify(json); +} +function object(entries) { + return Object.fromEntries(entries); +} +function identity3(x) { + return x; +} +function array(list2) { + const array2 = []; + while (List$isNonEmpty(list2)) { + array2.push(List$NonEmpty$first(list2)); + list2 = List$NonEmpty$rest(list2); + } + return array2; +} + +// build/dev/javascript/gleam_json/gleam/json.mjs +function to_string2(json) { + return json_to_string(json); +} +function string3(input) { + return identity3(input); +} +function bool(input) { + return identity3(input); +} +function int3(input) { + return identity3(input); +} +function object2(entries) { + return object(entries); +} +function preprocessed_array(from2) { + return array(from2); +} +function array2(entries, inner_type) { + let _pipe = entries; + let _pipe$1 = map2(_pipe, inner_type); + return preprocessed_array(_pipe$1); +} + +// build/dev/javascript/houdini/houdini.ffi.mjs +function do_escape(string4) { + return string4.replaceAll(/[><&"']/g, (replaced) => { + switch (replaced) { + case ">": + return ">"; + case "<": + return "<"; + case "'": + return "'"; + case "&": + return "&"; + case '"': + return """; + default: + return replaced; + } + }); +} + +// build/dev/javascript/houdini/houdini/internal/escape_js.mjs +function escape(text) { + return do_escape(text); +} + +// build/dev/javascript/houdini/houdini.mjs +function escape2(string4) { + return escape(string4); +} + +// build/dev/javascript/lustre/lustre/internals/constants.mjs +var empty_list = /* @__PURE__ */ toList([]); +var error_nil = /* @__PURE__ */ new Error(undefined); + +// build/dev/javascript/lustre/lustre/vdom/vattr.ffi.mjs +var GT = /* @__PURE__ */ Order$Gt(); +var LT = /* @__PURE__ */ Order$Lt(); +var EQ = /* @__PURE__ */ Order$Eq(); +function compare3(a, b) { + if (a.name === b.name) { + return EQ; + } else if (a.name < b.name) { + return LT; + } else { + return GT; + } +} + +// build/dev/javascript/lustre/lustre/vdom/vattr.mjs +class Attribute extends CustomType { + constructor(kind, name, value) { + super(); + this.kind = kind; + this.name = name; + this.value = value; + } +} +class Property extends CustomType { + constructor(kind, name, value) { + super(); + this.kind = kind; + this.name = name; + this.value = value; + } +} +class Event2 extends CustomType { + constructor(kind, name, handler, include, prevent_default, stop_propagation, debounce, throttle) { + super(); + this.kind = kind; + this.name = name; + this.handler = handler; + this.include = include; + this.prevent_default = prevent_default; + this.stop_propagation = stop_propagation; + this.debounce = debounce; + this.throttle = throttle; + } +} +class Handler extends CustomType { + constructor(prevent_default, stop_propagation, message) { + super(); + this.prevent_default = prevent_default; + this.stop_propagation = stop_propagation; + this.message = message; + } +} +class Never extends CustomType { + constructor(kind) { + super(); + this.kind = kind; + } +} +var attribute_kind = 0; +var property_kind = 1; +var event_kind = 2; +var never_kind = 0; +var never = /* @__PURE__ */ new Never(never_kind); +var always_kind = 2; +function merge(loop$attributes, loop$merged) { + while (true) { + let attributes = loop$attributes; + let merged = loop$merged; + if (attributes instanceof Empty) { + return merged; + } else { + let $ = attributes.head; + if ($ instanceof Attribute) { + let $1 = $.name; + if ($1 === "") { + let rest = attributes.tail; + loop$attributes = rest; + loop$merged = merged; + } else if ($1 === "class") { + let $2 = $.value; + if ($2 === "") { + let rest = attributes.tail; + loop$attributes = rest; + loop$merged = merged; + } else { + let $3 = attributes.tail; + if ($3 instanceof Empty) { + let attribute$1 = $; + let rest = $3; + loop$attributes = rest; + loop$merged = prepend(attribute$1, merged); + } else { + let $4 = $3.head; + if ($4 instanceof Attribute) { + let $5 = $4.name; + if ($5 === "class") { + let kind = $.kind; + let class1 = $2; + let rest = $3.tail; + let class2 = $4.value; + let value = class1 + " " + class2; + let attribute$1 = new Attribute(kind, "class", value); + loop$attributes = prepend(attribute$1, rest); + loop$merged = merged; + } else { + let attribute$1 = $; + let rest = $3; + loop$attributes = rest; + loop$merged = prepend(attribute$1, merged); + } + } else { + let attribute$1 = $; + let rest = $3; + loop$attributes = rest; + loop$merged = prepend(attribute$1, merged); + } + } + } + } else if ($1 === "style") { + let $2 = $.value; + if ($2 === "") { + let rest = attributes.tail; + loop$attributes = rest; + loop$merged = merged; + } else { + let $3 = attributes.tail; + if ($3 instanceof Empty) { + let attribute$1 = $; + let rest = $3; + loop$attributes = rest; + loop$merged = prepend(attribute$1, merged); + } else { + let $4 = $3.head; + if ($4 instanceof Attribute) { + let $5 = $4.name; + if ($5 === "style") { + let kind = $.kind; + let style1 = $2; + let rest = $3.tail; + let style2 = $4.value; + let value = style1 + ";" + style2; + let attribute$1 = new Attribute(kind, "style", value); + loop$attributes = prepend(attribute$1, rest); + loop$merged = merged; + } else { + let attribute$1 = $; + let rest = $3; + loop$attributes = rest; + loop$merged = prepend(attribute$1, merged); + } + } else { + let attribute$1 = $; + let rest = $3; + loop$attributes = rest; + loop$merged = prepend(attribute$1, merged); + } + } + } + } else { + let attribute$1 = $; + let rest = attributes.tail; + loop$attributes = rest; + loop$merged = prepend(attribute$1, merged); + } + } else { + let attribute$1 = $; + let rest = attributes.tail; + loop$attributes = rest; + loop$merged = prepend(attribute$1, merged); + } + } + } +} +function prepare(attributes) { + if (attributes instanceof Empty) { + return attributes; + } else { + let $ = attributes.tail; + if ($ instanceof Empty) { + return attributes; + } else { + let _pipe = attributes; + let _pipe$1 = sort(_pipe, (a, b) => { + return compare3(b, a); + }); + return merge(_pipe$1, empty_list); + } + } +} +function attribute(name, value) { + return new Attribute(attribute_kind, name, value); +} +function property(name, value) { + return new Property(property_kind, name, value); +} +function event(name, handler, include, prevent_default, stop_propagation, debounce, throttle) { + return new Event2(event_kind, name, handler, include, prevent_default, stop_propagation, debounce, throttle); +} + +// build/dev/javascript/lustre/lustre/attribute.mjs +function attribute2(name, value) { + return attribute(name, value); +} +function property2(name, value) { + return property(name, value); +} +function boolean_attribute(name, value) { + if (value) { + return attribute2(name, ""); + } else { + return property2(name, bool(false)); + } +} +function class$(name) { + return attribute2("class", name); +} +function style(property3, value) { + if (property3 === "") { + return class$(""); + } else if (value === "") { + return class$(""); + } else { + return attribute2("style", property3 + ":" + value + ";"); + } +} +function do_styles(loop$properties, loop$styles) { + while (true) { + let properties = loop$properties; + let styles = loop$styles; + if (properties instanceof Empty) { + return styles; + } else { + let $ = properties.head[0]; + if ($ === "") { + let rest = properties.tail; + loop$properties = rest; + loop$styles = styles; + } else { + let $1 = properties.head[1]; + if ($1 === "") { + let rest = properties.tail; + loop$properties = rest; + loop$styles = styles; + } else { + let rest = properties.tail; + let name$1 = $; + let value$1 = $1; + loop$properties = rest; + loop$styles = styles + name$1 + ":" + value$1 + ";"; + } + } + } + } +} +function styles(properties) { + return attribute2("style", do_styles(properties, "")); +} +function disabled(is_disabled) { + return boolean_attribute("disabled", is_disabled); +} +function min2(value) { + return attribute2("min", value); +} +function placeholder(text) { + return attribute2("placeholder", text); +} +function type_(control_type) { + return attribute2("type", control_type); +} +function value(control_value) { + return attribute2("value", control_value); +} + +// build/dev/javascript/lustre/lustre/effect.mjs +class Effect extends CustomType { + constructor(synchronous, before_paint, after_paint) { + super(); + this.synchronous = synchronous; + this.before_paint = before_paint; + this.after_paint = after_paint; + } +} + +class Actions extends CustomType { + constructor(dispatch, emit, select, root, provide) { + super(); + this.dispatch = dispatch; + this.emit = emit; + this.select = select; + this.root = root; + this.provide = provide; + } +} +var empty = /* @__PURE__ */ new Effect(/* @__PURE__ */ toList([]), /* @__PURE__ */ toList([]), /* @__PURE__ */ toList([])); +function perform(effect, dispatch, emit, select, root, provide) { + let actions = new Actions(dispatch, emit, select, root, provide); + return each(effect.synchronous, (run2) => { + return run2(actions); + }); +} +function none() { + return empty; +} +function from2(effect) { + let task = (actions) => { + let dispatch = actions.dispatch; + return effect(dispatch); + }; + return new Effect(toList([task]), empty.before_paint, empty.after_paint); +} +function batch(effects) { + return fold2(effects, empty, (acc, eff) => { + return new Effect(fold2(eff.synchronous, acc.synchronous, prepend2), fold2(eff.before_paint, acc.before_paint, prepend2), fold2(eff.after_paint, acc.after_paint, prepend2)); + }); +} + +// build/dev/javascript/lustre/lustre/internals/mutable_map.ffi.mjs +function empty2() { + return null; +} +function get2(map6, key) { + return map6?.get(key); +} +function get_or_compute(map6, key, compute) { + return map6?.get(key) ?? compute(); +} +function has_key(map6, key) { + return map6 && map6.has(key); +} +function insert2(map6, key, value2) { + map6 ??= new Map; + map6.set(key, value2); + return map6; +} +function remove(map6, key) { + map6?.delete(key); + return map6; +} + +// build/dev/javascript/lustre/lustre/internals/ref.ffi.mjs +function sameValueZero(x, y) { + if (typeof x === "number" && typeof y === "number") { + return x === y || x !== x && y !== y; + } + return x === y; +} + +// build/dev/javascript/lustre/lustre/internals/ref.mjs +function equal_lists(loop$xs, loop$ys) { + while (true) { + let xs = loop$xs; + let ys = loop$ys; + if (xs instanceof Empty) { + if (ys instanceof Empty) { + return true; + } else { + return false; + } + } else if (ys instanceof Empty) { + return false; + } else { + let x = xs.head; + let xs$1 = xs.tail; + let y = ys.head; + let ys$1 = ys.tail; + let $ = sameValueZero(x, y); + if ($) { + loop$xs = xs$1; + loop$ys = ys$1; + } else { + return $; + } + } + } +} + +// build/dev/javascript/lustre/lustre/vdom/vnode.mjs +class Fragment extends CustomType { + constructor(kind, key, children, keyed_children) { + super(); + this.kind = kind; + this.key = key; + this.children = children; + this.keyed_children = keyed_children; + } +} +class Element extends CustomType { + constructor(kind, key, namespace, tag, attributes, children, keyed_children, self_closing, void$) { + super(); + this.kind = kind; + this.key = key; + this.namespace = namespace; + this.tag = tag; + this.attributes = attributes; + this.children = children; + this.keyed_children = keyed_children; + this.self_closing = self_closing; + this.void = void$; + } +} +class Text extends CustomType { + constructor(kind, key, content) { + super(); + this.kind = kind; + this.key = key; + this.content = content; + } +} +class UnsafeInnerHtml extends CustomType { + constructor(kind, key, namespace, tag, attributes, inner_html) { + super(); + this.kind = kind; + this.key = key; + this.namespace = namespace; + this.tag = tag; + this.attributes = attributes; + this.inner_html = inner_html; + } +} +class Map2 extends CustomType { + constructor(kind, key, mapper, child) { + super(); + this.kind = kind; + this.key = key; + this.mapper = mapper; + this.child = child; + } +} +class Memo extends CustomType { + constructor(kind, key, dependencies, view) { + super(); + this.kind = kind; + this.key = key; + this.dependencies = dependencies; + this.view = view; + } +} +var fragment_kind = 0; +var element_kind = 1; +var text_kind = 2; +var unsafe_inner_html_kind = 3; +var map_kind = 4; +var memo_kind = 5; +function is_void_html_element(tag, namespace) { + if (namespace === "") { + if (tag === "area") { + return true; + } else if (tag === "base") { + return true; + } else if (tag === "br") { + return true; + } else if (tag === "col") { + return true; + } else if (tag === "embed") { + return true; + } else if (tag === "hr") { + return true; + } else if (tag === "img") { + return true; + } else if (tag === "input") { + return true; + } else if (tag === "link") { + return true; + } else if (tag === "meta") { + return true; + } else if (tag === "param") { + return true; + } else if (tag === "source") { + return true; + } else if (tag === "track") { + return true; + } else if (tag === "wbr") { + return true; + } else { + return false; + } + } else { + return false; + } +} +function to_keyed(key, node) { + if (node instanceof Fragment) { + return new Fragment(node.kind, key, node.children, node.keyed_children); + } else if (node instanceof Element) { + return new Element(node.kind, key, node.namespace, node.tag, node.attributes, node.children, node.keyed_children, node.self_closing, node.void); + } else if (node instanceof Text) { + return new Text(node.kind, key, node.content); + } else if (node instanceof UnsafeInnerHtml) { + return new UnsafeInnerHtml(node.kind, key, node.namespace, node.tag, node.attributes, node.inner_html); + } else if (node instanceof Map2) { + let child = node.child; + return new Map2(node.kind, key, node.mapper, to_keyed(key, child)); + } else { + let view = node.view; + return new Memo(node.kind, key, node.dependencies, () => { + return to_keyed(key, view()); + }); + } +} +function fragment(key, children, keyed_children) { + return new Fragment(fragment_kind, key, children, keyed_children); +} +function element(key, namespace, tag, attributes, children, keyed_children, self_closing, void$) { + return new Element(element_kind, key, namespace, tag, prepare(attributes), children, keyed_children, self_closing, void$); +} +function text(key, content) { + return new Text(text_kind, key, content); +} +function map6(element2, mapper) { + if (element2 instanceof Map2) { + let child_mapper = element2.mapper; + return new Map2(map_kind, element2.key, (handler) => { + return identity2(mapper)(child_mapper(handler)); + }, identity2(element2.child)); + } else { + return new Map2(map_kind, element2.key, identity2(mapper), identity2(element2)); + } +} +function memo(key, dependencies, view) { + return new Memo(memo_kind, key, dependencies, view); +} + +// build/dev/javascript/lustre/lustre/element.mjs +function element2(tag, attributes, children) { + return element("", "", tag, attributes, children, empty2(), false, is_void_html_element(tag, "")); +} +function text2(content) { + return text("", content); +} +function none2() { + return text("", ""); +} +function memo2(dependencies, view) { + return memo("", dependencies, view); +} +function ref(value2) { + return identity2(value2); +} +function map7(element3, f) { + return map6(element3, f); +} + +// build/dev/javascript/lustre/lustre/element/html.mjs +function text3(content) { + return text2(content); +} +function h1(attrs, children) { + return element2("h1", attrs, children); +} +function div(attrs, children) { + return element2("div", attrs, children); +} +function li(attrs, children) { + return element2("li", attrs, children); +} +function p(attrs, children) { + return element2("p", attrs, children); +} +function ul(attrs, children) { + return element2("ul", attrs, children); +} +function span(attrs, children) { + return element2("span", attrs, children); +} +function button(attrs, children) { + return element2("button", attrs, children); +} +function input(attrs) { + return element2("input", attrs, empty_list); +} + +// build/dev/javascript/lustre/lustre/vdom/patch.mjs +class Patch2 extends CustomType { + constructor(index4, removed, changes, children) { + super(); + this.index = index4; + this.removed = removed; + this.changes = changes; + this.children = children; + } +} +class ReplaceText extends CustomType { + constructor(kind, content) { + super(); + this.kind = kind; + this.content = content; + } +} +class ReplaceInnerHtml extends CustomType { + constructor(kind, inner_html) { + super(); + this.kind = kind; + this.inner_html = inner_html; + } +} +class Update extends CustomType { + constructor(kind, added, removed) { + super(); + this.kind = kind; + this.added = added; + this.removed = removed; + } +} +class Move extends CustomType { + constructor(kind, key, before) { + super(); + this.kind = kind; + this.key = key; + this.before = before; + } +} +class Replace extends CustomType { + constructor(kind, index4, with$) { + super(); + this.kind = kind; + this.index = index4; + this.with = with$; + } +} +class Remove extends CustomType { + constructor(kind, index4) { + super(); + this.kind = kind; + this.index = index4; + } +} +class Insert extends CustomType { + constructor(kind, children, before) { + super(); + this.kind = kind; + this.children = children; + this.before = before; + } +} +var replace_text_kind = 0; +var replace_inner_html_kind = 1; +var update_kind = 2; +var move_kind = 3; +var remove_kind = 4; +var replace_kind = 5; +var insert_kind = 6; +function new$3(index4, removed, changes, children) { + return new Patch2(index4, removed, changes, children); +} +function replace_text(content) { + return new ReplaceText(replace_text_kind, content); +} +function replace_inner_html(inner_html) { + return new ReplaceInnerHtml(replace_inner_html_kind, inner_html); +} +function update(added, removed) { + return new Update(update_kind, added, removed); +} +function move(key, before) { + return new Move(move_kind, key, before); +} +function remove2(index4) { + return new Remove(remove_kind, index4); +} +function replace2(index4, with$) { + return new Replace(replace_kind, index4, with$); +} +function insert3(children, before) { + return new Insert(insert_kind, children, before); +} + +// build/dev/javascript/lustre/lustre/runtime/transport.mjs +class Mount extends CustomType { + constructor(kind, open_shadow_root, will_adopt_styles, observed_attributes, observed_properties, requested_contexts, provided_contexts, vdom, memos) { + super(); + this.kind = kind; + this.open_shadow_root = open_shadow_root; + this.will_adopt_styles = will_adopt_styles; + this.observed_attributes = observed_attributes; + this.observed_properties = observed_properties; + this.requested_contexts = requested_contexts; + this.provided_contexts = provided_contexts; + this.vdom = vdom; + this.memos = memos; + } +} +class Reconcile extends CustomType { + constructor(kind, patch, memos) { + super(); + this.kind = kind; + this.patch = patch; + this.memos = memos; + } +} +class Emit extends CustomType { + constructor(kind, name, data2) { + super(); + this.kind = kind; + this.name = name; + this.data = data2; + } +} +class Provide extends CustomType { + constructor(kind, key, value2) { + super(); + this.kind = kind; + this.key = key; + this.value = value2; + } +} +class Batch extends CustomType { + constructor(kind, messages) { + super(); + this.kind = kind; + this.messages = messages; + } +} +var ServerMessage$isBatch = (value2) => value2 instanceof Batch; +class AttributeChanged extends CustomType { + constructor(kind, name, value2) { + super(); + this.kind = kind; + this.name = name; + this.value = value2; + } +} +var ServerMessage$isAttributeChanged = (value2) => value2 instanceof AttributeChanged; +class PropertyChanged extends CustomType { + constructor(kind, name, value2) { + super(); + this.kind = kind; + this.name = name; + this.value = value2; + } +} +var ServerMessage$isPropertyChanged = (value2) => value2 instanceof PropertyChanged; +class EventFired extends CustomType { + constructor(kind, path, name, event2) { + super(); + this.kind = kind; + this.path = path; + this.name = name; + this.event = event2; + } +} +var ServerMessage$isEventFired = (value2) => value2 instanceof EventFired; +class ContextProvided extends CustomType { + constructor(kind, key, value2) { + super(); + this.kind = kind; + this.key = key; + this.value = value2; + } +} +var ServerMessage$isContextProvided = (value2) => value2 instanceof ContextProvided; +var mount_kind = 0; +var reconcile_kind = 1; +var emit_kind = 2; +var provide_kind = 3; +function mount(open_shadow_root, will_adopt_styles, observed_attributes, observed_properties, requested_contexts, provided_contexts, vdom, memos) { + return new Mount(mount_kind, open_shadow_root, will_adopt_styles, observed_attributes, observed_properties, requested_contexts, provided_contexts, vdom, memos); +} +function reconcile(patch, memos) { + return new Reconcile(reconcile_kind, patch, memos); +} +function emit(name, data2) { + return new Emit(emit_kind, name, data2); +} +function provide(key, value2) { + return new Provide(provide_kind, key, value2); +} + +// build/dev/javascript/lustre/lustre/vdom/path.mjs +class Root extends CustomType { +} + +class Key extends CustomType { + constructor(key, parent) { + super(); + this.key = key; + this.parent = parent; + } +} + +class Index extends CustomType { + constructor(index4, parent) { + super(); + this.index = index4; + this.parent = parent; + } +} + +class Subtree extends CustomType { + constructor(parent) { + super(); + this.parent = parent; + } +} +var root = /* @__PURE__ */ new Root; +var separator_element = "\t"; +var separator_subtree = "\r"; +var separator_event = ` +`; +function do_matches(loop$path, loop$candidates) { + while (true) { + let path = loop$path; + let candidates = loop$candidates; + if (candidates instanceof Empty) { + return false; + } else { + let candidate = candidates.head; + let rest = candidates.tail; + let $ = starts_with(path, candidate); + if ($) { + return $; + } else { + loop$path = path; + loop$candidates = rest; + } + } + } +} +function add2(parent, index4, key) { + if (key === "") { + return new Index(index4, parent); + } else { + return new Key(key, parent); + } +} +function subtree(path) { + return new Subtree(path); +} +function finish_to_string(acc) { + if (acc instanceof Empty) { + return ""; + } else { + let segments = acc.tail; + return concat2(segments); + } +} +function split_subtree_path(path) { + return split2(path, separator_subtree); +} +function do_to_string(loop$full, loop$path, loop$acc) { + while (true) { + let full = loop$full; + let path = loop$path; + let acc = loop$acc; + if (path instanceof Root) { + return finish_to_string(acc); + } else if (path instanceof Key) { + let key = path.key; + let parent = path.parent; + loop$full = full; + loop$path = parent; + loop$acc = prepend(separator_element, prepend(key, acc)); + } else if (path instanceof Index) { + let index4 = path.index; + let parent = path.parent; + let acc$1 = prepend(separator_element, prepend(to_string(index4), acc)); + loop$full = full; + loop$path = parent; + loop$acc = acc$1; + } else if (!full) { + return finish_to_string(acc); + } else { + let parent = path.parent; + if (acc instanceof Empty) { + loop$full = full; + loop$path = parent; + loop$acc = acc; + } else { + let acc$1 = acc.tail; + loop$full = full; + loop$path = parent; + loop$acc = prepend(separator_subtree, acc$1); + } + } + } +} +function child(path) { + return do_to_string(false, path, empty_list); +} +function to_string4(path) { + return do_to_string(true, path, empty_list); +} +function matches(path, candidates) { + if (candidates instanceof Empty) { + return false; + } else { + return do_matches(to_string4(path), candidates); + } +} +function event2(path, event3) { + return do_to_string(false, path, prepend(separator_event, prepend(event3, empty_list))); +} + +// build/dev/javascript/lustre/lustre/vdom/cache.mjs +class Cache extends CustomType { + constructor(events, vdoms, old_vdoms, dispatched_paths, next_dispatched_paths) { + super(); + this.events = events; + this.vdoms = vdoms; + this.old_vdoms = old_vdoms; + this.dispatched_paths = dispatched_paths; + this.next_dispatched_paths = next_dispatched_paths; + } +} + +class Events extends CustomType { + constructor(handlers, children) { + super(); + this.handlers = handlers; + this.children = children; + } +} + +class Child extends CustomType { + constructor(mapper, events) { + super(); + this.mapper = mapper; + this.events = events; + } +} + +class AddedChildren extends CustomType { + constructor(handlers, children, vdoms) { + super(); + this.handlers = handlers; + this.children = children; + this.vdoms = vdoms; + } +} + +class DecodedEvent extends CustomType { + constructor(path, handler) { + super(); + this.path = path; + this.handler = handler; + } +} + +class DispatchedEvent extends CustomType { + constructor(path) { + super(); + this.path = path; + } +} +function compose_mapper(mapper, child_mapper) { + return (msg) => { + return mapper(child_mapper(msg)); + }; +} +function new_events() { + return new Events(empty2(), empty2()); +} +function new$4() { + return new Cache(new_events(), empty2(), empty2(), empty_list, empty_list); +} +function tick(cache) { + return new Cache(cache.events, empty2(), cache.vdoms, cache.next_dispatched_paths, empty_list); +} +function events(cache) { + return cache.events; +} +function update_events(cache, events2) { + return new Cache(events2, cache.vdoms, cache.old_vdoms, cache.dispatched_paths, cache.next_dispatched_paths); +} +function memos(cache) { + return cache.vdoms; +} +function get_old_memo(cache, old, new$5) { + return get_or_compute(cache.old_vdoms, old, new$5); +} +function keep_memo(cache, old, new$5) { + let node = get_or_compute(cache.old_vdoms, old, new$5); + let vdoms = insert2(cache.vdoms, new$5, node); + return new Cache(cache.events, vdoms, cache.old_vdoms, cache.dispatched_paths, cache.next_dispatched_paths); +} +function add_memo(cache, new$5, node) { + let vdoms = insert2(cache.vdoms, new$5, node); + return new Cache(cache.events, vdoms, cache.old_vdoms, cache.dispatched_paths, cache.next_dispatched_paths); +} +function get_subtree(events2, path, old_mapper) { + let child2 = get_or_compute(events2.children, path, () => { + return new Child(old_mapper, new_events()); + }); + return child2.events; +} +function update_subtree(parent, path, mapper, events2) { + let new_child = new Child(mapper, events2); + let children = insert2(parent.children, path, new_child); + return new Events(parent.handlers, children); +} +function do_add_event(handlers, path, name, handler) { + return insert2(handlers, event2(path, name), handler); +} +function add_event(events2, path, name, handler) { + let handlers = do_add_event(events2.handlers, path, name, handler); + return new Events(handlers, events2.children); +} +function do_remove_event(handlers, path, name) { + return remove(handlers, event2(path, name)); +} +function remove_event(events2, path, name) { + let handlers = do_remove_event(events2.handlers, path, name); + return new Events(handlers, events2.children); +} +function add_attributes(handlers, path, attributes) { + return fold2(attributes, handlers, (events2, attribute3) => { + if (attribute3 instanceof Event2) { + let name = attribute3.name; + let handler = attribute3.handler; + return do_add_event(events2, path, name, handler); + } else { + return events2; + } + }); +} +function do_add_children(loop$handlers, loop$children, loop$vdoms, loop$parent, loop$child_index, loop$nodes) { + while (true) { + let handlers = loop$handlers; + let children = loop$children; + let vdoms = loop$vdoms; + let parent = loop$parent; + let child_index = loop$child_index; + let nodes = loop$nodes; + let next = child_index + 1; + if (nodes instanceof Empty) { + return new AddedChildren(handlers, children, vdoms); + } else { + let $ = nodes.head; + if ($ instanceof Fragment) { + let rest = nodes.tail; + let key = $.key; + let nodes$1 = $.children; + let path = add2(parent, child_index, key); + let $1 = do_add_children(handlers, children, vdoms, path, 0, nodes$1); + let handlers$1; + let children$1; + let vdoms$1; + handlers$1 = $1.handlers; + children$1 = $1.children; + vdoms$1 = $1.vdoms; + loop$handlers = handlers$1; + loop$children = children$1; + loop$vdoms = vdoms$1; + loop$parent = parent; + loop$child_index = next; + loop$nodes = rest; + } else if ($ instanceof Element) { + let rest = nodes.tail; + let key = $.key; + let attributes = $.attributes; + let nodes$1 = $.children; + let path = add2(parent, child_index, key); + let handlers$1 = add_attributes(handlers, path, attributes); + let $1 = do_add_children(handlers$1, children, vdoms, path, 0, nodes$1); + let handlers$2; + let children$1; + let vdoms$1; + handlers$2 = $1.handlers; + children$1 = $1.children; + vdoms$1 = $1.vdoms; + loop$handlers = handlers$2; + loop$children = children$1; + loop$vdoms = vdoms$1; + loop$parent = parent; + loop$child_index = next; + loop$nodes = rest; + } else if ($ instanceof Text) { + let rest = nodes.tail; + loop$handlers = handlers; + loop$children = children; + loop$vdoms = vdoms; + loop$parent = parent; + loop$child_index = next; + loop$nodes = rest; + } else if ($ instanceof UnsafeInnerHtml) { + let rest = nodes.tail; + let key = $.key; + let attributes = $.attributes; + let path = add2(parent, child_index, key); + let handlers$1 = add_attributes(handlers, path, attributes); + loop$handlers = handlers$1; + loop$children = children; + loop$vdoms = vdoms; + loop$parent = parent; + loop$child_index = next; + loop$nodes = rest; + } else if ($ instanceof Map2) { + let rest = nodes.tail; + let key = $.key; + let mapper = $.mapper; + let child2 = $.child; + let path = add2(parent, child_index, key); + let added = do_add_children(empty2(), empty2(), vdoms, subtree(path), 0, prepend(child2, empty_list)); + let vdoms$1 = added.vdoms; + let child_events = new Events(added.handlers, added.children); + let child$1 = new Child(mapper, child_events); + let children$1 = insert2(children, child(path), child$1); + loop$handlers = handlers; + loop$children = children$1; + loop$vdoms = vdoms$1; + loop$parent = parent; + loop$child_index = next; + loop$nodes = rest; + } else { + let rest = nodes.tail; + let view = $.view; + let child_node = view(); + let vdoms$1 = insert2(vdoms, view, child_node); + let next$1 = child_index; + let rest$1 = prepend(child_node, rest); + loop$handlers = handlers; + loop$children = children; + loop$vdoms = vdoms$1; + loop$parent = parent; + loop$child_index = next$1; + loop$nodes = rest$1; + } + } + } +} +function add_children(cache, events2, path, child_index, nodes) { + let vdoms = cache.vdoms; + let handlers; + let children; + handlers = events2.handlers; + children = events2.children; + let $ = do_add_children(handlers, children, vdoms, path, child_index, nodes); + let handlers$1; + let children$1; + let vdoms$1; + handlers$1 = $.handlers; + children$1 = $.children; + vdoms$1 = $.vdoms; + return [ + new Cache(cache.events, vdoms$1, cache.old_vdoms, cache.dispatched_paths, cache.next_dispatched_paths), + new Events(handlers$1, children$1) + ]; +} +function add_child(cache, events2, parent, index4, child2) { + let children = prepend(child2, empty_list); + return add_children(cache, events2, parent, index4, children); +} +function from_node(root2) { + let cache = new$4(); + let $ = add_child(cache, cache.events, root, 0, root2); + let cache$1; + let events$1; + cache$1 = $[0]; + events$1 = $[1]; + return new Cache(events$1, cache$1.vdoms, cache$1.old_vdoms, cache$1.dispatched_paths, cache$1.next_dispatched_paths); +} +function remove_attributes(handlers, path, attributes) { + return fold2(attributes, handlers, (events2, attribute3) => { + if (attribute3 instanceof Event2) { + let name = attribute3.name; + return do_remove_event(events2, path, name); + } else { + return events2; + } + }); +} +function do_remove_children(loop$handlers, loop$children, loop$vdoms, loop$parent, loop$index, loop$nodes) { + while (true) { + let handlers = loop$handlers; + let children = loop$children; + let vdoms = loop$vdoms; + let parent = loop$parent; + let index4 = loop$index; + let nodes = loop$nodes; + let next = index4 + 1; + if (nodes instanceof Empty) { + return new Events(handlers, children); + } else { + let $ = nodes.head; + if ($ instanceof Fragment) { + let rest = nodes.tail; + let key = $.key; + let nodes$1 = $.children; + let path = add2(parent, index4, key); + let $1 = do_remove_children(handlers, children, vdoms, path, 0, nodes$1); + let handlers$1; + let children$1; + handlers$1 = $1.handlers; + children$1 = $1.children; + loop$handlers = handlers$1; + loop$children = children$1; + loop$vdoms = vdoms; + loop$parent = parent; + loop$index = next; + loop$nodes = rest; + } else if ($ instanceof Element) { + let rest = nodes.tail; + let key = $.key; + let attributes = $.attributes; + let nodes$1 = $.children; + let path = add2(parent, index4, key); + let handlers$1 = remove_attributes(handlers, path, attributes); + let $1 = do_remove_children(handlers$1, children, vdoms, path, 0, nodes$1); + let handlers$2; + let children$1; + handlers$2 = $1.handlers; + children$1 = $1.children; + loop$handlers = handlers$2; + loop$children = children$1; + loop$vdoms = vdoms; + loop$parent = parent; + loop$index = next; + loop$nodes = rest; + } else if ($ instanceof Text) { + let rest = nodes.tail; + loop$handlers = handlers; + loop$children = children; + loop$vdoms = vdoms; + loop$parent = parent; + loop$index = next; + loop$nodes = rest; + } else if ($ instanceof UnsafeInnerHtml) { + let rest = nodes.tail; + let key = $.key; + let attributes = $.attributes; + let path = add2(parent, index4, key); + let handlers$1 = remove_attributes(handlers, path, attributes); + loop$handlers = handlers$1; + loop$children = children; + loop$vdoms = vdoms; + loop$parent = parent; + loop$index = next; + loop$nodes = rest; + } else if ($ instanceof Map2) { + let rest = nodes.tail; + let key = $.key; + let path = add2(parent, index4, key); + let children$1 = remove(children, child(path)); + loop$handlers = handlers; + loop$children = children$1; + loop$vdoms = vdoms; + loop$parent = parent; + loop$index = next; + loop$nodes = rest; + } else { + let rest = nodes.tail; + let view = $.view; + let $1 = has_key(vdoms, view); + if ($1) { + let child2 = get2(vdoms, view); + let nodes$1 = prepend(child2, rest); + loop$handlers = handlers; + loop$children = children; + loop$vdoms = vdoms; + loop$parent = parent; + loop$index = index4; + loop$nodes = nodes$1; + } else { + loop$handlers = handlers; + loop$children = children; + loop$vdoms = vdoms; + loop$parent = parent; + loop$index = next; + loop$nodes = rest; + } + } + } + } +} +function remove_child(cache, events2, parent, child_index, child2) { + return do_remove_children(events2.handlers, events2.children, cache.old_vdoms, parent, child_index, prepend(child2, empty_list)); +} +function replace_child(cache, events2, parent, child_index, prev, next) { + let events$1 = remove_child(cache, events2, parent, child_index, prev); + return add_child(cache, events$1, parent, child_index, next); +} +function dispatch(cache, event3) { + let next_dispatched_paths = prepend(event3.path, cache.next_dispatched_paths); + let cache$1 = new Cache(cache.events, cache.vdoms, cache.old_vdoms, cache.dispatched_paths, next_dispatched_paths); + if (event3 instanceof DecodedEvent) { + let handler = event3.handler; + return [cache$1, new Ok(handler)]; + } else { + return [cache$1, error_nil]; + } +} +function has_dispatched_events(cache, path) { + return matches(path, cache.dispatched_paths); +} +function get_handler(loop$events, loop$path, loop$mapper) { + while (true) { + let events2 = loop$events; + let path = loop$path; + let mapper = loop$mapper; + if (path instanceof Empty) { + return error_nil; + } else { + let $ = path.tail; + if ($ instanceof Empty) { + let key = path.head; + let $1 = has_key(events2.handlers, key); + if ($1) { + let handler = get2(events2.handlers, key); + return new Ok(map3(handler, (handler2) => { + return new Handler(handler2.prevent_default, handler2.stop_propagation, identity2(mapper)(handler2.message)); + })); + } else { + return error_nil; + } + } else { + let key = path.head; + let path$1 = $; + let $1 = has_key(events2.children, key); + if ($1) { + let child2 = get2(events2.children, key); + let mapper$1 = compose_mapper(mapper, child2.mapper); + loop$events = child2.events; + loop$path = path$1; + loop$mapper = mapper$1; + } else { + return error_nil; + } + } + } + } +} +function decode2(cache, path, name, event3) { + let parts = split_subtree_path(path + separator_event + name); + let $ = get_handler(cache.events, parts, identity2); + if ($ instanceof Ok) { + let handler = $[0]; + let $1 = run(event3, handler); + if ($1 instanceof Ok) { + let handler$1 = $1[0]; + return new DecodedEvent(path, handler$1); + } else { + return new DispatchedEvent(path); + } + } else { + return new DispatchedEvent(path); + } +} +function handle(cache, path, name, event3) { + let _pipe = decode2(cache, path, name, event3); + return ((_capture) => { + return dispatch(cache, _capture); + })(_pipe); +} + +// build/dev/javascript/lustre/lustre/runtime/server/runtime.mjs +class ClientDispatchedMessage extends CustomType { + constructor(message) { + super(); + this.message = message; + } +} +var Message$isClientDispatchedMessage = (value2) => value2 instanceof ClientDispatchedMessage; +class ClientRegisteredCallback extends CustomType { + constructor(callback) { + super(); + this.callback = callback; + } +} +var Message$isClientRegisteredCallback = (value2) => value2 instanceof ClientRegisteredCallback; +class ClientDeregisteredCallback extends CustomType { + constructor(callback) { + super(); + this.callback = callback; + } +} +var Message$isClientDeregisteredCallback = (value2) => value2 instanceof ClientDeregisteredCallback; +class EffectDispatchedMessage extends CustomType { + constructor(message) { + super(); + this.message = message; + } +} +var Message$EffectDispatchedMessage = (message) => new EffectDispatchedMessage(message); +var Message$isEffectDispatchedMessage = (value2) => value2 instanceof EffectDispatchedMessage; +class EffectEmitEvent extends CustomType { + constructor(name, data2) { + super(); + this.name = name; + this.data = data2; + } +} +var Message$EffectEmitEvent = (name, data2) => new EffectEmitEvent(name, data2); +var Message$isEffectEmitEvent = (value2) => value2 instanceof EffectEmitEvent; +class EffectProvidedValue extends CustomType { + constructor(key, value2) { + super(); + this.key = key; + this.value = value2; + } +} +var Message$EffectProvidedValue = (key, value2) => new EffectProvidedValue(key, value2); +var Message$isEffectProvidedValue = (value2) => value2 instanceof EffectProvidedValue; +class SystemRequestedShutdown extends CustomType { +} +var Message$isSystemRequestedShutdown = (value2) => value2 instanceof SystemRequestedShutdown; + +// build/dev/javascript/lustre/lustre/runtime/app.mjs +class App extends CustomType { + constructor(name, init, update2, view, config2) { + super(); + this.name = name; + this.init = init; + this.update = update2; + this.view = view; + this.config = config2; + } +} +class Config2 extends CustomType { + constructor(open_shadow_root, adopt_styles, delegates_focus, attributes, properties, contexts, is_form_associated, on_form_autofill, on_form_reset, on_form_restore, on_connect, on_adopt, on_disconnect) { + super(); + this.open_shadow_root = open_shadow_root; + this.adopt_styles = adopt_styles; + this.delegates_focus = delegates_focus; + this.attributes = attributes; + this.properties = properties; + this.contexts = contexts; + this.is_form_associated = is_form_associated; + this.on_form_autofill = on_form_autofill; + this.on_form_reset = on_form_reset; + this.on_form_restore = on_form_restore; + this.on_connect = on_connect; + this.on_adopt = on_adopt; + this.on_disconnect = on_disconnect; + } +} +var default_config = /* @__PURE__ */ new Config2(true, true, false, empty_list, empty_list, empty_list, false, /* @__PURE__ */ new None, /* @__PURE__ */ new None, /* @__PURE__ */ new None, /* @__PURE__ */ new None, /* @__PURE__ */ new None, /* @__PURE__ */ new None); + +// build/dev/javascript/lustre/lustre/internals/equals.ffi.mjs +var isEqual2 = (a, b) => { + if (a === b) { + return true; + } + if (a == null || b == null) { + return false; + } + const type = typeof a; + if (type !== typeof b) { + return false; + } + if (type !== "object") { + return false; + } + const ctor = a.constructor; + if (ctor !== b.constructor) { + return false; + } + if (Array.isArray(a)) { + return areArraysEqual(a, b); + } + return areObjectsEqual(a, b); +}; +var areArraysEqual = (a, b) => { + let index4 = a.length; + if (index4 !== b.length) { + return false; + } + while (index4--) { + if (!isEqual2(a[index4], b[index4])) { + return false; + } + } + return true; +}; +var areObjectsEqual = (a, b) => { + const properties = Object.keys(a); + let index4 = properties.length; + if (Object.keys(b).length !== index4) { + return false; + } + while (index4--) { + const property3 = properties[index4]; + if (!Object.hasOwn(b, property3)) { + return false; + } + if (!isEqual2(a[property3], b[property3])) { + return false; + } + } + return true; +}; + +// build/dev/javascript/lustre/lustre/vdom/diff.mjs +class Diff extends CustomType { + constructor(patch, cache) { + super(); + this.patch = patch; + this.cache = cache; + } +} +class PartialDiff extends CustomType { + constructor(patch, cache, events2) { + super(); + this.patch = patch; + this.cache = cache; + this.events = events2; + } +} + +class AttributeChange extends CustomType { + constructor(added, removed, events2) { + super(); + this.added = added; + this.removed = removed; + this.events = events2; + } +} +function is_controlled(cache, namespace, tag, path) { + if (tag === "input" && namespace === "") { + return has_dispatched_events(cache, path); + } else if (tag === "select" && namespace === "") { + return has_dispatched_events(cache, path); + } else if (tag === "textarea" && namespace === "") { + return has_dispatched_events(cache, path); + } else { + return false; + } +} +function diff_attributes(loop$controlled, loop$path, loop$events, loop$old, loop$new, loop$added, loop$removed) { + while (true) { + let controlled = loop$controlled; + let path = loop$path; + let events2 = loop$events; + let old = loop$old; + let new$5 = loop$new; + let added = loop$added; + let removed = loop$removed; + if (old instanceof Empty) { + if (new$5 instanceof Empty) { + return new AttributeChange(added, removed, events2); + } else { + let $ = new$5.head; + if ($ instanceof Event2) { + let next = $; + let new$1 = new$5.tail; + let name = $.name; + let handler = $.handler; + let events$1 = add_event(events2, path, name, handler); + let added$1 = prepend(next, added); + loop$controlled = controlled; + loop$path = path; + loop$events = events$1; + loop$old = old; + loop$new = new$1; + loop$added = added$1; + loop$removed = removed; + } else { + let next = $; + let new$1 = new$5.tail; + let added$1 = prepend(next, added); + loop$controlled = controlled; + loop$path = path; + loop$events = events2; + loop$old = old; + loop$new = new$1; + loop$added = added$1; + loop$removed = removed; + } + } + } else if (new$5 instanceof Empty) { + let $ = old.head; + if ($ instanceof Event2) { + let prev = $; + let old$1 = old.tail; + let name = $.name; + let events$1 = remove_event(events2, path, name); + let removed$1 = prepend(prev, removed); + loop$controlled = controlled; + loop$path = path; + loop$events = events$1; + loop$old = old$1; + loop$new = new$5; + loop$added = added; + loop$removed = removed$1; + } else { + let prev = $; + let old$1 = old.tail; + let removed$1 = prepend(prev, removed); + loop$controlled = controlled; + loop$path = path; + loop$events = events2; + loop$old = old$1; + loop$new = new$5; + loop$added = added; + loop$removed = removed$1; + } + } else { + let prev = old.head; + let remaining_old = old.tail; + let next = new$5.head; + let remaining_new = new$5.tail; + let $ = compare3(prev, next); + if ($ instanceof Lt) { + if (prev instanceof Event2) { + let name = prev.name; + loop$controlled = controlled; + loop$path = path; + loop$events = remove_event(events2, path, name); + loop$old = remaining_old; + loop$new = new$5; + loop$added = added; + loop$removed = prepend(prev, removed); + } else { + loop$controlled = controlled; + loop$path = path; + loop$events = events2; + loop$old = remaining_old; + loop$new = new$5; + loop$added = added; + loop$removed = prepend(prev, removed); + } + } else if ($ instanceof Eq) { + if (prev instanceof Attribute) { + if (next instanceof Attribute) { + let _block; + let $1 = next.name; + if ($1 === "value") { + _block = controlled || prev.value !== next.value; + } else if ($1 === "checked") { + _block = controlled || prev.value !== next.value; + } else if ($1 === "selected") { + _block = controlled || prev.value !== next.value; + } else { + _block = prev.value !== next.value; + } + let has_changes = _block; + let _block$1; + if (has_changes) { + _block$1 = prepend(next, added); + } else { + _block$1 = added; + } + let added$1 = _block$1; + loop$controlled = controlled; + loop$path = path; + loop$events = events2; + loop$old = remaining_old; + loop$new = remaining_new; + loop$added = added$1; + loop$removed = removed; + } else if (next instanceof Event2) { + let name = next.name; + let handler = next.handler; + loop$controlled = controlled; + loop$path = path; + loop$events = add_event(events2, path, name, handler); + loop$old = remaining_old; + loop$new = remaining_new; + loop$added = prepend(next, added); + loop$removed = prepend(prev, removed); + } else { + loop$controlled = controlled; + loop$path = path; + loop$events = events2; + loop$old = remaining_old; + loop$new = remaining_new; + loop$added = prepend(next, added); + loop$removed = prepend(prev, removed); + } + } else if (prev instanceof Property) { + if (next instanceof Property) { + let _block; + let $1 = next.name; + if ($1 === "scrollLeft") { + _block = true; + } else if ($1 === "scrollRight") { + _block = true; + } else if ($1 === "value") { + _block = controlled || !isEqual2(prev.value, next.value); + } else if ($1 === "checked") { + _block = controlled || !isEqual2(prev.value, next.value); + } else if ($1 === "selected") { + _block = controlled || !isEqual2(prev.value, next.value); + } else { + _block = !isEqual2(prev.value, next.value); + } + let has_changes = _block; + let _block$1; + if (has_changes) { + _block$1 = prepend(next, added); + } else { + _block$1 = added; + } + let added$1 = _block$1; + loop$controlled = controlled; + loop$path = path; + loop$events = events2; + loop$old = remaining_old; + loop$new = remaining_new; + loop$added = added$1; + loop$removed = removed; + } else if (next instanceof Event2) { + let name = next.name; + let handler = next.handler; + loop$controlled = controlled; + loop$path = path; + loop$events = add_event(events2, path, name, handler); + loop$old = remaining_old; + loop$new = remaining_new; + loop$added = prepend(next, added); + loop$removed = prepend(prev, removed); + } else { + loop$controlled = controlled; + loop$path = path; + loop$events = events2; + loop$old = remaining_old; + loop$new = remaining_new; + loop$added = prepend(next, added); + loop$removed = prepend(prev, removed); + } + } else if (next instanceof Event2) { + let name = next.name; + let handler = next.handler; + let has_changes = prev.prevent_default.kind !== next.prevent_default.kind || prev.stop_propagation.kind !== next.stop_propagation.kind || prev.debounce !== next.debounce || prev.throttle !== next.throttle; + let _block; + if (has_changes) { + _block = prepend(next, added); + } else { + _block = added; + } + let added$1 = _block; + loop$controlled = controlled; + loop$path = path; + loop$events = add_event(events2, path, name, handler); + loop$old = remaining_old; + loop$new = remaining_new; + loop$added = added$1; + loop$removed = removed; + } else { + let name = prev.name; + loop$controlled = controlled; + loop$path = path; + loop$events = remove_event(events2, path, name); + loop$old = remaining_old; + loop$new = remaining_new; + loop$added = prepend(next, added); + loop$removed = prepend(prev, removed); + } + } else if (next instanceof Event2) { + let name = next.name; + let handler = next.handler; + loop$controlled = controlled; + loop$path = path; + loop$events = add_event(events2, path, name, handler); + loop$old = old; + loop$new = remaining_new; + loop$added = prepend(next, added); + loop$removed = removed; + } else { + loop$controlled = controlled; + loop$path = path; + loop$events = events2; + loop$old = old; + loop$new = remaining_new; + loop$added = prepend(next, added); + loop$removed = removed; + } + } + } +} +function do_diff(loop$old, loop$old_keyed, loop$new, loop$new_keyed, loop$moved, loop$moved_offset, loop$removed, loop$node_index, loop$patch_index, loop$changes, loop$children, loop$path, loop$cache, loop$events) { + while (true) { + let old = loop$old; + let old_keyed = loop$old_keyed; + let new$5 = loop$new; + let new_keyed = loop$new_keyed; + let moved = loop$moved; + let moved_offset = loop$moved_offset; + let removed = loop$removed; + let node_index = loop$node_index; + let patch_index = loop$patch_index; + let changes = loop$changes; + let children = loop$children; + let path = loop$path; + let cache = loop$cache; + let events2 = loop$events; + if (old instanceof Empty) { + if (new$5 instanceof Empty) { + let patch = new Patch2(patch_index, removed, changes, children); + return new PartialDiff(patch, cache, events2); + } else { + let $ = add_children(cache, events2, path, node_index, new$5); + let cache$1; + let events$1; + cache$1 = $[0]; + events$1 = $[1]; + let insert4 = insert3(new$5, node_index - moved_offset); + let changes$1 = prepend(insert4, changes); + let patch = new Patch2(patch_index, removed, changes$1, children); + return new PartialDiff(patch, cache$1, events$1); + } + } else if (new$5 instanceof Empty) { + let prev = old.head; + let old$1 = old.tail; + let _block; + let $ = prev.key === "" || !has_key(moved, prev.key); + if ($) { + _block = removed + 1; + } else { + _block = removed; + } + let removed$1 = _block; + let events$1 = remove_child(cache, events2, path, node_index, prev); + loop$old = old$1; + loop$old_keyed = old_keyed; + loop$new = new$5; + loop$new_keyed = new_keyed; + loop$moved = moved; + loop$moved_offset = moved_offset; + loop$removed = removed$1; + loop$node_index = node_index; + loop$patch_index = patch_index; + loop$changes = changes; + loop$children = children; + loop$path = path; + loop$cache = cache; + loop$events = events$1; + } else { + let prev = old.head; + let next = new$5.head; + if (prev.key !== next.key) { + let old_remaining = old.tail; + let new_remaining = new$5.tail; + let next_did_exist = has_key(old_keyed, next.key); + let prev_does_exist = has_key(new_keyed, prev.key); + if (prev_does_exist) { + if (next_did_exist) { + let $ = has_key(moved, prev.key); + if ($) { + loop$old = old_remaining; + loop$old_keyed = old_keyed; + loop$new = new$5; + loop$new_keyed = new_keyed; + loop$moved = moved; + loop$moved_offset = moved_offset - 1; + loop$removed = removed; + loop$node_index = node_index; + loop$patch_index = patch_index; + loop$changes = changes; + loop$children = children; + loop$path = path; + loop$cache = cache; + loop$events = events2; + } else { + let match = get2(old_keyed, next.key); + let before = node_index - moved_offset; + let changes$1 = prepend(move(next.key, before), changes); + let moved$1 = insert2(moved, next.key, undefined); + loop$old = prepend(match, old); + loop$old_keyed = old_keyed; + loop$new = new$5; + loop$new_keyed = new_keyed; + loop$moved = moved$1; + loop$moved_offset = moved_offset + 1; + loop$removed = removed; + loop$node_index = node_index; + loop$patch_index = patch_index; + loop$changes = changes$1; + loop$children = children; + loop$path = path; + loop$cache = cache; + loop$events = events2; + } + } else { + let before = node_index - moved_offset; + let $ = add_child(cache, events2, path, node_index, next); + let cache$1; + let events$1; + cache$1 = $[0]; + events$1 = $[1]; + let insert4 = insert3(toList([next]), before); + let changes$1 = prepend(insert4, changes); + loop$old = old; + loop$old_keyed = old_keyed; + loop$new = new_remaining; + loop$new_keyed = new_keyed; + loop$moved = moved; + loop$moved_offset = moved_offset + 1; + loop$removed = removed; + loop$node_index = node_index + 1; + loop$patch_index = patch_index; + loop$changes = changes$1; + loop$children = children; + loop$path = path; + loop$cache = cache$1; + loop$events = events$1; + } + } else if (next_did_exist) { + let index4 = node_index - moved_offset; + let changes$1 = prepend(remove2(index4), changes); + let events$1 = remove_child(cache, events2, path, node_index, prev); + loop$old = old_remaining; + loop$old_keyed = old_keyed; + loop$new = new$5; + loop$new_keyed = new_keyed; + loop$moved = moved; + loop$moved_offset = moved_offset - 1; + loop$removed = removed; + loop$node_index = node_index; + loop$patch_index = patch_index; + loop$changes = changes$1; + loop$children = children; + loop$path = path; + loop$cache = cache; + loop$events = events$1; + } else { + let change = replace2(node_index - moved_offset, next); + let $ = replace_child(cache, events2, path, node_index, prev, next); + let cache$1; + let events$1; + cache$1 = $[0]; + events$1 = $[1]; + loop$old = old_remaining; + loop$old_keyed = old_keyed; + loop$new = new_remaining; + loop$new_keyed = new_keyed; + loop$moved = moved; + loop$moved_offset = moved_offset; + loop$removed = removed; + loop$node_index = node_index + 1; + loop$patch_index = patch_index; + loop$changes = prepend(change, changes); + loop$children = children; + loop$path = path; + loop$cache = cache$1; + loop$events = events$1; + } + } else { + let $ = old.head; + if ($ instanceof Fragment) { + let $1 = new$5.head; + if ($1 instanceof Fragment) { + let prev2 = $; + let old$1 = old.tail; + let next2 = $1; + let new$1 = new$5.tail; + let $2 = do_diff(prev2.children, prev2.keyed_children, next2.children, next2.keyed_children, empty2(), 0, 0, 0, node_index, empty_list, empty_list, add2(path, node_index, next2.key), cache, events2); + let patch; + let cache$1; + let events$1; + patch = $2.patch; + cache$1 = $2.cache; + events$1 = $2.events; + let _block; + let $3 = patch.changes; + if ($3 instanceof Empty) { + let $4 = patch.children; + if ($4 instanceof Empty) { + let $5 = patch.removed; + if ($5 === 0) { + _block = children; + } else { + _block = prepend(patch, children); + } + } else { + _block = prepend(patch, children); + } + } else { + _block = prepend(patch, children); + } + let children$1 = _block; + loop$old = old$1; + loop$old_keyed = old_keyed; + loop$new = new$1; + loop$new_keyed = new_keyed; + loop$moved = moved; + loop$moved_offset = moved_offset; + loop$removed = removed; + loop$node_index = node_index + 1; + loop$patch_index = patch_index; + loop$changes = changes; + loop$children = children$1; + loop$path = path; + loop$cache = cache$1; + loop$events = events$1; + } else { + let prev2 = $; + let old_remaining = old.tail; + let next2 = $1; + let new_remaining = new$5.tail; + let change = replace2(node_index - moved_offset, next2); + let $2 = replace_child(cache, events2, path, node_index, prev2, next2); + let cache$1; + let events$1; + cache$1 = $2[0]; + events$1 = $2[1]; + loop$old = old_remaining; + loop$old_keyed = old_keyed; + loop$new = new_remaining; + loop$new_keyed = new_keyed; + loop$moved = moved; + loop$moved_offset = moved_offset; + loop$removed = removed; + loop$node_index = node_index + 1; + loop$patch_index = patch_index; + loop$changes = prepend(change, changes); + loop$children = children; + loop$path = path; + loop$cache = cache$1; + loop$events = events$1; + } + } else if ($ instanceof Element) { + let $1 = new$5.head; + if ($1 instanceof Element) { + let prev2 = $; + let next2 = $1; + if (prev2.namespace === next2.namespace && prev2.tag === next2.tag) { + let old$1 = old.tail; + let new$1 = new$5.tail; + let child_path = add2(path, node_index, next2.key); + let controlled = is_controlled(cache, next2.namespace, next2.tag, child_path); + let $2 = diff_attributes(controlled, child_path, events2, prev2.attributes, next2.attributes, empty_list, empty_list); + let added_attrs; + let removed_attrs; + let events$1; + added_attrs = $2.added; + removed_attrs = $2.removed; + events$1 = $2.events; + let _block; + if (added_attrs instanceof Empty && removed_attrs instanceof Empty) { + _block = empty_list; + } else { + _block = toList([update(added_attrs, removed_attrs)]); + } + let initial_child_changes = _block; + let $3 = do_diff(prev2.children, prev2.keyed_children, next2.children, next2.keyed_children, empty2(), 0, 0, 0, node_index, initial_child_changes, empty_list, child_path, cache, events$1); + let patch; + let cache$1; + let events$2; + patch = $3.patch; + cache$1 = $3.cache; + events$2 = $3.events; + let _block$1; + let $4 = patch.changes; + if ($4 instanceof Empty) { + let $5 = patch.children; + if ($5 instanceof Empty) { + let $6 = patch.removed; + if ($6 === 0) { + _block$1 = children; + } else { + _block$1 = prepend(patch, children); + } + } else { + _block$1 = prepend(patch, children); + } + } else { + _block$1 = prepend(patch, children); + } + let children$1 = _block$1; + loop$old = old$1; + loop$old_keyed = old_keyed; + loop$new = new$1; + loop$new_keyed = new_keyed; + loop$moved = moved; + loop$moved_offset = moved_offset; + loop$removed = removed; + loop$node_index = node_index + 1; + loop$patch_index = patch_index; + loop$changes = changes; + loop$children = children$1; + loop$path = path; + loop$cache = cache$1; + loop$events = events$2; + } else { + let prev3 = $; + let old_remaining = old.tail; + let next3 = $1; + let new_remaining = new$5.tail; + let change = replace2(node_index - moved_offset, next3); + let $2 = replace_child(cache, events2, path, node_index, prev3, next3); + let cache$1; + let events$1; + cache$1 = $2[0]; + events$1 = $2[1]; + loop$old = old_remaining; + loop$old_keyed = old_keyed; + loop$new = new_remaining; + loop$new_keyed = new_keyed; + loop$moved = moved; + loop$moved_offset = moved_offset; + loop$removed = removed; + loop$node_index = node_index + 1; + loop$patch_index = patch_index; + loop$changes = prepend(change, changes); + loop$children = children; + loop$path = path; + loop$cache = cache$1; + loop$events = events$1; + } + } else { + let prev2 = $; + let old_remaining = old.tail; + let next2 = $1; + let new_remaining = new$5.tail; + let change = replace2(node_index - moved_offset, next2); + let $2 = replace_child(cache, events2, path, node_index, prev2, next2); + let cache$1; + let events$1; + cache$1 = $2[0]; + events$1 = $2[1]; + loop$old = old_remaining; + loop$old_keyed = old_keyed; + loop$new = new_remaining; + loop$new_keyed = new_keyed; + loop$moved = moved; + loop$moved_offset = moved_offset; + loop$removed = removed; + loop$node_index = node_index + 1; + loop$patch_index = patch_index; + loop$changes = prepend(change, changes); + loop$children = children; + loop$path = path; + loop$cache = cache$1; + loop$events = events$1; + } + } else if ($ instanceof Text) { + let $1 = new$5.head; + if ($1 instanceof Text) { + let prev2 = $; + let next2 = $1; + if (prev2.content === next2.content) { + let old$1 = old.tail; + let new$1 = new$5.tail; + loop$old = old$1; + loop$old_keyed = old_keyed; + loop$new = new$1; + loop$new_keyed = new_keyed; + loop$moved = moved; + loop$moved_offset = moved_offset; + loop$removed = removed; + loop$node_index = node_index + 1; + loop$patch_index = patch_index; + loop$changes = changes; + loop$children = children; + loop$path = path; + loop$cache = cache; + loop$events = events2; + } else { + let old$1 = old.tail; + let next3 = $1; + let new$1 = new$5.tail; + let child2 = new$3(node_index, 0, toList([replace_text(next3.content)]), empty_list); + loop$old = old$1; + loop$old_keyed = old_keyed; + loop$new = new$1; + loop$new_keyed = new_keyed; + loop$moved = moved; + loop$moved_offset = moved_offset; + loop$removed = removed; + loop$node_index = node_index + 1; + loop$patch_index = patch_index; + loop$changes = changes; + loop$children = prepend(child2, children); + loop$path = path; + loop$cache = cache; + loop$events = events2; + } + } else { + let prev2 = $; + let old_remaining = old.tail; + let next2 = $1; + let new_remaining = new$5.tail; + let change = replace2(node_index - moved_offset, next2); + let $2 = replace_child(cache, events2, path, node_index, prev2, next2); + let cache$1; + let events$1; + cache$1 = $2[0]; + events$1 = $2[1]; + loop$old = old_remaining; + loop$old_keyed = old_keyed; + loop$new = new_remaining; + loop$new_keyed = new_keyed; + loop$moved = moved; + loop$moved_offset = moved_offset; + loop$removed = removed; + loop$node_index = node_index + 1; + loop$patch_index = patch_index; + loop$changes = prepend(change, changes); + loop$children = children; + loop$path = path; + loop$cache = cache$1; + loop$events = events$1; + } + } else if ($ instanceof UnsafeInnerHtml) { + let $1 = new$5.head; + if ($1 instanceof UnsafeInnerHtml) { + let prev2 = $; + let old$1 = old.tail; + let next2 = $1; + let new$1 = new$5.tail; + let child_path = add2(path, node_index, next2.key); + let $2 = diff_attributes(false, child_path, events2, prev2.attributes, next2.attributes, empty_list, empty_list); + let added_attrs; + let removed_attrs; + let events$1; + added_attrs = $2.added; + removed_attrs = $2.removed; + events$1 = $2.events; + let _block; + if (added_attrs instanceof Empty && removed_attrs instanceof Empty) { + _block = empty_list; + } else { + _block = toList([update(added_attrs, removed_attrs)]); + } + let child_changes = _block; + let _block$1; + let $3 = prev2.inner_html === next2.inner_html; + if ($3) { + _block$1 = child_changes; + } else { + _block$1 = prepend(replace_inner_html(next2.inner_html), child_changes); + } + let child_changes$1 = _block$1; + let _block$2; + if (child_changes$1 instanceof Empty) { + _block$2 = children; + } else { + _block$2 = prepend(new$3(node_index, 0, child_changes$1, toList([])), children); + } + let children$1 = _block$2; + loop$old = old$1; + loop$old_keyed = old_keyed; + loop$new = new$1; + loop$new_keyed = new_keyed; + loop$moved = moved; + loop$moved_offset = moved_offset; + loop$removed = removed; + loop$node_index = node_index + 1; + loop$patch_index = patch_index; + loop$changes = changes; + loop$children = children$1; + loop$path = path; + loop$cache = cache; + loop$events = events$1; + } else { + let prev2 = $; + let old_remaining = old.tail; + let next2 = $1; + let new_remaining = new$5.tail; + let change = replace2(node_index - moved_offset, next2); + let $2 = replace_child(cache, events2, path, node_index, prev2, next2); + let cache$1; + let events$1; + cache$1 = $2[0]; + events$1 = $2[1]; + loop$old = old_remaining; + loop$old_keyed = old_keyed; + loop$new = new_remaining; + loop$new_keyed = new_keyed; + loop$moved = moved; + loop$moved_offset = moved_offset; + loop$removed = removed; + loop$node_index = node_index + 1; + loop$patch_index = patch_index; + loop$changes = prepend(change, changes); + loop$children = children; + loop$path = path; + loop$cache = cache$1; + loop$events = events$1; + } + } else if ($ instanceof Map2) { + let $1 = new$5.head; + if ($1 instanceof Map2) { + let prev2 = $; + let old$1 = old.tail; + let next2 = $1; + let new$1 = new$5.tail; + let child_path = add2(path, node_index, next2.key); + let child_key = child(child_path); + let $2 = do_diff(prepend(prev2.child, empty_list), empty2(), prepend(next2.child, empty_list), empty2(), empty2(), 0, 0, 0, node_index, empty_list, empty_list, subtree(child_path), cache, get_subtree(events2, child_key, prev2.mapper)); + let patch; + let cache$1; + let child_events; + patch = $2.patch; + cache$1 = $2.cache; + child_events = $2.events; + let events$1 = update_subtree(events2, child_key, next2.mapper, child_events); + let _block; + let $3 = patch.changes; + if ($3 instanceof Empty) { + let $4 = patch.children; + if ($4 instanceof Empty) { + let $5 = patch.removed; + if ($5 === 0) { + _block = children; + } else { + _block = prepend(patch, children); + } + } else { + _block = prepend(patch, children); + } + } else { + _block = prepend(patch, children); + } + let children$1 = _block; + loop$old = old$1; + loop$old_keyed = old_keyed; + loop$new = new$1; + loop$new_keyed = new_keyed; + loop$moved = moved; + loop$moved_offset = moved_offset; + loop$removed = removed; + loop$node_index = node_index + 1; + loop$patch_index = patch_index; + loop$changes = changes; + loop$children = children$1; + loop$path = path; + loop$cache = cache$1; + loop$events = events$1; + } else { + let prev2 = $; + let old_remaining = old.tail; + let next2 = $1; + let new_remaining = new$5.tail; + let change = replace2(node_index - moved_offset, next2); + let $2 = replace_child(cache, events2, path, node_index, prev2, next2); + let cache$1; + let events$1; + cache$1 = $2[0]; + events$1 = $2[1]; + loop$old = old_remaining; + loop$old_keyed = old_keyed; + loop$new = new_remaining; + loop$new_keyed = new_keyed; + loop$moved = moved; + loop$moved_offset = moved_offset; + loop$removed = removed; + loop$node_index = node_index + 1; + loop$patch_index = patch_index; + loop$changes = prepend(change, changes); + loop$children = children; + loop$path = path; + loop$cache = cache$1; + loop$events = events$1; + } + } else { + let $1 = new$5.head; + if ($1 instanceof Memo) { + let prev2 = $; + let old$1 = old.tail; + let next2 = $1; + let new$1 = new$5.tail; + let $2 = equal_lists(prev2.dependencies, next2.dependencies); + if ($2) { + let cache$1 = keep_memo(cache, prev2.view, next2.view); + loop$old = old$1; + loop$old_keyed = old_keyed; + loop$new = new$1; + loop$new_keyed = new_keyed; + loop$moved = moved; + loop$moved_offset = moved_offset; + loop$removed = removed; + loop$node_index = node_index + 1; + loop$patch_index = patch_index; + loop$changes = changes; + loop$children = children; + loop$path = path; + loop$cache = cache$1; + loop$events = events2; + } else { + let prev_node = get_old_memo(cache, prev2.view, prev2.view); + let next_node = next2.view(); + let cache$1 = add_memo(cache, next2.view, next_node); + loop$old = prepend(prev_node, old$1); + loop$old_keyed = old_keyed; + loop$new = prepend(next_node, new$1); + loop$new_keyed = new_keyed; + loop$moved = moved; + loop$moved_offset = moved_offset; + loop$removed = removed; + loop$node_index = node_index; + loop$patch_index = patch_index; + loop$changes = changes; + loop$children = children; + loop$path = path; + loop$cache = cache$1; + loop$events = events2; + } + } else { + let prev2 = $; + let old_remaining = old.tail; + let next2 = $1; + let new_remaining = new$5.tail; + let change = replace2(node_index - moved_offset, next2); + let $2 = replace_child(cache, events2, path, node_index, prev2, next2); + let cache$1; + let events$1; + cache$1 = $2[0]; + events$1 = $2[1]; + loop$old = old_remaining; + loop$old_keyed = old_keyed; + loop$new = new_remaining; + loop$new_keyed = new_keyed; + loop$moved = moved; + loop$moved_offset = moved_offset; + loop$removed = removed; + loop$node_index = node_index + 1; + loop$patch_index = patch_index; + loop$changes = prepend(change, changes); + loop$children = children; + loop$path = path; + loop$cache = cache$1; + loop$events = events$1; + } + } + } + } + } +} +function diff(cache, old, new$5) { + let cache$1 = tick(cache); + let $ = do_diff(prepend(old, empty_list), empty2(), prepend(new$5, empty_list), empty2(), empty2(), 0, 0, 0, 0, empty_list, empty_list, root, cache$1, events(cache$1)); + let patch; + let cache$2; + let events2; + patch = $.patch; + cache$2 = $.cache; + events2 = $.events; + return new Diff(patch, update_events(cache$2, events2)); +} + +// build/dev/javascript/lustre/lustre/internals/list.ffi.mjs +var toList2 = (arr) => arr.reduceRight((xs, x) => List$NonEmpty(x, xs), empty_list); +var iterate = (list4, callback) => { + if (Array.isArray(list4)) { + for (let i = 0;i < list4.length; i++) { + callback(list4[i]); + } + } else if (list4) { + for (list4;List$NonEmpty$rest(list4); list4 = List$NonEmpty$rest(list4)) { + callback(List$NonEmpty$first(list4)); + } + } +}; +var append4 = (a, b) => { + if (!List$NonEmpty$rest(a)) { + return b; + } else if (!List$NonEmpty$rest(b)) { + return a; + } else { + return append2(a, b); + } +}; + +// build/dev/javascript/lustre/lustre/internals/constants.ffi.mjs +var NAMESPACE_HTML = "http://www.w3.org/1999/xhtml"; +var ELEMENT_NODE = 1; +var TEXT_NODE = 3; +var COMMENT_NODE = 8; +var SUPPORTS_MOVE_BEFORE = !!globalThis.HTMLElement?.prototype?.moveBefore; + +// build/dev/javascript/lustre/lustre/vdom/reconciler.ffi.mjs +var setTimeout = globalThis.setTimeout; +var clearTimeout = globalThis.clearTimeout; +var createElementNS = (ns, name) => globalThis.document.createElementNS(ns, name); +var createTextNode = (data2) => globalThis.document.createTextNode(data2); +var createComment = (data2) => globalThis.document.createComment(data2); +var createDocumentFragment = () => globalThis.document.createDocumentFragment(); +var insertBefore = (parent, node, reference) => parent.insertBefore(node, reference); +var moveBefore = SUPPORTS_MOVE_BEFORE ? (parent, node, reference) => parent.moveBefore(node, reference) : insertBefore; +var removeChild = (parent, child2) => parent.removeChild(child2); +var getAttribute = (node, name) => node.getAttribute(name); +var setAttribute = (node, name, value2) => node.setAttribute(name, value2); +var removeAttribute = (node, name) => node.removeAttribute(name); +var addEventListener = (node, name, handler, options) => node.addEventListener(name, handler, options); +var removeEventListener = (node, name, handler) => node.removeEventListener(name, handler); +var setInnerHtml = (node, innerHtml) => node.innerHTML = innerHtml; +var setData = (node, data2) => node.data = data2; +var meta = Symbol("lustre"); + +class MetadataNode { + constructor(kind, parent, node, key) { + this.kind = kind; + this.key = key; + this.parent = parent; + this.children = []; + this.node = node; + this.endNode = null; + this.handlers = new Map; + this.throttles = new Map; + this.debouncers = new Map; + } + get isVirtual() { + return this.kind === fragment_kind || this.kind === map_kind; + } + get parentNode() { + return this.isVirtual ? this.node.parentNode : this.node; + } +} +var insertMetadataChild = (kind, parent, node, index4, key) => { + const child2 = new MetadataNode(kind, parent, node, key); + node[meta] = child2; + parent?.children.splice(index4, 0, child2); + return child2; +}; +var getPath = (node) => { + let path = ""; + for (let current = node[meta];current.parent; current = current.parent) { + const separator = current.parent && current.parent.kind === map_kind ? separator_subtree : separator_element; + if (current.key) { + path = `${separator}${current.key}${path}`; + } else { + const index4 = current.parent.children.indexOf(current); + path = `${separator}${index4}${path}`; + } + } + return path.slice(1); +}; + +class Reconciler { + #root = null; + #decodeEvent; + #dispatch; + #debug = false; + constructor(root2, decodeEvent, dispatch2, { debug = false } = {}) { + this.#root = root2; + this.#decodeEvent = decodeEvent; + this.#dispatch = dispatch2; + this.#debug = debug; + } + mount(vdom) { + insertMetadataChild(element_kind, null, this.#root, 0, null); + this.#insertChild(this.#root, null, this.#root[meta], 0, vdom); + } + push(patch, memos2 = null) { + this.#memos = memos2; + this.#stack.push({ node: this.#root[meta], patch }); + this.#reconcile(); + } + #memos; + #stack = []; + #reconcile() { + const stack = this.#stack; + while (stack.length) { + const { node, patch } = stack.pop(); + const { children: childNodes } = node; + const { changes, removed, children: childPatches } = patch; + iterate(changes, (change) => this.#patch(node, change)); + if (removed) { + this.#removeChildren(node, childNodes.length - removed, removed); + } + iterate(childPatches, (childPatch) => { + const child2 = childNodes[childPatch.index | 0]; + this.#stack.push({ node: child2, patch: childPatch }); + }); + } + } + #patch(node, change) { + switch (change.kind) { + case replace_text_kind: + this.#replaceText(node, change); + break; + case replace_inner_html_kind: + this.#replaceInnerHtml(node, change); + break; + case update_kind: + this.#update(node, change); + break; + case move_kind: + this.#move(node, change); + break; + case remove_kind: + this.#remove(node, change); + break; + case replace_kind: + this.#replace(node, change); + break; + case insert_kind: + this.#insert(node, change); + break; + } + } + #insert(parent, { children, before }) { + const fragment2 = createDocumentFragment(); + const beforeEl = this.#getReference(parent, before); + this.#insertChildren(fragment2, null, parent, before | 0, children); + insertBefore(parent.parentNode, fragment2, beforeEl); + } + #replace(parent, { index: index4, with: child2 }) { + this.#removeChildren(parent, index4 | 0, 1); + const beforeEl = this.#getReference(parent, index4); + this.#insertChild(parent.parentNode, beforeEl, parent, index4 | 0, child2); + } + #getReference(node, index4) { + index4 = index4 | 0; + const { children } = node; + const childCount = children.length; + if (index4 < childCount) + return children[index4].node; + if (node.endNode) + return node.endNode; + if (!node.isVirtual) + return null; + while (node.isVirtual && node.children.length) { + if (node.endNode) + return node.endNode.nextSibling; + node = node.children[node.children.length - 1]; + } + return node.node.nextSibling; + } + #move(parent, { key, before }) { + before = before | 0; + const { children, parentNode } = parent; + const beforeEl = children[before].node; + let prev = children[before]; + for (let i = before + 1;i < children.length; ++i) { + const next = children[i]; + children[i] = prev; + prev = next; + if (next.key === key) { + children[before] = next; + break; + } + } + this.#moveChild(parentNode, prev, beforeEl); + } + #moveChildren(domParent, children, beforeEl) { + for (let i = 0;i < children.length; ++i) { + this.#moveChild(domParent, children[i], beforeEl); + } + } + #moveChild(domParent, child2, beforeEl) { + moveBefore(domParent, child2.node, beforeEl); + if (child2.isVirtual) { + this.#moveChildren(domParent, child2.children, beforeEl); + } + if (child2.endNode) { + moveBefore(domParent, child2.endNode, beforeEl); + } + } + #remove(parent, { index: index4 }) { + this.#removeChildren(parent, index4, 1); + } + #removeChildren(parent, index4, count) { + const { children, parentNode } = parent; + const deleted = children.splice(index4, count); + for (let i = 0;i < deleted.length; ++i) { + const child2 = deleted[i]; + const { node, endNode, isVirtual, children: nestedChildren } = child2; + removeChild(parentNode, node); + if (endNode) { + removeChild(parentNode, endNode); + } + this.#removeDebouncers(child2); + if (isVirtual) { + deleted.push(...nestedChildren); + } + } + } + #removeDebouncers(node) { + const { debouncers, children } = node; + for (const { timeout } of debouncers.values()) { + if (timeout) { + clearTimeout(timeout); + } + } + debouncers.clear(); + iterate(children, (child2) => this.#removeDebouncers(child2)); + } + #update({ node, handlers, throttles, debouncers }, { added, removed }) { + iterate(removed, ({ name }) => { + if (handlers.delete(name)) { + removeEventListener(node, name, handleEvent); + this.#updateDebounceThrottle(throttles, name, 0); + this.#updateDebounceThrottle(debouncers, name, 0); + } else { + removeAttribute(node, name); + SYNCED_ATTRIBUTES[name]?.removed?.(node, name); + } + }); + iterate(added, (attribute3) => this.#createAttribute(node, attribute3)); + } + #replaceText({ node }, { content }) { + setData(node, content ?? ""); + } + #replaceInnerHtml({ node }, { inner_html }) { + setInnerHtml(node, inner_html ?? ""); + } + #insertChildren(domParent, beforeEl, metaParent, index4, children) { + iterate(children, (child2) => this.#insertChild(domParent, beforeEl, metaParent, index4++, child2)); + } + #insertChild(domParent, beforeEl, metaParent, index4, vnode) { + switch (vnode.kind) { + case element_kind: { + const node = this.#createElement(metaParent, index4, vnode); + this.#insertChildren(node, null, node[meta], 0, vnode.children); + insertBefore(domParent, node, beforeEl); + break; + } + case text_kind: { + const node = this.#createTextNode(metaParent, index4, vnode); + insertBefore(domParent, node, beforeEl); + break; + } + case fragment_kind: { + const marker = "lustre:fragment"; + const head = this.#createHead(marker, metaParent, index4, vnode); + insertBefore(domParent, head, beforeEl); + this.#insertChildren(domParent, beforeEl, head[meta], 0, vnode.children); + if (this.#debug) { + head[meta].endNode = createComment(` /${marker} `); + insertBefore(domParent, head[meta].endNode, beforeEl); + } + break; + } + case unsafe_inner_html_kind: { + const node = this.#createElement(metaParent, index4, vnode); + this.#replaceInnerHtml({ node }, vnode); + insertBefore(domParent, node, beforeEl); + break; + } + case map_kind: { + const head = this.#createHead("lustre:map", metaParent, index4, vnode); + insertBefore(domParent, head, beforeEl); + this.#insertChild(domParent, beforeEl, head[meta], 0, vnode.child); + break; + } + case memo_kind: { + const child2 = this.#memos?.get(vnode.view) ?? vnode.view(); + this.#insertChild(domParent, beforeEl, metaParent, index4, child2); + break; + } + } + } + #createElement(parent, index4, { kind, key, tag, namespace, attributes }) { + const node = createElementNS(namespace || NAMESPACE_HTML, tag); + insertMetadataChild(kind, parent, node, index4, key); + if (this.#debug && key) { + setAttribute(node, "data-lustre-key", key); + } + iterate(attributes, (attribute3) => this.#createAttribute(node, attribute3)); + return node; + } + #createTextNode(parent, index4, { kind, key, content }) { + const node = createTextNode(content ?? ""); + insertMetadataChild(kind, parent, node, index4, key); + return node; + } + #createHead(marker, parent, index4, { kind, key }) { + const node = this.#debug ? createComment(markerComment(marker, key)) : createTextNode(""); + insertMetadataChild(kind, parent, node, index4, key); + return node; + } + #createAttribute(node, attribute3) { + const { debouncers, handlers, throttles } = node[meta]; + const { + kind, + name, + value: value2, + prevent_default: prevent, + debounce: debounceDelay, + throttle: throttleDelay + } = attribute3; + switch (kind) { + case attribute_kind: { + const valueOrDefault = value2 ?? ""; + if (name === "virtual:defaultValue") { + node.defaultValue = valueOrDefault; + return; + } else if (name === "virtual:defaultChecked") { + node.defaultChecked = true; + return; + } else if (name === "virtual:defaultSelected") { + node.defaultSelected = true; + return; + } + if (valueOrDefault !== getAttribute(node, name)) { + setAttribute(node, name, valueOrDefault); + } + SYNCED_ATTRIBUTES[name]?.added?.(node, valueOrDefault); + break; + } + case property_kind: + node[name] = value2; + break; + case event_kind: { + if (handlers.has(name)) { + removeEventListener(node, name, handleEvent); + } + const passive = prevent.kind === never_kind; + addEventListener(node, name, handleEvent, { passive }); + this.#updateDebounceThrottle(throttles, name, throttleDelay); + this.#updateDebounceThrottle(debouncers, name, debounceDelay); + handlers.set(name, (event3) => this.#handleEvent(attribute3, event3)); + break; + } + } + } + #updateDebounceThrottle(map8, name, delay) { + const debounceOrThrottle = map8.get(name); + if (delay > 0) { + if (debounceOrThrottle) { + debounceOrThrottle.delay = delay; + } else { + map8.set(name, { delay }); + } + } else if (debounceOrThrottle) { + const { timeout } = debounceOrThrottle; + if (timeout) { + clearTimeout(timeout); + } + map8.delete(name); + } + } + #handleEvent(attribute3, event3) { + const { currentTarget, type } = event3; + const { debouncers, throttles } = currentTarget[meta]; + const path = getPath(currentTarget); + const { + prevent_default: prevent, + stop_propagation: stop, + include + } = attribute3; + if (prevent.kind === always_kind) + event3.preventDefault(); + if (stop.kind === always_kind) + event3.stopPropagation(); + if (type === "submit") { + event3.detail ??= {}; + event3.detail.formData = [ + ...new FormData(event3.target, event3.submitter).entries() + ]; + } + const data2 = this.#decodeEvent(event3, path, type, include); + const throttle = throttles.get(type); + if (throttle) { + const now = Date.now(); + const last = throttle.last || 0; + if (now > last + throttle.delay) { + throttle.last = now; + throttle.lastEvent = event3; + this.#dispatch(event3, data2); + } + } + const debounce = debouncers.get(type); + if (debounce) { + clearTimeout(debounce.timeout); + debounce.timeout = setTimeout(() => { + if (event3 === throttles.get(type)?.lastEvent) + return; + this.#dispatch(event3, data2); + }, debounce.delay); + } + if (!throttle && !debounce) { + this.#dispatch(event3, data2); + } + } +} +var markerComment = (marker, key) => { + if (key) { + return ` ${marker} key="${escape2(key)}" `; + } else { + return ` ${marker} `; + } +}; +var handleEvent = (event3) => { + const { currentTarget, type } = event3; + const handler = currentTarget[meta].handlers.get(type); + handler(event3); +}; +var syncedBooleanAttribute = (name) => { + return { + added(node) { + node[name] = true; + }, + removed(node) { + node[name] = false; + } + }; +}; +var syncedAttribute = (name) => { + return { + added(node, value2) { + node[name] = value2; + } + }; +}; +var SYNCED_ATTRIBUTES = { + checked: syncedBooleanAttribute("checked"), + selected: syncedBooleanAttribute("selected"), + value: syncedAttribute("value"), + autofocus: { + added(node) { + queueMicrotask(() => { + node.focus?.(); + }); + } + }, + autoplay: { + added(node) { + try { + node.play?.(); + } catch (e) { + console.error(e); + } + } + } +}; + +// build/dev/javascript/lustre/lustre/element/keyed.mjs +function do_extract_keyed_children(loop$key_children_pairs, loop$keyed_children, loop$children) { + while (true) { + let key_children_pairs = loop$key_children_pairs; + let keyed_children = loop$keyed_children; + let children = loop$children; + if (key_children_pairs instanceof Empty) { + return [keyed_children, reverse(children)]; + } else { + let rest = key_children_pairs.tail; + let key = key_children_pairs.head[0]; + let element$1 = key_children_pairs.head[1]; + let keyed_element = to_keyed(key, element$1); + let _block; + if (key === "") { + _block = keyed_children; + } else { + _block = insert2(keyed_children, key, keyed_element); + } + let keyed_children$1 = _block; + let children$1 = prepend(keyed_element, children); + loop$key_children_pairs = rest; + loop$keyed_children = keyed_children$1; + loop$children = children$1; + } + } +} +function extract_keyed_children(children) { + return do_extract_keyed_children(children, empty2(), empty_list); +} +function element3(tag, attributes, children) { + let $ = extract_keyed_children(children); + let keyed_children; + let children$1; + keyed_children = $[0]; + children$1 = $[1]; + return element("", "", tag, attributes, children$1, keyed_children, false, is_void_html_element(tag, "")); +} +function namespaced2(namespace, tag, attributes, children) { + let $ = extract_keyed_children(children); + let keyed_children; + let children$1; + keyed_children = $[0]; + children$1 = $[1]; + return element("", namespace, tag, attributes, children$1, keyed_children, false, is_void_html_element(tag, namespace)); +} +function fragment2(children) { + let $ = extract_keyed_children(children); + let keyed_children; + let children$1; + keyed_children = $[0]; + children$1 = $[1]; + return fragment("", children$1, keyed_children); +} + +// build/dev/javascript/lustre/lustre/vdom/virtualise.ffi.mjs +var virtualise = (root2) => { + const rootMeta = insertMetadataChild(element_kind, null, root2, 0, null); + for (let child2 = root2.firstChild;child2; child2 = child2.nextSibling) { + const result = virtualiseChild(rootMeta, root2, child2, 0); + if (result) + return result.vnode; + } + const placeholder2 = globalThis.document.createTextNode(""); + insertMetadataChild(text_kind, rootMeta, placeholder2, 0, null); + root2.insertBefore(placeholder2, root2.firstChild); + return none2(); +}; +var virtualiseChild = (meta2, domParent, child2, index4) => { + if (child2.nodeType === COMMENT_NODE) { + const data2 = child2.data.trim(); + if (data2.startsWith("lustre:fragment")) { + return virtualiseFragment(meta2, domParent, child2, index4); + } + if (data2.startsWith("lustre:map")) { + return virtualiseMap(meta2, domParent, child2, index4); + } + if (data2.startsWith("lustre:memo")) { + return virtualiseMemo(meta2, domParent, child2, index4); + } + return null; + } + if (child2.nodeType === ELEMENT_NODE) { + return virtualiseElement(meta2, child2, index4); + } + if (child2.nodeType === TEXT_NODE) { + return virtualiseText(meta2, child2, index4); + } + return null; +}; +var virtualiseElement = (metaParent, node, index4) => { + const key = node.getAttribute("data-lustre-key") ?? ""; + if (key) { + node.removeAttribute("data-lustre-key"); + } + const meta2 = insertMetadataChild(element_kind, metaParent, node, index4, key); + const tag = node.localName; + const namespace = node.namespaceURI; + const isHtmlElement = !namespace || namespace === NAMESPACE_HTML; + if (isHtmlElement && INPUT_ELEMENTS.includes(tag)) { + virtualiseInputEvents(tag, node); + } + const attributes = virtualiseAttributes(node); + const children = []; + for (let childNode = node.firstChild;childNode; ) { + const child2 = virtualiseChild(meta2, node, childNode, children.length); + if (child2) { + children.push([child2.key, child2.vnode]); + childNode = child2.next; + } else { + childNode = childNode.nextSibling; + } + } + const vnode = isHtmlElement ? element3(tag, attributes, toList3(children)) : namespaced2(namespace, tag, attributes, toList3(children)); + return childResult(key, vnode, node.nextSibling); +}; +var virtualiseText = (meta2, node, index4) => { + insertMetadataChild(text_kind, meta2, node, index4, null); + return childResult("", text2(node.data), node.nextSibling); +}; +var virtualiseFragment = (metaParent, domParent, node, index4) => { + const key = parseKey(node.data); + const meta2 = insertMetadataChild(fragment_kind, metaParent, node, index4, key); + const children = []; + node = node.nextSibling; + while (node && (node.nodeType !== COMMENT_NODE || node.data.trim() !== "/lustre:fragment")) { + const child2 = virtualiseChild(meta2, domParent, node, children.length); + if (child2) { + children.push([child2.key, child2.vnode]); + node = child2.next; + } else { + node = node.nextSibling; + } + } + meta2.endNode = node; + const vnode = fragment2(toList3(children)); + return childResult(key, vnode, node?.nextSibling); +}; +var virtualiseMap = (metaParent, domParent, node, index4) => { + const key = parseKey(node.data); + const meta2 = insertMetadataChild(map_kind, metaParent, node, index4, key); + const child2 = virtualiseNextChild(meta2, domParent, node, 0); + if (!child2) + return null; + const vnode = map7(child2.vnode, (x) => x); + return childResult(key, vnode, child2.next); +}; +var virtualiseMemo = (meta2, domParent, node, index4) => { + const key = parseKey(node.data); + const child2 = virtualiseNextChild(meta2, domParent, node, index4); + if (!child2) + return null; + domParent.removeChild(node); + const vnode = memo2(toList3([ref({})]), () => child2.vnode); + return childResult(key, vnode, child2.next); +}; +var virtualiseNextChild = (meta2, domParent, node, index4) => { + while (true) { + node = node.nextSibling; + if (!node) + return null; + const child2 = virtualiseChild(meta2, domParent, node, index4); + if (child2) + return child2; + } +}; +var childResult = (key, vnode, next) => { + return { key, vnode, next }; +}; +var virtualiseAttributes = (node) => { + const attributes = []; + for (let i = 0;i < node.attributes.length; i++) { + const attr = node.attributes[i]; + if (attr.name !== "xmlns") { + attributes.push(attribute2(attr.localName, attr.value)); + } + } + return toList3(attributes); +}; +var INPUT_ELEMENTS = ["input", "select", "textarea"]; +var virtualiseInputEvents = (tag, node) => { + const value2 = node.value; + const checked = node.checked; + if (tag === "input" && node.type === "checkbox" && !checked) + return; + if (tag === "input" && node.type === "radio" && !checked) + return; + if (node.type !== "checkbox" && node.type !== "radio" && !value2) + return; + queueMicrotask(() => { + node.value = value2; + node.checked = checked; + node.dispatchEvent(new Event("input", { bubbles: true })); + node.dispatchEvent(new Event("change", { bubbles: true })); + if (globalThis.document.activeElement !== node) { + node.dispatchEvent(new Event("blur", { bubbles: true })); + } + }); +}; +var parseKey = (data2) => { + const keyMatch = data2.match(/key="([^"]*)"/); + if (!keyMatch) + return ""; + return unescapeKey(keyMatch[1]); +}; +var unescapeKey = (key) => { + return key.replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/&/g, "&").replace(/'/g, "'"); +}; +var toList3 = (arr) => arr.reduceRight((xs, x) => List$NonEmpty(x, xs), empty_list); + +// build/dev/javascript/lustre/lustre/runtime/client/runtime.ffi.mjs +var is_browser = () => !!globalThis.document; +class Runtime { + constructor(root2, [model, effects], view, update2, options) { + this.root = root2; + this.#model = model; + this.#view = view; + this.#update = update2; + this.root.addEventListener("context-request", (event3) => { + if (!(event3.context && event3.callback)) + return; + if (!this.#contexts.has(event3.context)) + return; + event3.stopImmediatePropagation(); + const context = this.#contexts.get(event3.context); + if (event3.subscribe) { + const unsubscribe = () => { + context.subscribers = context.subscribers.filter((subscriber) => subscriber !== event3.callback); + }; + context.subscribers.push([event3.callback, unsubscribe]); + event3.callback(context.value, unsubscribe); + } else { + event3.callback(context.value); + } + }); + const decodeEvent = (event3, path, name) => decode2(this.#cache, path, name, event3); + const dispatch2 = (event3, data2) => { + const [cache, result] = dispatch(this.#cache, data2); + this.#cache = cache; + if (Result$isOk(result)) { + const handler = Result$Ok$0(result); + if (handler.stop_propagation) + event3.stopPropagation(); + if (handler.prevent_default) + event3.preventDefault(); + this.dispatch(handler.message, false); + } + }; + this.#reconciler = new Reconciler(this.root, decodeEvent, dispatch2, options); + this.#vdom = virtualise(this.root); + this.#cache = new$4(); + this.#handleEffects(effects); + this.#render(); + } + root = null; + dispatch(msg, shouldFlush = false) { + if (this.#shouldQueue) { + this.#queue.push(msg); + } else { + const [model, effects] = this.#update(this.#model, msg); + this.#model = model; + this.#tick(effects, shouldFlush); + } + } + emit(event3, data2) { + const target = this.root.host ?? this.root; + target.dispatchEvent(new CustomEvent(event3, { + detail: data2, + bubbles: true, + composed: true + })); + } + provide(key, value2) { + if (!this.#contexts.has(key)) { + this.#contexts.set(key, { value: value2, subscribers: [] }); + } else { + const context = this.#contexts.get(key); + if (isEqual2(context.value, value2)) { + return; + } + context.value = value2; + for (let i = context.subscribers.length - 1;i >= 0; i--) { + const [subscriber, unsubscribe] = context.subscribers[i]; + if (!subscriber) { + context.subscribers.splice(i, 1); + continue; + } + subscriber(value2, unsubscribe); + } + } + } + #model; + #view; + #update; + #vdom; + #cache; + #reconciler; + #contexts = new Map; + #shouldQueue = false; + #queue = []; + #beforePaint = empty_list; + #afterPaint = empty_list; + #renderTimer = null; + #actions = { + dispatch: (msg) => this.dispatch(msg), + emit: (event3, data2) => this.emit(event3, data2), + select: () => {}, + root: () => this.root, + provide: (key, value2) => this.provide(key, value2) + }; + #tick(effects, shouldFlush = false) { + this.#handleEffects(effects); + if (!this.#renderTimer) { + if (shouldFlush) { + this.#renderTimer = "sync"; + queueMicrotask(() => this.#render()); + } else { + this.#renderTimer = window.requestAnimationFrame(() => this.#render()); + } + } + } + #handleEffects(effects) { + this.#shouldQueue = true; + while (true) { + iterate(effects.synchronous, (effect) => effect(this.#actions)); + this.#beforePaint = append4(this.#beforePaint, effects.before_paint); + this.#afterPaint = append4(this.#afterPaint, effects.after_paint); + if (!this.#queue.length) + break; + const msg = this.#queue.shift(); + [this.#model, effects] = this.#update(this.#model, msg); + } + this.#shouldQueue = false; + } + #render() { + this.#renderTimer = null; + const next = this.#view(this.#model); + const { patch, cache } = diff(this.#cache, this.#vdom, next); + this.#cache = cache; + this.#vdom = next; + this.#reconciler.push(patch, memos(cache)); + if (List$isNonEmpty(this.#beforePaint)) { + const effects = makeEffect(this.#beforePaint); + this.#beforePaint = empty_list; + queueMicrotask(() => { + this.#tick(effects, true); + }); + } + if (List$isNonEmpty(this.#afterPaint)) { + const effects = makeEffect(this.#afterPaint); + this.#afterPaint = empty_list; + window.requestAnimationFrame(() => this.#tick(effects, true)); + } + } +} +function makeEffect(synchronous) { + return { + synchronous, + after_paint: empty_list, + before_paint: empty_list + }; +} +var copiedStyleSheets = new WeakMap; + +// build/dev/javascript/lustre/lustre/runtime/client/spa.ffi.mjs +class Spa { + #runtime; + constructor(root2, [init, effects], update2, view) { + this.#runtime = new Runtime(root2, [init, effects], view, update2); + } + send(message) { + if (Message$isEffectDispatchedMessage(message)) { + this.dispatch(message.message, false); + } else if (Message$isEffectEmitEvent(message)) { + this.emit(message.name, message.data); + } else if (Message$isSystemRequestedShutdown(message)) {} + } + dispatch(msg) { + this.#runtime.dispatch(msg); + } + emit(event3, data2) { + this.#runtime.emit(event3, data2); + } +} +var start = ({ init, update: update2, view }, selector, flags) => { + if (!is_browser()) + return Result$Error(Error$NotABrowser()); + const root2 = selector instanceof HTMLElement ? selector : globalThis.document.querySelector(selector); + if (!root2) + return Result$Error(Error$ElementNotFound(selector)); + return Result$Ok(new Spa(root2, init(flags), update2, view)); +}; + +// build/dev/javascript/lustre/lustre/runtime/server/runtime.ffi.mjs +class Runtime2 { + #model; + #update; + #view; + #config; + #vdom; + #cache; + #providers = make(); + #callbacks = /* @__PURE__ */ new Set; + constructor(_, init, update2, view, config2, start_arguments) { + const [model, effects] = init(start_arguments); + this.#model = model; + this.#update = update2; + this.#view = view; + this.#config = config2; + this.#vdom = this.#view(this.#model); + this.#cache = from_node(this.#vdom); + this.#handle_effect(effects); + } + send(msg) { + if (Message$isClientDispatchedMessage(msg)) { + const { message } = msg; + const next = this.#handle_client_message(message); + const diff2 = diff(this.#cache, this.#vdom, next); + this.#vdom = next; + this.#cache = diff2.cache; + this.broadcast(reconcile(diff2.patch, memos(diff2.cache))); + } else if (Message$isClientRegisteredCallback(msg)) { + const { callback } = msg; + this.#callbacks.add(callback); + callback(mount(this.#config.open_shadow_root, this.#config.adopt_styles, keys(this.#config.attributes), keys(this.#config.properties), keys(this.#config.contexts), this.#providers, this.#vdom, memos(this.#cache))); + if (Option$isSome(config.on_connect)) { + this.#dispatch(Option$Some$0(config.on_connect)); + } + } else if (Message$isClientDeregisteredCallback(msg)) { + const { callback } = msg; + this.#callbacks.delete(callback); + if (Option$isSome(config.on_disconnect)) { + this.#dispatch(Option$Some$0(config.on_disconnect)); + } + } else if (Message$isEffectDispatchedMessage(msg)) { + const { message } = msg; + const [model, effect] = this.#update(this.#model, message); + const next = this.#view(model); + const diff2 = diff(this.#cache, this.#vdom, next); + this.#handle_effect(effect); + this.#model = model; + this.#vdom = next; + this.#cache = diff2.cache; + this.broadcast(reconcile(diff2.patch, memos(diff2.cache))); + } else if (Message$isEffectEmitEvent(msg)) { + const { name, data: data2 } = msg; + this.broadcast(emit(name, data2)); + } else if (Message$isEffectProvidedValue(msg)) { + const { key, value: value2 } = msg; + const existing = get(this.#providers, key); + if (Result$isOk(existing) && isEqual2(Result$Ok$0(existing), value2)) { + return; + } + this.#providers = insert(this.#providers, key, value2); + this.broadcast(provide(key, value2)); + } else if (Message$isSystemRequestedShutdown(msg)) { + this.#model = null; + this.#update = null; + this.#view = null; + this.#config = null; + this.#vdom = null; + this.#cache = null; + this.#providers = null; + this.#callbacks.clear(); + } + } + broadcast(msg) { + for (const callback of this.#callbacks) { + callback(msg); + } + } + #handle_client_message(msg) { + if (ServerMessage$isBatch(msg)) { + const { messages } = msg; + let model = this.#model; + let effect = none(); + for (let list4 = messages;List$NonEmpty$rest(list4); list4 = List$NonEmpty$rest(list4)) { + const result = this.#handle_client_message(List$NonEmpty$first(list4)); + if (Result$isOk(result)) { + model = Result$Ok$0(result)[0]; + effect = batch(toList2([effect, Result$Ok$0(result)[1]])); + break; + } + } + this.#handle_effect(effect); + this.#model = model; + return this.#view(model); + } else if (ServerMessage$isAttributeChanged(msg)) { + const { name, value: value2 } = msg; + const result = this.#handle_attribute_change(name, value2); + if (!Result$isOk(result)) { + return this.#vdom; + } + return this.#dispatch(Result$Ok$0(result)); + } else if (ServerMessage$isPropertyChanged(msg)) { + const { name, value: value2 } = msg; + const result = this.#handle_properties_change(name, value2); + if (!Result$isOk(result)) { + return this.#vdom; + } + return this.#dispatch(Result$Ok$0(result)); + } else if (ServerMessage$isEventFired(msg)) { + const { path, name, event: event3 } = msg; + const [cache, result] = handle(this.#cache, path, name, event3); + this.#cache = cache; + if (!Result$isOk(result)) { + return this.#vdom; + } + const { message } = Result$Ok$0(result); + return this.#dispatch(message); + } else if (ServerMessage$isContextProvided(msg)) { + const { key, value: value2 } = msg; + let result = get(this.#config.contexts, key); + if (!Result$isOk(result)) { + return this.#vdom; + } + result = run(value2, Result$Ok$0(result)); + if (!Result$isOk(result)) { + return this.#vdom; + } + return this.#dispatch(Result$Ok$0(result)); + } + } + #dispatch(msg) { + const [model, effects] = this.#update(this.#model, msg); + this.#handle_effect(effects); + this.#model = model; + return this.#view(this.#model); + } + #handle_attribute_change(name, value2) { + const result = get(this.#config.attributes, name); + if (!Result$isOk(result)) { + return result; + } + return Result$Ok$0(result)(value2); + } + #handle_properties_change(name, value2) { + const result = get(this.#config.properties, name); + if (!Result$isOk(result)) { + return result; + } + return Result$Ok$0(result)(value2); + } + #handle_effect(effect) { + const dispatch2 = (message) => this.send(Message$EffectDispatchedMessage(message)); + const emit2 = (name, data2) => this.send(Message$EffectEmitEvent(name, data2)); + const select = () => { + return; + }; + const internals = () => { + return; + }; + const provide2 = (key, value2) => this.send(Message$EffectProvidedValue(key, value2)); + globalThis.queueMicrotask(() => { + perform(effect, dispatch2, emit2, select, internals, provide2); + }); + } +} + +// build/dev/javascript/lustre/lustre.mjs +class ElementNotFound extends CustomType { + constructor(selector) { + super(); + this.selector = selector; + } +} +var Error$ElementNotFound = (selector) => new ElementNotFound(selector); +class NotABrowser extends CustomType { +} +var Error$NotABrowser = () => new NotABrowser; +function application(init, update2, view) { + return new App(new None, init, update2, view, default_config); +} +function start4(app, selector, arguments$) { + return guard(!is_browser(), new Error(new NotABrowser), () => { + return start(app, selector, arguments$); + }); +} + +// build/dev/javascript/lustre/lustre/event.mjs +function on(name, handler) { + return event(name, map3(handler, (msg) => { + return new Handler(false, false, msg); + }), empty_list, never, never, 0, 0); +} +function on_click(msg) { + return on("click", success(msg)); +} +function on_input(msg) { + return on("input", subfield(toList(["target", "value"]), string2, (value2) => { + return success(msg(value2)); + })); +} + +// build/dev/javascript/gleam_stdlib/gleam/uri.mjs +class Uri extends CustomType { + constructor(scheme, userinfo, host, port, path, query, fragment3) { + super(); + this.scheme = scheme; + this.userinfo = userinfo; + this.host = host; + this.port = port; + this.path = path; + this.query = query; + this.fragment = fragment3; + } +} +var empty3 = /* @__PURE__ */ new Uri(/* @__PURE__ */ new None, /* @__PURE__ */ new None, /* @__PURE__ */ new None, /* @__PURE__ */ new None, "", /* @__PURE__ */ new None, /* @__PURE__ */ new None); +function is_valid_host_within_brackets_char(char) { + return 48 >= char && char <= 57 || 65 >= char && char <= 90 || 97 >= char && char <= 122 || char === 58 || char === 46; +} +function parse_fragment(rest, pieces) { + return new Ok(new Uri(pieces.scheme, pieces.userinfo, pieces.host, pieces.port, pieces.path, pieces.query, new Some(rest))); +} +function parse_query_with_question_mark_loop(loop$original, loop$uri_string, loop$pieces, loop$size) { + while (true) { + let original = loop$original; + let uri_string = loop$uri_string; + let pieces = loop$pieces; + let size3 = loop$size; + if (uri_string.startsWith("#")) { + if (size3 === 0) { + let rest = uri_string.slice(1); + return parse_fragment(rest, pieces); + } else { + let rest = uri_string.slice(1); + let query = string_codeunit_slice(original, 0, size3); + let pieces$1 = new Uri(pieces.scheme, pieces.userinfo, pieces.host, pieces.port, pieces.path, new Some(query), pieces.fragment); + return parse_fragment(rest, pieces$1); + } + } else if (uri_string === "") { + return new Ok(new Uri(pieces.scheme, pieces.userinfo, pieces.host, pieces.port, pieces.path, new Some(original), pieces.fragment)); + } else { + let $ = pop_codeunit(uri_string); + let rest; + rest = $[1]; + loop$original = original; + loop$uri_string = rest; + loop$pieces = pieces; + loop$size = size3 + 1; + } + } +} +function parse_query_with_question_mark(uri_string, pieces) { + return parse_query_with_question_mark_loop(uri_string, uri_string, pieces, 0); +} +function parse_path_loop(loop$original, loop$uri_string, loop$pieces, loop$size) { + while (true) { + let original = loop$original; + let uri_string = loop$uri_string; + let pieces = loop$pieces; + let size3 = loop$size; + if (uri_string.startsWith("?")) { + let rest = uri_string.slice(1); + let path = string_codeunit_slice(original, 0, size3); + let pieces$1 = new Uri(pieces.scheme, pieces.userinfo, pieces.host, pieces.port, path, pieces.query, pieces.fragment); + return parse_query_with_question_mark(rest, pieces$1); + } else if (uri_string.startsWith("#")) { + let rest = uri_string.slice(1); + let path = string_codeunit_slice(original, 0, size3); + let pieces$1 = new Uri(pieces.scheme, pieces.userinfo, pieces.host, pieces.port, path, pieces.query, pieces.fragment); + return parse_fragment(rest, pieces$1); + } else if (uri_string === "") { + return new Ok(new Uri(pieces.scheme, pieces.userinfo, pieces.host, pieces.port, original, pieces.query, pieces.fragment)); + } else { + let $ = pop_codeunit(uri_string); + let rest; + rest = $[1]; + loop$original = original; + loop$uri_string = rest; + loop$pieces = pieces; + loop$size = size3 + 1; + } + } +} +function parse_path(uri_string, pieces) { + return parse_path_loop(uri_string, uri_string, pieces, 0); +} +function parse_port_loop(loop$uri_string, loop$pieces, loop$port) { + while (true) { + let uri_string = loop$uri_string; + let pieces = loop$pieces; + let port = loop$port; + if (uri_string.startsWith("0")) { + let rest = uri_string.slice(1); + loop$uri_string = rest; + loop$pieces = pieces; + loop$port = port * 10; + } else if (uri_string.startsWith("1")) { + let rest = uri_string.slice(1); + loop$uri_string = rest; + loop$pieces = pieces; + loop$port = port * 10 + 1; + } else if (uri_string.startsWith("2")) { + let rest = uri_string.slice(1); + loop$uri_string = rest; + loop$pieces = pieces; + loop$port = port * 10 + 2; + } else if (uri_string.startsWith("3")) { + let rest = uri_string.slice(1); + loop$uri_string = rest; + loop$pieces = pieces; + loop$port = port * 10 + 3; + } else if (uri_string.startsWith("4")) { + let rest = uri_string.slice(1); + loop$uri_string = rest; + loop$pieces = pieces; + loop$port = port * 10 + 4; + } else if (uri_string.startsWith("5")) { + let rest = uri_string.slice(1); + loop$uri_string = rest; + loop$pieces = pieces; + loop$port = port * 10 + 5; + } else if (uri_string.startsWith("6")) { + let rest = uri_string.slice(1); + loop$uri_string = rest; + loop$pieces = pieces; + loop$port = port * 10 + 6; + } else if (uri_string.startsWith("7")) { + let rest = uri_string.slice(1); + loop$uri_string = rest; + loop$pieces = pieces; + loop$port = port * 10 + 7; + } else if (uri_string.startsWith("8")) { + let rest = uri_string.slice(1); + loop$uri_string = rest; + loop$pieces = pieces; + loop$port = port * 10 + 8; + } else if (uri_string.startsWith("9")) { + let rest = uri_string.slice(1); + loop$uri_string = rest; + loop$pieces = pieces; + loop$port = port * 10 + 9; + } else if (uri_string.startsWith("?")) { + let rest = uri_string.slice(1); + let pieces$1 = new Uri(pieces.scheme, pieces.userinfo, pieces.host, new Some(port), pieces.path, pieces.query, pieces.fragment); + return parse_query_with_question_mark(rest, pieces$1); + } else if (uri_string.startsWith("#")) { + let rest = uri_string.slice(1); + let pieces$1 = new Uri(pieces.scheme, pieces.userinfo, pieces.host, new Some(port), pieces.path, pieces.query, pieces.fragment); + return parse_fragment(rest, pieces$1); + } else if (uri_string.startsWith("/")) { + let pieces$1 = new Uri(pieces.scheme, pieces.userinfo, pieces.host, new Some(port), pieces.path, pieces.query, pieces.fragment); + return parse_path(uri_string, pieces$1); + } else if (uri_string === "") { + return new Ok(new Uri(pieces.scheme, pieces.userinfo, pieces.host, new Some(port), pieces.path, pieces.query, pieces.fragment)); + } else { + return new Error(undefined); + } + } +} +function parse_port(uri_string, pieces) { + if (uri_string.startsWith(":0")) { + let rest = uri_string.slice(2); + return parse_port_loop(rest, pieces, 0); + } else if (uri_string.startsWith(":1")) { + let rest = uri_string.slice(2); + return parse_port_loop(rest, pieces, 1); + } else if (uri_string.startsWith(":2")) { + let rest = uri_string.slice(2); + return parse_port_loop(rest, pieces, 2); + } else if (uri_string.startsWith(":3")) { + let rest = uri_string.slice(2); + return parse_port_loop(rest, pieces, 3); + } else if (uri_string.startsWith(":4")) { + let rest = uri_string.slice(2); + return parse_port_loop(rest, pieces, 4); + } else if (uri_string.startsWith(":5")) { + let rest = uri_string.slice(2); + return parse_port_loop(rest, pieces, 5); + } else if (uri_string.startsWith(":6")) { + let rest = uri_string.slice(2); + return parse_port_loop(rest, pieces, 6); + } else if (uri_string.startsWith(":7")) { + let rest = uri_string.slice(2); + return parse_port_loop(rest, pieces, 7); + } else if (uri_string.startsWith(":8")) { + let rest = uri_string.slice(2); + return parse_port_loop(rest, pieces, 8); + } else if (uri_string.startsWith(":9")) { + let rest = uri_string.slice(2); + return parse_port_loop(rest, pieces, 9); + } else if (uri_string === ":") { + return new Ok(pieces); + } else if (uri_string === "") { + return new Ok(pieces); + } else if (uri_string.startsWith("?")) { + let rest = uri_string.slice(1); + return parse_query_with_question_mark(rest, pieces); + } else if (uri_string.startsWith(":?")) { + let rest = uri_string.slice(2); + return parse_query_with_question_mark(rest, pieces); + } else if (uri_string.startsWith("#")) { + let rest = uri_string.slice(1); + return parse_fragment(rest, pieces); + } else if (uri_string.startsWith(":#")) { + let rest = uri_string.slice(2); + return parse_fragment(rest, pieces); + } else if (uri_string.startsWith("/")) { + return parse_path(uri_string, pieces); + } else if (uri_string.startsWith(":")) { + let rest = uri_string.slice(1); + if (rest.startsWith("/")) { + return parse_path(rest, pieces); + } else { + return new Error(undefined); + } + } else { + return new Error(undefined); + } +} +function parse_host_outside_of_brackets_loop(loop$original, loop$uri_string, loop$pieces, loop$size) { + while (true) { + let original = loop$original; + let uri_string = loop$uri_string; + let pieces = loop$pieces; + let size3 = loop$size; + if (uri_string === "") { + return new Ok(new Uri(pieces.scheme, pieces.userinfo, new Some(original), pieces.port, pieces.path, pieces.query, pieces.fragment)); + } else if (uri_string.startsWith(":")) { + let host = string_codeunit_slice(original, 0, size3); + let pieces$1 = new Uri(pieces.scheme, pieces.userinfo, new Some(host), pieces.port, pieces.path, pieces.query, pieces.fragment); + return parse_port(uri_string, pieces$1); + } else if (uri_string.startsWith("/")) { + let host = string_codeunit_slice(original, 0, size3); + let pieces$1 = new Uri(pieces.scheme, pieces.userinfo, new Some(host), pieces.port, pieces.path, pieces.query, pieces.fragment); + return parse_path(uri_string, pieces$1); + } else if (uri_string.startsWith("?")) { + let rest = uri_string.slice(1); + let host = string_codeunit_slice(original, 0, size3); + let pieces$1 = new Uri(pieces.scheme, pieces.userinfo, new Some(host), pieces.port, pieces.path, pieces.query, pieces.fragment); + return parse_query_with_question_mark(rest, pieces$1); + } else if (uri_string.startsWith("#")) { + let rest = uri_string.slice(1); + let host = string_codeunit_slice(original, 0, size3); + let pieces$1 = new Uri(pieces.scheme, pieces.userinfo, new Some(host), pieces.port, pieces.path, pieces.query, pieces.fragment); + return parse_fragment(rest, pieces$1); + } else { + let $ = pop_codeunit(uri_string); + let rest; + rest = $[1]; + loop$original = original; + loop$uri_string = rest; + loop$pieces = pieces; + loop$size = size3 + 1; + } + } +} +function parse_host_within_brackets_loop(loop$original, loop$uri_string, loop$pieces, loop$size) { + while (true) { + let original = loop$original; + let uri_string = loop$uri_string; + let pieces = loop$pieces; + let size3 = loop$size; + if (uri_string === "") { + return new Ok(new Uri(pieces.scheme, pieces.userinfo, new Some(uri_string), pieces.port, pieces.path, pieces.query, pieces.fragment)); + } else if (uri_string.startsWith("]")) { + if (size3 === 0) { + let rest = uri_string.slice(1); + return parse_port(rest, pieces); + } else { + let rest = uri_string.slice(1); + let host = string_codeunit_slice(original, 0, size3 + 1); + let pieces$1 = new Uri(pieces.scheme, pieces.userinfo, new Some(host), pieces.port, pieces.path, pieces.query, pieces.fragment); + return parse_port(rest, pieces$1); + } + } else if (uri_string.startsWith("/")) { + if (size3 === 0) { + return parse_path(uri_string, pieces); + } else { + let host = string_codeunit_slice(original, 0, size3); + let pieces$1 = new Uri(pieces.scheme, pieces.userinfo, new Some(host), pieces.port, pieces.path, pieces.query, pieces.fragment); + return parse_path(uri_string, pieces$1); + } + } else if (uri_string.startsWith("?")) { + if (size3 === 0) { + let rest = uri_string.slice(1); + return parse_query_with_question_mark(rest, pieces); + } else { + let rest = uri_string.slice(1); + let host = string_codeunit_slice(original, 0, size3); + let pieces$1 = new Uri(pieces.scheme, pieces.userinfo, new Some(host), pieces.port, pieces.path, pieces.query, pieces.fragment); + return parse_query_with_question_mark(rest, pieces$1); + } + } else if (uri_string.startsWith("#")) { + if (size3 === 0) { + let rest = uri_string.slice(1); + return parse_fragment(rest, pieces); + } else { + let rest = uri_string.slice(1); + let host = string_codeunit_slice(original, 0, size3); + let pieces$1 = new Uri(pieces.scheme, pieces.userinfo, new Some(host), pieces.port, pieces.path, pieces.query, pieces.fragment); + return parse_fragment(rest, pieces$1); + } + } else { + let $ = pop_codeunit(uri_string); + let char; + let rest; + char = $[0]; + rest = $[1]; + let $1 = is_valid_host_within_brackets_char(char); + if ($1) { + loop$original = original; + loop$uri_string = rest; + loop$pieces = pieces; + loop$size = size3 + 1; + } else { + return parse_host_outside_of_brackets_loop(original, original, pieces, 0); + } + } + } +} +function parse_host_within_brackets(uri_string, pieces) { + return parse_host_within_brackets_loop(uri_string, uri_string, pieces, 0); +} +function parse_host_outside_of_brackets(uri_string, pieces) { + return parse_host_outside_of_brackets_loop(uri_string, uri_string, pieces, 0); +} +function parse_host(uri_string, pieces) { + if (uri_string.startsWith("[")) { + return parse_host_within_brackets(uri_string, pieces); + } else if (uri_string.startsWith(":")) { + let pieces$1 = new Uri(pieces.scheme, pieces.userinfo, new Some(""), pieces.port, pieces.path, pieces.query, pieces.fragment); + return parse_port(uri_string, pieces$1); + } else if (uri_string === "") { + return new Ok(new Uri(pieces.scheme, pieces.userinfo, new Some(""), pieces.port, pieces.path, pieces.query, pieces.fragment)); + } else { + return parse_host_outside_of_brackets(uri_string, pieces); + } +} +function parse_userinfo_loop(loop$original, loop$uri_string, loop$pieces, loop$size) { + while (true) { + let original = loop$original; + let uri_string = loop$uri_string; + let pieces = loop$pieces; + let size3 = loop$size; + if (uri_string.startsWith("@")) { + if (size3 === 0) { + let rest = uri_string.slice(1); + return parse_host(rest, pieces); + } else { + let rest = uri_string.slice(1); + let userinfo = string_codeunit_slice(original, 0, size3); + let pieces$1 = new Uri(pieces.scheme, new Some(userinfo), pieces.host, pieces.port, pieces.path, pieces.query, pieces.fragment); + return parse_host(rest, pieces$1); + } + } else if (uri_string === "") { + return parse_host(original, pieces); + } else if (uri_string.startsWith("/")) { + return parse_host(original, pieces); + } else if (uri_string.startsWith("?")) { + return parse_host(original, pieces); + } else if (uri_string.startsWith("#")) { + return parse_host(original, pieces); + } else { + let $ = pop_codeunit(uri_string); + let rest; + rest = $[1]; + loop$original = original; + loop$uri_string = rest; + loop$pieces = pieces; + loop$size = size3 + 1; + } + } +} +function parse_authority_pieces(string5, pieces) { + return parse_userinfo_loop(string5, string5, pieces, 0); +} +function parse_authority_with_slashes(uri_string, pieces) { + if (uri_string === "//") { + return new Ok(new Uri(pieces.scheme, pieces.userinfo, new Some(""), pieces.port, pieces.path, pieces.query, pieces.fragment)); + } else if (uri_string.startsWith("//")) { + let rest = uri_string.slice(2); + return parse_authority_pieces(rest, pieces); + } else { + return parse_path(uri_string, pieces); + } +} +function parse_scheme_loop(loop$original, loop$uri_string, loop$pieces, loop$size) { + while (true) { + let original = loop$original; + let uri_string = loop$uri_string; + let pieces = loop$pieces; + let size3 = loop$size; + if (uri_string.startsWith("/")) { + if (size3 === 0) { + return parse_authority_with_slashes(uri_string, pieces); + } else { + let scheme = string_codeunit_slice(original, 0, size3); + let pieces$1 = new Uri(new Some(lowercase(scheme)), pieces.userinfo, pieces.host, pieces.port, pieces.path, pieces.query, pieces.fragment); + return parse_authority_with_slashes(uri_string, pieces$1); + } + } else if (uri_string.startsWith("?")) { + if (size3 === 0) { + let rest = uri_string.slice(1); + return parse_query_with_question_mark(rest, pieces); + } else { + let rest = uri_string.slice(1); + let scheme = string_codeunit_slice(original, 0, size3); + let pieces$1 = new Uri(new Some(lowercase(scheme)), pieces.userinfo, pieces.host, pieces.port, pieces.path, pieces.query, pieces.fragment); + return parse_query_with_question_mark(rest, pieces$1); + } + } else if (uri_string.startsWith("#")) { + if (size3 === 0) { + let rest = uri_string.slice(1); + return parse_fragment(rest, pieces); + } else { + let rest = uri_string.slice(1); + let scheme = string_codeunit_slice(original, 0, size3); + let pieces$1 = new Uri(new Some(lowercase(scheme)), pieces.userinfo, pieces.host, pieces.port, pieces.path, pieces.query, pieces.fragment); + return parse_fragment(rest, pieces$1); + } + } else if (uri_string.startsWith(":")) { + if (size3 === 0) { + return new Error(undefined); + } else { + let rest = uri_string.slice(1); + let scheme = string_codeunit_slice(original, 0, size3); + let pieces$1 = new Uri(new Some(lowercase(scheme)), pieces.userinfo, pieces.host, pieces.port, pieces.path, pieces.query, pieces.fragment); + return parse_authority_with_slashes(rest, pieces$1); + } + } else if (uri_string === "") { + return new Ok(new Uri(pieces.scheme, pieces.userinfo, pieces.host, pieces.port, original, pieces.query, pieces.fragment)); + } else { + let $ = pop_codeunit(uri_string); + let rest; + rest = $[1]; + loop$original = original; + loop$uri_string = rest; + loop$pieces = pieces; + loop$size = size3 + 1; + } + } +} +function to_string5(uri) { + let _block; + let $ = uri.fragment; + if ($ instanceof Some) { + let fragment3 = $[0]; + _block = toList(["#", fragment3]); + } else { + _block = toList([]); + } + let parts = _block; + let _block$1; + let $1 = uri.query; + if ($1 instanceof Some) { + let query = $1[0]; + _block$1 = prepend("?", prepend(query, parts)); + } else { + _block$1 = parts; + } + let parts$1 = _block$1; + let parts$2 = prepend(uri.path, parts$1); + let _block$2; + let $2 = uri.host; + let $3 = starts_with(uri.path, "/"); + if ($2 instanceof Some && !$3) { + let host = $2[0]; + if (host !== "") { + _block$2 = prepend("/", parts$2); + } else { + _block$2 = parts$2; + } + } else { + _block$2 = parts$2; + } + let parts$3 = _block$2; + let _block$3; + let $4 = uri.host; + let $5 = uri.port; + if ($4 instanceof Some && $5 instanceof Some) { + let port = $5[0]; + _block$3 = prepend(":", prepend(to_string(port), parts$3)); + } else { + _block$3 = parts$3; + } + let parts$4 = _block$3; + let _block$4; + let $6 = uri.scheme; + let $7 = uri.userinfo; + let $8 = uri.host; + if ($6 instanceof Some) { + if ($7 instanceof Some) { + if ($8 instanceof Some) { + let s = $6[0]; + let u = $7[0]; + let h = $8[0]; + _block$4 = prepend(s, prepend("://", prepend(u, prepend("@", prepend(h, parts$4))))); + } else { + let s = $6[0]; + _block$4 = prepend(s, prepend(":", parts$4)); + } + } else if ($8 instanceof Some) { + let s = $6[0]; + let h = $8[0]; + _block$4 = prepend(s, prepend("://", prepend(h, parts$4))); + } else { + let s = $6[0]; + _block$4 = prepend(s, prepend(":", parts$4)); + } + } else if ($7 instanceof None && $8 instanceof Some) { + let h = $8[0]; + _block$4 = prepend("//", prepend(h, parts$4)); + } else { + _block$4 = parts$4; + } + let parts$5 = _block$4; + return concat2(parts$5); +} +function parse2(uri_string) { + return parse_scheme_loop(uri_string, uri_string, empty3, 0); +} + +// build/dev/javascript/gleam_http/gleam/http/request.mjs +class Request extends CustomType { + constructor(method, headers, body, scheme, host, port, path, query) { + super(); + this.method = method; + this.headers = headers; + this.body = body; + this.scheme = scheme; + this.host = host; + this.port = port; + this.path = path; + this.query = query; + } +} +function to_uri(request) { + return new Uri(new Some(scheme_to_string(request.scheme)), new None, new Some(request.host), request.port, request.path, request.query, new None); +} +function from_uri(uri) { + return try$((() => { + let _pipe = uri.scheme; + let _pipe$1 = unwrap(_pipe, ""); + return scheme_from_string(_pipe$1); + })(), (scheme) => { + return try$((() => { + let _pipe = uri.host; + return to_result(_pipe, undefined); + })(), (host) => { + let req = new Request(new Get, toList([]), "", scheme, host, uri.port, uri.path, uri.query); + return new Ok(req); + }); + }); +} +function set_header2(request, key, value2) { + let headers = key_set(request.headers, lowercase(key), value2); + return new Request(request.method, headers, request.body, request.scheme, request.host, request.port, request.path, request.query); +} +function set_body(req, body) { + return new Request(req.method, req.headers, body, req.scheme, req.host, req.port, req.path, req.query); +} +function set_method(req, method) { + return new Request(method, req.headers, req.body, req.scheme, req.host, req.port, req.path, req.query); +} +// build/dev/javascript/gleam_javascript/gleam_javascript_ffi.mjs +class PromiseLayer { + constructor(promise) { + this.promise = promise; + } + static wrap(value2) { + return value2 instanceof Promise ? new PromiseLayer(value2) : value2; + } + static unwrap(value2) { + return value2 instanceof PromiseLayer ? value2.promise : value2; + } +} +function resolve(value2) { + return Promise.resolve(PromiseLayer.wrap(value2)); +} +function then_await(promise, fn) { + return promise.then((value2) => fn(PromiseLayer.unwrap(value2))); +} +function map_promise(promise, fn) { + return promise.then((value2) => PromiseLayer.wrap(fn(PromiseLayer.unwrap(value2)))); +} + +// build/dev/javascript/gleam_javascript/gleam/javascript/promise.mjs +function tap(promise, callback) { + let _pipe = promise; + return map_promise(_pipe, (a) => { + callback(a); + return a; + }); +} +function try_await(promise, callback) { + let _pipe = promise; + return then_await(_pipe, (result) => { + if (result instanceof Ok) { + let a = result[0]; + return callback(a); + } else { + let e = result[0]; + return resolve(new Error(e)); + } + }); +} +// build/dev/javascript/gleam_fetch/gleam_fetch_ffi.mjs +async function raw_send(request) { + try { + return new Ok(await fetch(request)); + } catch (error) { + return new Error(new NetworkError(error.toString())); + } +} +function from_fetch_response(response) { + return new Response(response.status, List.fromArray([...response.headers]), response); +} +function request_common(request) { + let url = to_string5(to_uri(request)); + let method = method_to_string(request.method).toUpperCase(); + let options = { + headers: make_headers(request.headers), + method + }; + return [url, options]; +} +function to_fetch_request(request) { + let [url, options] = request_common(request); + if (options.method !== "GET" && options.method !== "HEAD") + options.body = request.body; + return new globalThis.Request(url, options); +} +function make_headers(headersList) { + let headers = new globalThis.Headers; + for (let [k, v] of headersList) + headers.append(k.toLowerCase(), v); + return headers; +} +async function read_text_body(response) { + let body; + try { + body = await response.body.text(); + } catch (error) { + return new Error(new UnableToReadBody); + } + return new Ok(response.withFields({ body })); +} + +// build/dev/javascript/gleam_fetch/gleam/fetch.mjs +class NetworkError extends CustomType { + constructor($0) { + super(); + this[0] = $0; + } +} +class UnableToReadBody extends CustomType { +} +function send2(request) { + let _pipe = request; + let _pipe$1 = to_fetch_request(_pipe); + let _pipe$2 = raw_send(_pipe$1); + return try_await(_pipe$2, (resp) => { + return resolve(new Ok(from_fetch_response(resp))); + }); +} +// build/dev/javascript/rsvp/rsvp.ffi.mjs +var from_relative_url = (url_string) => { + if (!globalThis.location) + return new Error(undefined); + const url = new URL(url_string, globalThis.location.href); + const uri = uri_from_url(url); + return new Ok(uri); +}; +var uri_from_url = (url) => { + const optional = (value2) => value2 ? new Some(value2) : new None; + return new Uri(optional(url.protocol?.slice(0, -1)), new None, optional(url.hostname), optional(url.port && Number(url.port)), url.pathname, optional(url.search?.slice(1)), optional(url.hash?.slice(1))); +}; + +// build/dev/javascript/rsvp/rsvp.mjs +class BadBody extends CustomType { +} +class BadUrl extends CustomType { + constructor($0) { + super(); + this[0] = $0; + } +} +class HttpError extends CustomType { + constructor($0) { + super(); + this[0] = $0; + } +} +class NetworkError2 extends CustomType { +} +class UnhandledResponse extends CustomType { + constructor($0) { + super(); + this[0] = $0; + } +} +class Handler2 extends CustomType { + constructor(run2) { + super(); + this.run = run2; + } +} +function expect_ok_response(handler) { + return new Handler2((result) => { + return handler(try$(result, (response) => { + let $ = response.status; + let code = $; + if (code >= 200 && code < 300) { + return new Ok(response); + } else { + let code2 = $; + if (code2 >= 400 && code2 < 600) { + return new Error(new HttpError(response)); + } else { + return new Error(new UnhandledResponse(response)); + } + } + })); + }); +} +function do_send(request, handler) { + return from2((dispatch2) => { + let _pipe = send2(request); + let _pipe$1 = try_await(_pipe, read_text_body); + let _pipe$2 = map_promise(_pipe$1, (_capture) => { + return map_error(_capture, (error) => { + if (error instanceof NetworkError) { + return new NetworkError2; + } else if (error instanceof UnableToReadBody) { + return new BadBody; + } else { + return new BadBody; + } + }); + }); + let _pipe$3 = map_promise(_pipe$2, handler.run); + tap(_pipe$3, dispatch2); + return; + }); +} +function send3(request, handler) { + return do_send(request, handler); +} +function reject(err, handler) { + return from2((dispatch2) => { + let _pipe = new Error(err); + let _pipe$1 = handler.run(_pipe); + return dispatch2(_pipe$1); + }); +} +function to_uri2(uri_string) { + let _block; + if (uri_string.startsWith("./")) { + _block = from_relative_url(uri_string); + } else if (uri_string.startsWith("/")) { + _block = from_relative_url(uri_string); + } else { + _block = parse2(uri_string); + } + let _pipe = _block; + return replace_error(_pipe, new BadUrl(uri_string)); +} +function post(url, body, handler) { + let $ = to_uri2(url); + if ($ instanceof Ok) { + let uri = $[0]; + let $1 = from_uri(uri); + if ($1 instanceof Ok) { + let request = $1[0]; + let _pipe = request; + let _pipe$1 = set_method(_pipe, new Post); + let _pipe$2 = set_header2(_pipe$1, "content-type", "application/json"); + let _pipe$3 = set_body(_pipe$2, to_string2(body)); + return send3(_pipe$3, handler); + } else { + return reject(new BadUrl(url), handler); + } + } else { + let err = $[0]; + return reject(err, handler); + } +} +// build/dev/javascript/client/shared.mjs +class GroceryItem extends CustomType { + constructor(name, quantity) { + super(); + this.name = name; + this.quantity = quantity; + } +} +function grocery_item_to_json(grocery_item) { + let name; + let quantity; + name = grocery_item.name; + quantity = grocery_item.quantity; + return object2(toList([["name", string3(name)], ["quantity", int3(quantity)]])); +} +function grocery_list_to_json(items) { + return array2(items, grocery_item_to_json); +} + +// build/dev/javascript/client/client.mjs +var FILEPATH = "src/client.gleam"; + +class Model extends CustomType { + constructor(items, new_item, saving, error) { + super(); + this.items = items; + this.new_item = new_item; + this.saving = saving; + this.error = error; + } +} + +class ServerSavedList extends CustomType { + constructor($0) { + super(); + this[0] = $0; + } +} + +class UserAddedItem extends CustomType { +} + +class UserTypedNewItem extends CustomType { + constructor($0) { + super(); + this[0] = $0; + } +} + +class UserSavedList extends CustomType { +} + +class UserUpdatedQuantity extends CustomType { + constructor(index5, quantity) { + super(); + this.index = index5; + this.quantity = quantity; + } +} +function init(items) { + let model = new Model(items, "", false, new None); + return [model, none()]; +} +function save_list(items) { + let body = grocery_list_to_json(items); + let url = "/api/groceries"; + return post(url, body, expect_ok_response((var0) => { + return new ServerSavedList(var0); + })); +} +function update2(model, msg) { + if (msg instanceof ServerSavedList) { + let $ = msg[0]; + if ($ instanceof Ok) { + return [ + new Model(model.items, model.new_item, false, new None), + none() + ]; + } else { + return [ + new Model(model.items, model.new_item, false, new Some("Failed to save list")), + none() + ]; + } + } else if (msg instanceof UserAddedItem) { + let $ = model.new_item; + if ($ === "") { + return [model, none()]; + } else { + let name = $; + let item = new GroceryItem(name, 1); + let updated_items = append2(model.items, toList([item])); + return [ + new Model(updated_items, "", model.saving, model.error), + none() + ]; + } + } else if (msg instanceof UserTypedNewItem) { + let text4 = msg[0]; + return [ + new Model(model.items, text4, model.saving, model.error), + none() + ]; + } else if (msg instanceof UserSavedList) { + return [ + new Model(model.items, model.new_item, true, model.error), + save_list(model.items) + ]; + } else { + let index5 = msg.index; + let quantity = msg.quantity; + let updated_items = index_map(model.items, (item, item_index) => { + let $ = item_index === index5; + if ($) { + return new GroceryItem(item.name, quantity); + } else { + return item; + } + }); + return [ + new Model(updated_items, model.new_item, model.saving, model.error), + none() + ]; + } +} +function view_new_item(new_item) { + return div(toList([]), toList([ + input(toList([ + placeholder("Enter item name"), + value(new_item), + on_input((var0) => { + return new UserTypedNewItem(var0); + }) + ])), + button(toList([on_click(new UserAddedItem)]), toList([text3("Add")])) + ])); +} +function view_grocery_item(item, index5) { + return div(toList([styles(toList([["display", "flex"], ["gap", "1em"]]))]), toList([ + span(toList([style("flex", "1")]), toList([text3(item.name)])), + input(toList([ + style("width", "4em"), + type_("number"), + value(to_string(item.quantity)), + min2("0"), + on_input((value2) => { + let _pipe = unwrap2(parse_int(value2), 0); + return ((_capture) => { + return new UserUpdatedQuantity(index5, _capture); + })(_pipe); + }) + ])) + ])); +} +function view_grocery_list(items) { + if (items instanceof Empty) { + return p(toList([]), toList([text3("No items in your list yet.")])); + } else { + return ul(toList([]), index_map(items, (item, index5) => { + return li(toList([]), toList([view_grocery_item(item, index5)])); + })); + } +} +function view(model) { + let styles2 = toList([ + ["max-width", "30ch"], + ["margin", "0 auto"], + ["display", "flex"], + ["flex-direction", "column"], + ["gap", "1em"] + ]); + return div(toList([styles(styles2)]), toList([ + h1(toList([]), toList([text3("Grocery List")])), + view_grocery_list(model.items), + view_new_item(model.new_item), + div(toList([]), toList([ + button(toList([ + on_click(new UserSavedList), + disabled(model.saving) + ]), toList([ + text3((() => { + let $ = model.saving; + if ($) { + return "Saving..."; + } else { + return "Save List"; + } + })()) + ])) + ])), + (() => { + let $ = model.error; + if ($ instanceof Some) { + let error = $[0]; + return div(toList([style("color", "red")]), toList([text3(error)])); + } else { + return none2(); + } + })() + ])); +} +function main() { + let app = application(init, update2, view); + let $ = start4(app, "#app", toList([])); + if (!($ instanceof Ok)) { + throw makeError("let_assert", FILEPATH, "client", 17, "main", "Pattern match failed, no pattern matched the value.", { value: $, start: 417, end: 465, pattern_start: 428, pattern_end: 433 }); + } + return; +} + +// .lustre/build/client.mjs +main(); diff --git a/client/dist/index.html b/client/dist/index.html new file mode 100644 index 0000000..ae79510 --- /dev/null +++ b/client/dist/index.html @@ -0,0 +1,17 @@ + + + + + + + + client + + + + + + + +
+ \ No newline at end of file diff --git a/client/gleam.toml b/client/gleam.toml new file mode 100644 index 0000000..05fd0f0 --- /dev/null +++ b/client/gleam.toml @@ -0,0 +1,15 @@ +name = "client" +version = "1.0.0" +target = "javascript" + +[dependencies] +gleam_stdlib = ">= 0.44.0 and < 2.0.0" +lustre = ">= 5.6.0 and < 6.0.0" +rsvp = ">= 1.2.0 and < 2.0.0" +gleam_json = ">= 3.1.0 and < 4.0.0" +gleam_http = ">= 4.3.0 and < 5.0.0" +plinth = ">= 0.10.2 and < 1.0.0" + +[dev_dependencies] +gleeunit = ">= 1.0.0 and < 2.0.0" +lustre_dev_tools = ">= 2.3.5 and < 3.0.0" diff --git a/client/manifest.toml b/client/manifest.toml new file mode 100644 index 0000000..9cf9beb --- /dev/null +++ b/client/manifest.toml @@ -0,0 +1,57 @@ +# This file was generated by Gleam +# You typically do not need to edit this file + +packages = [ + { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" }, + { name = "booklet", version = "1.1.0", build_tools = ["gleam"], requirements = [], otp_app = "booklet", source = "hex", outer_checksum = "08E0FDB78DC4D8A5D3C80295B021505C7D2A2E7B6C6D5EAB7286C36F4A53C851" }, + { name = "directories", version = "1.2.0", build_tools = ["gleam"], requirements = ["envoy", "gleam_stdlib", "platform", "simplifile"], otp_app = "directories", source = "hex", outer_checksum = "D13090CFCDF6759B87217E8DDD73A75903A700148A82C1D33799F333E249BF9E" }, + { name = "envoy", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "envoy", source = "hex", outer_checksum = "850DA9D29D2E5987735872A2B5C81035146D7FE19EFC486129E44440D03FD832" }, + { name = "exception", version = "2.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "329D269D5C2A314F7364BD2711372B6F2C58FA6F39981572E5CA68624D291F8C" }, + { name = "filepath", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "B06A9AF0BF10E51401D64B98E4B627F1D2E48C154967DA7AF4D0914780A6D40A" }, + { name = "gleam_community_ansi", version = "1.4.4", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_regexp", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "1B3AEA6074AB34D5F0674744F36DDC7290303A03295507E2DEC61EDD6F5777FE" }, + { name = "gleam_community_colour", version = "2.0.4", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "6DB4665555D7D2B27F0EA32EF47E8BEBC4303821765F9C73D483F38EE24894F0" }, + { name = "gleam_crypto", version = "1.5.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "50774BAFFF1144E7872814C566C5D653D83A3EBF23ACC3156B757A1B6819086E" }, + { name = "gleam_erlang", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "1124AD3AA21143E5AF0FC5CF3D9529F6DB8CA03E43A55711B60B6B7B3874375C" }, + { name = "gleam_fetch", version = "1.3.1", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_javascript", "gleam_stdlib"], otp_app = "gleam_fetch", source = "hex", outer_checksum = "A8FEB5FC4F9C4C72A71BA0D7AC249CF3AE4E98A4123607A5077D8C0B8ECC5A40" }, + { name = "gleam_http", version = "4.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "82EA6A717C842456188C190AFB372665EA56CE13D8559BF3B1DD9E40F619EE0C" }, + { name = "gleam_httpc", version = "5.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "C545172618D07811494E97AAA4A0FB34DA6F6D0061FDC8041C2F8E3BE2B2E48F" }, + { name = "gleam_javascript", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "EF6C77A506F026C6FB37941889477CD5E4234FCD4337FF0E9384E297CB8F97EB" }, + { name = "gleam_json", version = "3.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "44FDAA8847BE8FC48CA7A1C089706BD54BADCC4C45B237A992EDDF9F2CDB2836" }, + { name = "gleam_otp", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "BA6A294E295E428EC1562DC1C11EA7530DCB981E8359134BEABC8493B7B2258E" }, + { name = "gleam_regexp", version = "1.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_regexp", source = "hex", outer_checksum = "9C215C6CA84A5B35BB934A9B61A9A306EC743153BE2B0425A0D032E477B062A9" }, + { name = "gleam_stdlib", version = "0.70.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "86949BF5D1F0E4AC0AB5B06F235D8A5CC11A2DFC33BF22F752156ED61CA7D0FF" }, + { name = "gleam_time", version = "1.8.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_time", source = "hex", outer_checksum = "533D8723774D61AD4998324F5DD1DABDCDBFABAFB9E87CB5D03C6955448FC97D" }, + { name = "gleam_yielder", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_yielder", source = "hex", outer_checksum = "8E4E4ECFA7982859F430C57F549200C7749823C106759F4A19A78AEA6687717A" }, + { name = "gleeunit", version = "1.9.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "DA9553CE58B67924B3C631F96FE3370C49EB6D6DC6B384EC4862CC4AAA718F3C" }, + { name = "glint", version = "1.2.1", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_community_colour", "gleam_stdlib", "snag"], otp_app = "glint", source = "hex", outer_checksum = "2214C7CEFDE457CEE62140C3D4899B964E05236DA74E4243DFADF4AF29C382BB" }, + { name = "glisten", version = "8.0.3", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib", "logging", "telemetry"], otp_app = "glisten", source = "hex", outer_checksum = "86B838196592D9EBDE7A1D2369AE3A51E568F7DD2D168706C463C42D17B95312" }, + { name = "gramps", version = "6.0.0", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gramps", source = "hex", outer_checksum = "8B7195978FBFD30B43DF791A8A272041B81E45D245314D7A41FC57237AA882A0" }, + { name = "group_registry", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "group_registry", source = "hex", outer_checksum = "BC798A53D6F2406DB94E27CB45C57052CB56B32ACF7CC16EA20F6BAEC7E36B90" }, + { name = "houdini", version = "1.2.0", build_tools = ["gleam"], requirements = [], otp_app = "houdini", source = "hex", outer_checksum = "5DB1053F1AF828049C2B206D4403C18970ABEF5C18671CA3C2D2ED0DD64F6385" }, + { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" }, + { name = "justin", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "justin", source = "hex", outer_checksum = "7FA0C6DB78640C6DC5FBFD59BF3456009F3F8B485BF6825E97E1EB44E9A1E2CD" }, + { name = "logging", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "1098FBF10B54B44C2C7FDF0B01C1253CAFACDACABEFB4B0D027803246753E06D" }, + { name = "lustre", version = "5.6.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib", "houdini"], otp_app = "lustre", source = "hex", outer_checksum = "EE558CD4DB9F09FCC16417ADF0183A3C2DAC3E4B21ED3AC0CAE859792AB810CA" }, + { name = "lustre_dev_tools", version = "2.3.5", build_tools = ["gleam"], requirements = ["argv", "booklet", "filepath", "gleam_community_ansi", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_httpc", "gleam_json", "gleam_otp", "gleam_regexp", "gleam_stdlib", "glint", "group_registry", "justin", "lustre", "mist", "polly", "simplifile", "tom", "wisp"], otp_app = "lustre_dev_tools", source = "hex", outer_checksum = "0B376F4057AB6E0FB5D48DFED9943D48571968D132EBD67EAED0A21EA29027FE" }, + { name = "marceau", version = "1.3.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "2D1C27504BEF45005F5DFB18591F8610FB4BFA91744878210BDC464412EC44E9" }, + { name = "mist", version = "5.0.4", build_tools = ["gleam"], requirements = ["exception", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "gleam_yielder", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "7CED4B2D81FD547ADB093D97B9928B9419A7F58B8562A30A6CC17A252B31AD05" }, + { name = "platform", version = "1.0.0", build_tools = ["gleam"], requirements = [], otp_app = "platform", source = "hex", outer_checksum = "8339420A95AD89AAC0F82F4C3DB8DD401041742D6C3F46132A8739F6AEB75391" }, + { name = "plinth", version = "0.10.2", build_tools = ["gleam"], requirements = ["gleam_javascript", "gleam_json", "gleam_stdlib"], otp_app = "plinth", source = "hex", outer_checksum = "3FE77CED3F19D70918EE32CE8BFB12BE1C28CA004D997F874C2D8DAD2DB73D87" }, + { name = "polly", version = "3.1.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_erlang", "gleam_otp", "gleam_stdlib", "simplifile"], otp_app = "polly", source = "hex", outer_checksum = "51FB565D81FF6212FDF3306D44419601F2A7C4EDD1F00FC9DA5C376A00AED4FE" }, + { name = "rsvp", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_fetch", "gleam_http", "gleam_httpc", "gleam_javascript", "gleam_json", "gleam_stdlib", "lustre"], otp_app = "rsvp", source = "hex", outer_checksum = "40F9E0E662FF258E10C7041A9591261FE802D56625FB444B91510969644F7722" }, + { name = "simplifile", version = "2.4.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "7C18AFA4FED0B4CE1FA5B0B4BAC1FA1744427054EA993565F6F3F82E5453170D" }, + { name = "snag", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "274F41D6C3ECF99F7686FDCE54183333E41D2C1CA5A3A673F9A8B2C7A4401077" }, + { name = "telemetry", version = "1.4.1", build_tools = ["rebar3"], requirements = [], otp_app = "telemetry", source = "hex", outer_checksum = "2172E05A27531D3D31DD9782841065C50DD5C3C7699D95266B2EDD54C2DAFA1C" }, + { name = "tom", version = "2.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_time"], otp_app = "tom", source = "hex", outer_checksum = "234A842F3D087D35737483F5DFB6DE9839E3366EF0CAF8726D2D094210227670" }, + { name = "wisp", version = "2.2.1", build_tools = ["gleam"], requirements = ["directories", "exception", "filepath", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_json", "gleam_stdlib", "houdini", "logging", "marceau", "mist", "simplifile"], otp_app = "wisp", source = "hex", outer_checksum = "6E37308F4F8C45B5D3F2A2DC71BDE4DA6AD2F7978D26A7C8D6790CE275C8614E" }, +] + +[requirements] +gleam_http = { version = ">= 4.3.0 and < 5.0.0" } +gleam_json = { version = ">= 3.1.0 and < 4.0.0" } +gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" } +gleeunit = { version = ">= 1.0.0 and < 2.0.0" } +lustre = { version = ">= 5.6.0 and < 6.0.0" } +lustre_dev_tools = { version = ">= 2.3.5 and < 3.0.0" } +plinth = { version = ">= 0.10.2 and < 1.0.0" } +rsvp = { version = ">= 1.2.0 and < 2.0.0" } diff --git a/client/src/client.gleam b/client/src/client.gleam new file mode 100644 index 0000000..17d8591 --- /dev/null +++ b/client/src/client.gleam @@ -0,0 +1,83 @@ +import gleam/json +import gleam/list +import gleam/result +import lustre +import lustre/attribute +import lustre/effect.{type Effect} +import lustre/element.{type Element} +import lustre/element/html +import plinth/browser/document +import plinth/browser/element as plinth_element +import shared + +pub fn main() { + let initial_items = + document.query_selector("#model") + |> result.map(plinth_element.inner_text) + |> result.try(fn(json) { + json.parse(json, shared.grocery_list_decoder()) + |> result.replace_error(Nil) + }) + |> result.unwrap([]) + + let app = lustre.application(init, update, view) + let assert Ok(_) = lustre.start(app, "#app", initial_items) + + Nil +} + +type Model { + Model(rooms: List(String), name: String) +} + +fn init(items: List(String)) -> #(Model, Effect(Msg)) { + let model = Model(rooms: items, name: "") + + #(model, effect.none()) +} + +type Msg { + UserTypedNewItem(String) + UserAddedItem +} + +fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) { + case msg { + UserAddedItem -> { + case model.name { + "" -> #(model, effect.none()) + name -> { + let updated_items = [name, ..model.rooms] + + #(Model(rooms: updated_items, name: ""), effect.none()) + } + } + } + + UserTypedNewItem(text) -> #(Model(..model, name: text), effect.none()) + } +} + +fn view(model: Model) -> Element(Msg) { + let styles = [ + #("max-width", "30ch"), + #("margin", "0 auto"), + #("display", "flex"), + #("flex-direction", "column"), + #("gap", "1em"), + ] + + html.div([attribute.styles(styles)], [ + html.h1([], [html.text("Select your QuizRoom")]), + view_room_list(model.rooms), + ]) +} + +fn view_room_list(items: List(String)) -> Element(Msg) { + case items { + [] -> html.p([], [html.text("No items in your list yet.")]) + _ -> { + html.ul([], list.map(items, fn(item) { html.li([], [html.text(item)]) })) + } + } +} diff --git a/client/src/shared.gleam b/client/src/shared.gleam new file mode 100644 index 0000000..f0bae3c --- /dev/null +++ b/client/src/shared.gleam @@ -0,0 +1,19 @@ +import gleam/dynamic/decode +import gleam/json + +pub type GroceryItem { + GroceryItem(name: String, quantity: Int) +} + +pub fn grocery_list_decoder() -> decode.Decoder(List(String)) { + decode.list(decode.string) +} + +fn grocery_item_to_json(grocery_item: GroceryItem) -> json.Json { + let GroceryItem(name:, quantity:) = grocery_item + json.object([#("name", json.string(name)), #("quantity", json.int(quantity))]) +} + +pub fn grocery_list_to_json(items: List(GroceryItem)) -> json.Json { + json.array(items, grocery_item_to_json) +} \ No newline at end of file diff --git a/game1.png b/game1.png new file mode 100644 index 0000000..2b6d365 Binary files /dev/null and b/game1.png differ diff --git a/game2.png b/game2.png new file mode 100644 index 0000000..58a343f Binary files /dev/null and b/game2.png differ diff --git a/layout.html b/layout.html deleted file mode 100644 index f462667..0000000 --- a/layout.html +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - -
-

QUIZTerminal

-

What is your name?

-

-
-
-
- TESTsfdfdsk jhfkj dshfkjs skjfdshjs TESTsfdfdsk jhfkj - dshfkjs skjfdshjsTESTsfdfdsk jhfkj dshfkjs skjfdshjs -
-
TEST
-
TEST
-
- TESTsfdfdsk jhfkj dshfkjs skjfdshjs TESTsfdfdsk jhfkj - dshfkjs skjfdshjsTESTsfdfdsk jhfkj dshfkjs skjfdshjs -
-
TEST
-
- TESTsfdfdsk jhfkj dshfkjs skjfdshjs TESTsfdfdsk jhfkj - dshfkjs skjfdshjsTESTsfdfdsk jhfkj dshfkjs skjfdshjs -
-
TEST
-
TEST
-
- - - diff --git a/manifest.toml b/manifest.toml deleted file mode 100644 index fda893a..0000000 --- a/manifest.toml +++ /dev/null @@ -1,33 +0,0 @@ -# This file was generated by Gleam -# You typically do not need to edit this file - -packages = [ - { name = "exception", version = "2.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "329D269D5C2A314F7364BD2711372B6F2C58FA6F39981572E5CA68624D291F8C" }, - { name = "gleam_crypto", version = "1.5.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "50774BAFFF1144E7872814C566C5D653D83A3EBF23ACC3156B757A1B6819086E" }, - { name = "gleam_erlang", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "1124AD3AA21143E5AF0FC5CF3D9529F6DB8CA03E43A55711B60B6B7B3874375C" }, - { name = "gleam_http", version = "4.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "DD0271B32C356FB684EC7E9F48B1E835D0480168848581F68983C0CC371405D4" }, - { name = "gleam_json", version = "3.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "874FA3C3BB6E22DD2BB111966BD40B3759E9094E05257899A7C08F5DE77EC049" }, - { name = "gleam_otp", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "7987CBEBC8060B88F14575DEF546253F3116EBE2A5DA6FD82F38243FCE97C54B" }, - { name = "gleam_stdlib", version = "0.63.1", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "E1D5EC07638F606E48F0EA1556044DD805F2ACE9092A6F6AFBE4A0CC4DA21C2F" }, - { name = "gleam_yielder", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_yielder", source = "hex", outer_checksum = "8E4E4ECFA7982859F430C57F549200C7749823C106759F4A19A78AEA6687717A" }, - { name = "glisten", version = "8.0.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib", "logging", "telemetry"], otp_app = "glisten", source = "hex", outer_checksum = "534BB27C71FB9E506345A767C0D76B17A9E9199934340C975DC003C710E3692D" }, - { name = "gramps", version = "6.0.0", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gramps", source = "hex", outer_checksum = "8B7195978FBFD30B43DF791A8A272041B81E45D245314D7A41FC57237AA882A0" }, - { name = "group_registry", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "group_registry", source = "hex", outer_checksum = "BC798A53D6F2406DB94E27CB45C57052CB56B32ACF7CC16EA20F6BAEC7E36B90" }, - { name = "houdini", version = "1.2.0", build_tools = ["gleam"], requirements = [], otp_app = "houdini", source = "hex", outer_checksum = "5DB1053F1AF828049C2B206D4403C18970ABEF5C18671CA3C2D2ED0DD64F6385" }, - { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" }, - { name = "logging", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "1098FBF10B54B44C2C7FDF0B01C1253CAFACDACABEFB4B0D027803246753E06D" }, - { name = "lustre", version = "5.3.5", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib", "houdini"], otp_app = "lustre", source = "hex", outer_checksum = "5CBB5DD2849D8316A2101792FC35AEB58CE4B151451044A9C2A2A70A2F7FCEB8" }, - { name = "mist", version = "5.0.3", build_tools = ["gleam"], requirements = ["exception", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "gleam_yielder", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "7C4BE717A81305323C47C8A591E6B9BA4AC7F56354BF70B4D3DF08CC01192668" }, - { name = "telemetry", version = "1.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "telemetry", source = "hex", outer_checksum = "7015FC8919DBE63764F4B4B87A95B7C0996BD539E0D499BE6EC9D7F3875B79E6" }, -] - -[requirements] -gleam_crypto = { version = ">= 1.5.1 and < 2.0.0" } -gleam_erlang = { version = ">= 1.0.0 and < 2.0.0" } -gleam_http = { version = ">= 3.7.2 and < 5.0.0" } -gleam_json = { version = ">= 2.3.0 and < 4.0.0" } -gleam_otp = { version = ">= 1.1.0 and < 2.0.0" } -gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" } -group_registry = { version = ">= 1.0.0 and < 2.0.0" } -lustre = { version = ">= 5.3.5 and < 6.0.0" } -mist = { version = ">= 5.0.0 and < 6.0.0" } diff --git a/priv/layout.css b/priv/layout.css deleted file mode 100644 index bfbd3d8..0000000 --- a/priv/layout.css +++ /dev/null @@ -1,69 +0,0 @@ -body { - background-color: #000000; - color: #73ad21; -} -.center { - margin: auto; - width: 50%; - border: 3px solid #73ad21; - padding: 10px; -} -.under { - margin: auto; - cursor: default; - margin-bottom: 8px; - display: grid; - width: 50%; - grid-template-columns: 33% 33% 33%; -} - -.under_cell { - text-align: center; - border: 3px solid #73ad21; - margin: 5px; - width: 95%; - min-height: 75px; -} - -.under_cell_nb { - text-align: center; - margin: 5px; - width: 95%; - min-height: 75px; -} - -.control { - margin: auto; - text-align: center; - cursor: default; - margin-bottom: 8px; - display: grid; - width: 50%; - grid-template-columns: 100% - border: 3px solid #73ad21; - padding: 10px; -} - -input { - background-color: #000000; - color: #00ff00; - width: 20cap; -} - -.controlbutton { - background-color: #000000; - color: #00FF00; - border: 3px solid #73ad21; - width: 100%; -} - -.controlbutton:focus { - border: 3px solid #534d01; -} - -input:focus { - background-color: #000000; - width: 20cap; - border-color: #00ff00; - outline: none; -} diff --git a/screenshot.png b/screenshot.png deleted file mode 100644 index 1ad1b81..0000000 Binary files a/screenshot.png and /dev/null differ diff --git a/gleam.toml b/server/gleam.toml similarity index 80% rename from gleam.toml rename to server/gleam.toml index e3286ee..e1ed142 100644 --- a/gleam.toml +++ b/server/gleam.toml @@ -1,5 +1,5 @@ name = "quizterm" -version = "0.5.0" +version = "1.0.0" [dependencies] gleam_erlang = ">= 1.0.0 and < 2.0.0" @@ -7,7 +7,8 @@ gleam_http = ">= 3.7.2 and < 5.0.0" gleam_json = ">= 2.3.0 and < 4.0.0" gleam_otp = ">= 1.1.0 and < 2.0.0" gleam_stdlib = ">= 0.44.0 and < 2.0.0" -mist = ">= 5.0.0 and < 6.0.0" +mist = ">= 6.0.0 and < 7.0.0" lustre = ">= 5.3.5 and < 6.0.0" gleam_crypto = ">= 1.5.1 and < 2.0.0" group_registry = ">= 1.0.0 and < 2.0.0" +wisp = ">= 2.2.1 and < 3.0.0" diff --git a/server/manifest.toml b/server/manifest.toml new file mode 100644 index 0000000..81fd2aa --- /dev/null +++ b/server/manifest.toml @@ -0,0 +1,39 @@ +# This file was generated by Gleam +# You typically do not need to edit this file + +packages = [ + { name = "directories", version = "1.2.0", build_tools = ["gleam"], requirements = ["envoy", "gleam_stdlib", "platform", "simplifile"], otp_app = "directories", source = "hex", outer_checksum = "D13090CFCDF6759B87217E8DDD73A75903A700148A82C1D33799F333E249BF9E" }, + { name = "envoy", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "envoy", source = "hex", outer_checksum = "850DA9D29D2E5987735872A2B5C81035146D7FE19EFC486129E44440D03FD832" }, + { name = "exception", version = "2.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "329D269D5C2A314F7364BD2711372B6F2C58FA6F39981572E5CA68624D291F8C" }, + { name = "filepath", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "B06A9AF0BF10E51401D64B98E4B627F1D2E48C154967DA7AF4D0914780A6D40A" }, + { name = "gleam_crypto", version = "1.5.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "50774BAFFF1144E7872814C566C5D653D83A3EBF23ACC3156B757A1B6819086E" }, + { name = "gleam_erlang", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "1124AD3AA21143E5AF0FC5CF3D9529F6DB8CA03E43A55711B60B6B7B3874375C" }, + { name = "gleam_http", version = "4.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "82EA6A717C842456188C190AFB372665EA56CE13D8559BF3B1DD9E40F619EE0C" }, + { name = "gleam_json", version = "3.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "44FDAA8847BE8FC48CA7A1C089706BD54BADCC4C45B237A992EDDF9F2CDB2836" }, + { name = "gleam_otp", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "BA6A294E295E428EC1562DC1C11EA7530DCB981E8359134BEABC8493B7B2258E" }, + { name = "gleam_stdlib", version = "0.70.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "86949BF5D1F0E4AC0AB5B06F235D8A5CC11A2DFC33BF22F752156ED61CA7D0FF" }, + { name = "glisten", version = "9.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib", "logging"], otp_app = "glisten", source = "hex", outer_checksum = "D92808C66F7D3F22F2289CD04CBA8151757AAE9CB3D86992F0C6DE32A41205E1" }, + { name = "gramps", version = "6.0.0", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gramps", source = "hex", outer_checksum = "8B7195978FBFD30B43DF791A8A272041B81E45D245314D7A41FC57237AA882A0" }, + { name = "group_registry", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "group_registry", source = "hex", outer_checksum = "BC798A53D6F2406DB94E27CB45C57052CB56B32ACF7CC16EA20F6BAEC7E36B90" }, + { name = "houdini", version = "1.2.0", build_tools = ["gleam"], requirements = [], otp_app = "houdini", source = "hex", outer_checksum = "5DB1053F1AF828049C2B206D4403C18970ABEF5C18671CA3C2D2ED0DD64F6385" }, + { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" }, + { name = "logging", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "1098FBF10B54B44C2C7FDF0B01C1253CAFACDACABEFB4B0D027803246753E06D" }, + { name = "lustre", version = "5.6.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib", "houdini"], otp_app = "lustre", source = "hex", outer_checksum = "EE558CD4DB9F09FCC16417ADF0183A3C2DAC3E4B21ED3AC0CAE859792AB810CA" }, + { name = "marceau", version = "1.3.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "2D1C27504BEF45005F5DFB18591F8610FB4BFA91744878210BDC464412EC44E9" }, + { name = "mist", version = "6.0.2", build_tools = ["gleam"], requirements = ["exception", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "6B03DEEA38A02F276333CB27B53B16D3D45BD741B89599085A601BAF635F2006" }, + { name = "platform", version = "1.0.0", build_tools = ["gleam"], requirements = [], otp_app = "platform", source = "hex", outer_checksum = "8339420A95AD89AAC0F82F4C3DB8DD401041742D6C3F46132A8739F6AEB75391" }, + { name = "simplifile", version = "2.4.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "7C18AFA4FED0B4CE1FA5B0B4BAC1FA1744427054EA993565F6F3F82E5453170D" }, + { name = "wisp", version = "2.2.2", build_tools = ["gleam"], requirements = ["directories", "exception", "filepath", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_json", "gleam_stdlib", "houdini", "logging", "marceau", "mist", "simplifile"], otp_app = "wisp", source = "hex", outer_checksum = "5FF5F1E288C3437252ABB93D8F9CF42FF652CE7AD54480CFE736038DC09C4F22" }, +] + +[requirements] +gleam_crypto = { version = ">= 1.5.1 and < 2.0.0" } +gleam_erlang = { version = ">= 1.0.0 and < 2.0.0" } +gleam_http = { version = ">= 3.7.2 and < 5.0.0" } +gleam_json = { version = ">= 2.3.0 and < 4.0.0" } +gleam_otp = { version = ">= 1.1.0 and < 2.0.0" } +gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" } +group_registry = { version = ">= 1.0.0 and < 2.0.0" } +lustre = { version = ">= 5.3.5 and < 6.0.0" } +mist = { version = ">= 6.0.0 and < 7.0.0" } +wisp = { version = ">= 2.2.1 and < 3.0.0" } diff --git a/server/priv/static/layout.css b/server/priv/static/layout.css new file mode 100644 index 0000000..94434d9 --- /dev/null +++ b/server/priv/static/layout.css @@ -0,0 +1,276 @@ +/* Reset and Base Styles */ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + background: #000; + color: #00ff00; + font-family: "Courier New", "Courier", monospace; + overflow-x: hidden; + height: 100vh; +} + +/* Terminal Screen Container */ +.terminal-screen { + min-height: 100vh; + background: #000; + padding: 2rem; + padding-bottom: 5rem; /* Space for prompt */ + position: relative; +} + +/* CRT Glow Effect */ +.terminal-glow { + position: relative; + text-shadow: + 0 0 5px rgba(0, 255, 0, 0.8), + 0 0 10px rgba(0, 255, 0, 0.5), + 0 0 20px rgba(0, 255, 0, 0.3); + animation: flicker 0.15s infinite alternate; +} + +/* Scanlines Effect */ +.scanlines { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: repeating-linear-gradient( + 0deg, + rgba(0, 0, 0, 0.15), + rgba(0, 0, 0, 0.15) 1px, + transparent 1px, + transparent 2px + ); + pointer-events: none; + z-index: 1000; + opacity: 0.3; +} + +/* Terminal Header */ +.terminal-header { + text-align: center; + margin-bottom: 3rem; + position: relative; + z-index: 1; +} + +.terminal-title { + font-size: 1.25rem; + line-height: 1.6; + letter-spacing: 0.05em; + color: #00ff00; + margin-bottom: 1rem; + white-space: pre-wrap; + display: inline-block; + text-align: left; +} + +.terminal-status { + font-size: 0.875rem; + margin-top: 1rem; + color: #00aa00; +} + +.status-blink { + animation: blink 1s infinite; + color: #00ff00; +} + +.mb-4 { + margin-bottom: 1rem; +} + +/* Terminal Sections */ +.terminal-section { + margin-bottom: 2rem; + position: relative; + z-index: 1; +} + +.terminal-box { + border: 2px solid #00ff00; + padding: 1rem; + text-align: center; + max-width: 500px; + margin: 0 auto; +} + +.terminal-label { + color: #00ff00; + font-weight: bold; + display: inline-block; +} + +/* Participants Grid */ +.participants-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 1rem; + max-width: 1200px; + margin: 0 auto; +} + +.participant-box { + border: 2px solid #00ff00; + padding: 1rem; + transition: all 0.2s; +} + +.participant-hidden { + border: 0px dashed #005500; + padding: 1rem; + color: #000000; + transition: all 0.2s; +} + +.participant-disconnect { + border: 1px dashed #005500; + padding: 1rem; + transition: all 0.2s; +} + +.participant-box:hover { + background: rgba(0, 255, 0, 0.05); + box-shadow: 0 0 20px rgba(0, 255, 0, 0.3); +} + +.participant-name { + color: #00ff00; + font-weight: bold; + margin-bottom: 0.5rem; + font-size: 1rem; +} + +.participant-answer { + color: #00aa00; + font-size: 0.875rem; +} + +.dim { + color: #006600; +} + +/* Terminal Prompt */ +.terminal-prompt { + position: fixed; + bottom: 0; + left: 0; + right: 0; + background: #000; + border-top: 2px solid #00ff00; + padding: 1rem 2rem; + z-index: 100; +} + +.prompt-line { + font-size: 1.125rem; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.prompt-symbol { + color: #00ff00; + font-weight: bold; +} + +.prompt-text { + color: #00ff00; +} + +/* Blinking Cursor */ +.cursor-blink { + animation: blink 1s infinite; + color: #00ff00; + font-weight: bold; +} + +/* Animations */ +@keyframes blink { + 0%, + 49% { + opacity: 1; + } + 50%, + 100% { + opacity: 0; + } +} + +@keyframes flicker { + 0% { + opacity: 0.98; + } + 100% { + opacity: 1; + } +} + +/* Responsive */ +@media (max-width: 768px) { + .terminal-screen { + padding: 1rem; + padding-bottom: 4rem; + } + + .terminal-title { + font-size: 0.875rem; + } + + .participants-grid { + grid-template-columns: 1fr; + } + + .terminal-prompt { + padding: 0.75rem 1rem; + } + + .prompt-line { + font-size: 0.875rem; + } + + .ml-8 { + margin-left: 0; + display: block; + margin-top: 0.5rem; + } +} + +input { + background-color: #000000; + color: #00ff00; + width: 20cap; +} + +input:focus { + background-color: #000000; + width: 20cap; + border-color: #00ff00; + outline: none; +} + +button { + background-color: #000000; + color: #00ff00; + border: 3px solid #73ad21; +} + +button:focus { + border: 3px solid #534d01; +} + +.controlbutton { + background-color: #000000; + color: #00ff00; + border: 3px solid #73ad21; + width: 100%; +} + +.controlbutton:focus { + border: 3px solid #534d01; +} diff --git a/server/priv/static/root.html b/server/priv/static/root.html new file mode 100644 index 0000000..dd37914 --- /dev/null +++ b/server/priv/static/root.html @@ -0,0 +1,31 @@ + + + + + QUIZTERMINAL v1.0 + + + + + + +
+
+
+
+╔═══════════════════════════════════════╗
+║       Q U I Z T E R M I N A L         ║
+╚═══════════════════════════════════════╝
+
+
+
+
+
+ + diff --git a/server/src/backend/playerhandler.gleam b/server/src/backend/playerhandler.gleam new file mode 100644 index 0000000..1e02e30 --- /dev/null +++ b/server/src/backend/playerhandler.gleam @@ -0,0 +1,262 @@ +import gleam/erlang/process.{type Subject} +import gleam/int +import gleam/list +import gleam/option.{None, Some} +import gleam/otp/actor +import group_registry.{type GroupRegistry} +import shared/message.{ + type AnswerStatus, type NotifyClient, type StateControl, Answer, AnswerQuiz, + Await, GiveAnswer, GiveName, GiveSingleAnswer, GivenAnswer, HasAnswered, + IDontKnow, Lobby, NotAnswered, PingTime, Pong, PurgePlayers, RevealAnswer, + User, +} + +type State { + State( + question_number: Int, + // int in #pair: answer number + slow_answers: List(#(String, List(#(Int, String)))), + // int in #pair: ping counted since response back. + name_answers: List(#(String, #(Int, AnswerStatus))), + hide_answers: Bool, + question: option.Option(String), + state_handler: actor.Started(Subject(StateControl)), + ) +} + +pub fn initialize( + state_handler: actor.Started(Subject(StateControl)), + registry: GroupRegistry(NotifyClient), +) { + actor.new(State(1, [], [], True, None, state_handler)) + |> actor.on_message(fn(state: State, message) { + let question = case state.question { + None -> { + case + actor.call(state_handler.data, 1000, message.FetchQuestion( + state.question_number, + _, + )) + { + Some(question) -> question + None -> "(no question text found)" + } + } + Some(question) -> question + } + + let state = State(..state, question: Some(question)) + + case message { + // Ask all the clients to let us know they are still there by sending a Pong with their name. Schedule + // a new ping as well. Count unacced pings per client + PingTime(sender) -> ping(state, registry, sender) + + // A client has responded to the ping with a pong. Reset the unacced ping count + Pong(name) -> pong(state, name) + + // (Controller) client asks to remove all players from the board + PurgePlayers -> purge_players(state, registry) + + // A new player has signed up, put their name in the registry + GiveName(name) -> give_name(state, registry, name) + + // A player has answered a question, put it in their state. If every player has answered, signal + // to reveal answers (live game) + GiveAnswer(name, answer) -> give_answer(state, registry, name, answer) + + // A player has answered a question in "single" game. Register the answer. + GiveSingleAnswer(name, question, answer) -> { + State( + ..state, + slow_answers: case list.key_find(state.slow_answers, name) { + Ok(l) -> { + list.key_set( + state.slow_answers, + name, + list.key_set(l, question, answer), + ) + } + Error(_) -> { + list.key_set(state.slow_answers, name, [#(question, answer)]) + } + }, + ) + } + // Reveal all answers given by players, setting the game in a "wait for next question" mode + RevealAnswer -> revel_answers(state, registry) + + // Switch from "Wait for next question" to "Answer next question" mode + AnswerQuiz -> answer_quiz(state, registry) + } + |> actor.continue() + }) + |> actor.start +} + +// Reschedule a new ping request, and ask clients to ping us back +fn ping(state, registry, sender) { + broadcast(registry, message.Ping) + process.send_after(sender, 500, message.PingTime(sender)) + State( + ..state, + // Increase ping count with one, + // filter away users with more than 4 missed pings first. + name_answers: list.map( + list.filter(state.name_answers, fn(user) { + let #(_, #(count, _)) = user + count < 8 + }), + fn(user) { + let #(name, #(count, stat)) = user + #(name, #(count + 1, stat)) + }, + ), + ) + |> broadcast_lobby(registry) +} + +fn give_answer(state, registry, name, answer) { + let state = + State( + ..state, + name_answers: list.key_set( + state.name_answers, + name, + #(0, case answer { + Some("?") -> IDontKnow + Some(answer) -> GivenAnswer(answer) + None -> IDontKnow + }), + ), + ) + // Check if everyone has answered, if so, reveal answer. + case + list.filter(state.name_answers, fn(x) { + case x { + #(_, #(_, message.NotAnswered)) -> True + _ -> False + } + }) + |> list.length + { + 0 -> { + broadcast(registry, Await) + State(..state, hide_answers: False) + } + _ -> state + } + |> broadcast_lobby(registry) +} + +fn give_name(state: State, registry, name) { + // Let the new client (and everyone else) know the current question state + case state.hide_answers { + True -> broadcast(registry, Answer) + False -> broadcast(registry, Await) + } + // Add the new user to lobby, and broadcast lobby + State( + ..state, + name_answers: list.key_set(state.name_answers, name, #(0, NotAnswered)), + ) + |> broadcast_lobby(registry) +} + +fn answer_quiz(state, registry) { + // Tell the clients to switch to "answer quiz" mode + broadcast(registry, Answer) + State( + ..state, + name_answers: list.map(state.name_answers, fn(user) { + let #(name, #(count, _)) = user + #(name, #(count, NotAnswered)) + }), + question: None, + question_number: state.question_number + 1, + hide_answers: True, + ) + |> broadcast_lobby(registry) +} + +fn purge_players(state: State, registry) { + broadcast(registry, message.Exit) + State(1, [], [], True, None, state.state_handler) + |> broadcast_lobby(registry) +} + +fn revel_answers(state, registry) { + // Tell the clients to switch to "view answers" mode + broadcast(registry, Await) + State(..state, hide_answers: False) + |> broadcast_lobby(registry) +} + +fn pong(state: State, name) { + // Reset ping count + case list.key_find(state.name_answers, name) { + Ok(#(_, answer)) -> + State( + ..state, + name_answers: list.key_set(state.name_answers, name, #(0, answer)), + ) + Error(_) -> state + } +} + +// Combine the active player answers with the answers given by the "single" player. +fn combine_lists(state: State) { + list.append( + list.map(state.name_answers, fn(name_answer) { + let #(name, #(ping_time, answer)) = name_answer + User(name, ping_time, case answer, state.hide_answers { + GivenAnswer(_), True -> HasAnswered + GivenAnswer(answer), False -> GivenAnswer(answer) + other, _ -> other + }) + }), + // Second list require a bit more work Iterate over each payers answers, + // creating user objects where question number match current question number. + list.flat_map(state.slow_answers, fn(name_answers) { + let #(name, answers) = name_answers + list.filter_map(answers, fn(number_answer) { + let #(answer_number, answer) = number_answer + case state.question_number == answer_number { + True -> { + Ok( + User(name, 0, case state.hide_answers { + True -> HasAnswered + False -> GivenAnswer(answer) + }), + ) + } + False -> Error("ignore") + } + }) + }), + ) +} + +fn broadcast_lobby(state: State, registry: GroupRegistry(NotifyClient)) { + broadcast( + registry, + Lobby( + "Question " + <> int.to_string(state.question_number) + <> ": " + <> case state.question { + Some(question) -> question + None -> "(question not found)" + }, + combine_lists(state), + ), + ) + + state +} + +fn broadcast(registry: GroupRegistry(msg), msg) -> Nil { + use member <- list.each(group_registry.members(registry, "quiz")) + + process.send(member, msg) +} diff --git a/server/src/backend/roomhandler.gleam b/server/src/backend/roomhandler.gleam new file mode 100644 index 0000000..04fd45b --- /dev/null +++ b/server/src/backend/roomhandler.gleam @@ -0,0 +1,68 @@ +import backend/playerhandler as player_handler +import gleam/erlang/process.{type Subject} +import gleam/list +import gleam/option.{Some} +import gleam/otp/actor.{type Started} +import group_registry +import shared/message.{type ClientsServer, type RoomControl, type StateControl} + +// Room handler, actor to hold the rooms for the different teams playing. +// +// Reacts to: +// CreateRoom(id) - create room with given ID. +// +// Responds to: +// FetchRoom(id, ) - Fetch room with the given id. + +type Room { + Room(questions: List(#(Int, String)), rooms: List(#(String, ClientsServer))) +} + +pub fn initialize(state_handler: Started(Subject(StateControl))) { + actor.new(Room([], [])) + |> actor.on_message(fn(state: Room, message: RoomControl(ClientsServer)) { + case message { + message.CreateRoom(id:) -> { + case + // Does room already exist? + state.rooms |> list.key_find(id) + { + Error(_) -> { + // Prevent overflowing server with rooms, set max 200 + case list.length(state.rooms) < 200 { + True -> { + // Room not found (not really an error case), create it. + let name = process.new_name("quiz-registry" <> id) + let assert Ok(actor.Started(data: registry, ..)) = + group_registry.start(name) + let assert Ok(actor) = + player_handler.initialize(state_handler, registry) + process.send_after( + actor.data, + 1000, + message.PingTime(actor.data), + ) + Room(..state, rooms: [#(id, #(registry, actor)), ..state.rooms]) + } + False -> state + } + } + // Room exists, do nothing. + Ok(_) -> state + } + } + message.FetchRoom(id:, subject:) -> { + case + // Find the room, if it exists + state.rooms |> list.key_find(id) + { + Ok(room) -> actor.send(subject, Some(room)) + Error(_) -> actor.send(subject, option.None) + } + state + } + } + |> actor.continue() + }) + |> actor.start +} diff --git a/src/backend/sockethandler.gleam b/server/src/backend/sockethandler.gleam similarity index 56% rename from src/backend/sockethandler.gleam rename to server/src/backend/sockethandler.gleam index 5135a01..335d3f2 100644 --- a/src/backend/sockethandler.gleam +++ b/server/src/backend/sockethandler.gleam @@ -1,23 +1,63 @@ +import gleam/bytes_tree import gleam/erlang/process.{type Selector, type Subject} import gleam/http/request.{type Request} import gleam/http/response.{type Response} import gleam/json -import gleam/option.{type Option, Some} +import gleam/option.{type Option, None, Some} +import gleam/otp/actor import lustre import lustre/server_component import mist.{type Connection, type ResponseData} +import shared/message pub fn serve( request: Request(Connection), component: lustre.App(start_args, model, msg), - start_args: start_args, + id: String, + actor: actor.Started(Subject(message.RoomControl(start_args))), ) -> Response(ResponseData) { - mist.websocket( - request:, - on_init: init_socket(_, component, start_args), - handler: loop_socket, - on_close: close_socket, - ) + let start_args = actor.call(actor.data, 1000, message.FetchRoom(id, _)) + case start_args { + Some(start_args) -> + mist.websocket( + request:, + on_init: init_socket(_, component, start_args), + handler: loop_socket, + on_close: close_socket, + ) + None -> + response.new(404) + |> response.set_body( + bytes_tree.from_string("Requested resource not found") |> mist.Bytes, + ) + } +} + +pub fn serve_slow( + request: Request(Connection), + component: lustre.App(#(List(#(Int, String)), start_args), model, msg), + id: String, + roomhandler: actor.Started(Subject(message.RoomControl(start_args))), + statehandler: actor.Started(Subject(message.StateControl)), +) -> Response(ResponseData) { + let start_args_opt = + actor.call(roomhandler.data, 1000, message.FetchRoom(id, _)) + let answer_list = actor.call(statehandler.data, 1000, message.FetchQuestions) + + case start_args_opt { + Some(start_args) -> + mist.websocket( + request:, + on_init: init_socket(_, component, #(answer_list, start_args)), + handler: loop_socket, + on_close: close_socket, + ) + None -> + response.new(404) + |> response.set_body( + bytes_tree.from_string("Requested resource not found") |> mist.Bytes, + ) + } } type Socket(msg) { @@ -30,7 +70,7 @@ type Socket(msg) { type SocketMessage(msg) = server_component.ClientMessage(msg) -type SocketInit(msg) = +pub type SocketInit(msg) = #(Socket(msg), Option(Selector(SocketMessage(msg)))) fn init_socket( @@ -86,6 +126,6 @@ fn loop_socket( } fn close_socket(state: Socket(msg)) -> Nil { - server_component.deregister_subject(state.self) + lustre.shutdown() |> lustre.send(to: state.component) } diff --git a/server/src/backend/statehandler.gleam b/server/src/backend/statehandler.gleam new file mode 100644 index 0000000..12b3cb7 --- /dev/null +++ b/server/src/backend/statehandler.gleam @@ -0,0 +1,75 @@ +import gleam/list +import gleam/option.{type Option, None, Some} +import gleam/otp/actor +import shared/message.{type StateControl, SetQuestion} + +type State { + State(uri: Option(String), questions: List(#(Int, #(String, String)))) +} + +pub fn initialize() { + actor.new(State(None, [])) + |> actor.on_message(fn(state: State, message: StateControl) { + case message { + SetQuestion(id:, question:) if id >= 0 && id <= 14 -> { + case list.key_find(state.questions, id) { + Ok(#(_, answer)) -> + State( + ..state, + questions: list.key_set(state.questions, id, #(question, answer)), + ) + Error(_) -> + State( + ..state, + questions: list.key_set(state.questions, id, #( + question, + "not provided", + )), + ) + } + } + message.SetAnswer(id:, answer:) if id >= 0 && id <= 14 -> + case list.key_find(state.questions, id) { + Ok(#(question, _)) -> + State( + ..state, + questions: list.key_set(state.questions, id, #(question, answer)), + ) + Error(_) -> + State( + ..state, + questions: list.key_set(state.questions, id, #( + "not provided", + answer, + )), + ) + } + + // Ignore requests for questions/answers not between 1 and 14. + message.SetQuestion(_, _) | message.SetAnswer(_, _) -> state + message.FetchQuestion(id:, subject:) -> { + case + // Find the room, if it exists + list.key_find(state.questions, id) + { + Ok(#(question, _)) -> actor.send(subject, Some(question)) + Error(_) -> actor.send(subject, option.None) + } + state + } + message.SetInfo(uri) -> State(..state, uri: Some(uri)) + message.FetchQuestions(subject) -> { + actor.send( + subject, + list.map(state.questions, fn(x) { + let #(i, #(q, _)) = x + #(i, q) + }), + ) + state + } + } + |> actor.continue() + }) + |> actor.start +} diff --git a/server/src/quizterm.gleam b/server/src/quizterm.gleam new file mode 100644 index 0000000..7030a77 --- /dev/null +++ b/server/src/quizterm.gleam @@ -0,0 +1,103 @@ +import backend/roomhandler +import backend/sockethandler +import backend/statehandler +import gleam/bytes_tree +import gleam/erlang/application +import gleam/erlang/process +import gleam/http/request +import gleam/http/response.{type Response} +import gleam/list +import gleam/option.{None} +import gleam/result +import gleam/string +import mist.{type ResponseData, File} +import web/components/answerlist +import web/components/card +import web/components/control +import web/router +import wisp +import wisp/wisp_mist + +pub fn main() { + wisp.configure_logger() + + let assert Ok(state_handler) = statehandler.initialize() + let assert Ok(room_handler) = roomhandler.initialize(state_handler) + + let assert Ok(_) = + fn(req) { + case request.path_segments(req) { + ["lustre", "runtime.mjs"] -> serve_runtime() + [] | ["index.html"]-> serve_static("root.html") + ["client.js"] -> serve_static("client.js") + ["static", file] -> serve_static(file) + ["socket", "card", id] -> + sockethandler.serve(req, card.component(), id, room_handler) + ["socket", "control", id] -> + sockethandler.serve(req, control.component(), id, room_handler) + ["socket", "slow", id] -> + sockethandler.serve_slow( + req, + answerlist.component(), + id, + room_handler, + state_handler, + ) + _ -> + wisp_mist.handler( + router.handle_request(room_handler, state_handler, _), + "very_secret", + )(req) + } + } + |> mist.new + |> mist.bind("0.0.0.0") + |> mist.port(1234) + |> mist.start + + process.sleep_forever() +} + +fn serve_static(filename: String) { + let assert Ok(priv) = application.priv_directory("quizterm") + let surname = string.split(filename, ".") |> list.last + let path = priv <> "/static/" <> filename + let data = + mist.send_file(path, offset: 0, limit: None) + |> result.map(fn(file) { + echo "SUCCESS " <> filename + response.new(200) + |> response.set_header("Content-Type", case surname { + Ok("css") -> "text/css" + Ok("js") -> "application/javascript" + Ok(_) | Error(_) -> "text/html" + }) + |> response.set_body(file) + }) + |> result.lazy_unwrap(fn() { + echo "FAIL " <> filename + response.new(404) + |> response.set_body( + bytes_tree.from_string("Requested resource not found") |> mist.Bytes, + ) + }) + echo "Attempting to serve file " <> filename <> " was " + + data +} + +fn serve_runtime() -> Response(ResponseData) { + let assert Ok(lustre_priv) = application.priv_directory("lustre") + let file_path = lustre_priv <> "/static/lustre-server-component.mjs" + + case mist.send_file(file_path, offset: 0, limit: None) { + Ok(file) -> + response.new(200) + |> response.prepend_header("content-type", "application/javascript") + |> response.set_body(file) + + Error(_) -> + response.new(404) + |> response.set_body(mist.Bytes(bytes_tree.new())) + } +} diff --git a/server/src/shared/message.gleam b/server/src/shared/message.gleam new file mode 100644 index 0000000..3a6bb11 --- /dev/null +++ b/server/src/shared/message.gleam @@ -0,0 +1,50 @@ +import gleam/erlang/process.{type Subject} +import gleam/option.{type Option} +import gleam/otp/actor.{type Started} +import group_registry.{type GroupRegistry} + +pub type ClientsServer = + #(GroupRegistry(NotifyClient), Started(Subject(NotifyServer))) + +pub type NotifyServer { + PingTime(Subject(NotifyServer)) + Pong(name: String) + AnswerQuiz + RevealAnswer + PurgePlayers + GiveName(name: String) + GiveAnswer(name: String, answer: Option(String)) + GiveSingleAnswer(name: String, question: Int, answer: String) +} + +pub type StateControl { + SetQuestion(id: Int, question: String) + SetAnswer(id: Int, answer: String) + SetInfo(url: String) + FetchQuestion(id: Int, subject: Subject(Option(String))) + FetchQuestions(subject: Subject(List(#(Int, String)))) +} + +pub type RoomControl(msg) { + CreateRoom(id: String) + FetchRoom(id: String, subject: Subject(Option(msg))) +} + +pub type AnswerStatus { + NotAnswered + HasAnswered + IDontKnow + GivenAnswer(answer: String) +} + +pub type NotifyClient { + Ping + Lobby(question: String, names: List(User)) + Answer + Await + Exit +} + +pub type User { + User(name: String, ping_time: Int, answer: AnswerStatus) +} diff --git a/server/src/web/components/answerlist.gleam b/server/src/web/components/answerlist.gleam new file mode 100644 index 0000000..78adcfd --- /dev/null +++ b/server/src/web/components/answerlist.gleam @@ -0,0 +1,182 @@ +import gleam/erlang/process.{type Subject} +import gleam/int +import gleam/list +import gleam/option.{type Option, None, Some} +import gleam/otp/actor.{type Started} +import lustre +import lustre/attribute.{class} +import lustre/effect.{type Effect} +import lustre/element.{type Element} +import lustre/element/html +import shared/message.{type NotifyClient, type NotifyServer} +import web/components/shared.{ + step_prompt, view_input, view_named_input, view_named_keyed_input, view_yes_no, +} + +pub fn component() -> lustre.App( + #(List(#(Int, String)), message.ClientsServer), + Model, + Msg, +) { + lustre.application(init, update, view) +} + +pub opaque type Model { + Model( + state: Msg, + answers: List(#(Int, #(String, String))), + handler: Started(Subject(NotifyServer)), + ) +} + +fn init( + start_args: #(List(#(Int, String)), message.ClientsServer), +) -> #(Model, Effect(Msg)) { + let #(answers, handlers) = start_args + let #(_registry, handler) = handlers + + // Convert a "question number -> question text" array to + // "question number" -> #("question text", "users answer" array + // with blank user answers. + let initial_array = + list.filter(answers, fn(x) { + let #(i, _) = x + i <= 14 && i >= 0 + }) + |> list.map(fn(x) { + let #(a, b) = x + #(a, #(b, "")) + }) + + #(Model(Initial, initial_array, handler), effect.none()) +} + +pub opaque type Msg { + Initial + SharedMessage(message: NotifyClient) + ReceiveName(message: String) + AcceptName(accept: Option(String)) + GiveQuestion(name: String, question: String) + GiveAnswer(name: String, question: Int, answer: String) +} + +fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) { + case msg { + Initial | SharedMessage(_) -> #(model, effect.none()) + AcceptName(None) -> #(Model(Initial, [], model.handler), effect.none()) + AcceptName(Some(name)) -> { + #(Model(..model, state: GiveQuestion(name, "")), effect.none()) + } + GiveQuestion(name, question) -> + case int.parse(question) { + Ok(question) if question >= 1 && question <= 14 -> #( + Model(..model, state: GiveAnswer(name:, question:, answer: "")), + effect.none(), + ) + _ -> #( + Model(..model, state: GiveQuestion(name:, question: "")), + effect.none(), + ) + } + GiveAnswer(name, question, answer) -> { + actor.send( + model.handler.data, + message.GiveSingleAnswer(name:, question:, answer:), + ) + let new_value = case list.key_find(model.answers, question) { + Ok(pair) -> { + let #(a, _) = pair + #(a, answer) + } + Error(_) -> #("", answer) + } + #( + Model( + ..model, + state: GiveQuestion(name, ""), + answers: list.key_set(model.answers, question, new_value), + ), + effect.none(), + ) + } + ReceiveName(_) -> #(Model(..model, state: msg), effect.none()) + } +} + +fn view(model: Model) -> Element(Msg) { + element.fragment([ + html.div([attribute.class("terminal-prompt")], [ + case model.state { + Initial -> + step_prompt( + "Hello stranger. To join the quiz, I need to know your name", + fn() { view_input(ReceiveName) }, + ) + ReceiveName(name) -> + step_prompt( + "Your name is " <> name <> "? Are you absolutely sure???", + fn() { view_yes_no(name, AcceptName) }, + ) + GiveQuestion(name, _) -> + step_prompt( + "Enter the number of the question you want to answer", + fn() { view_named_input(name, GiveQuestion) }, + ) + GiveAnswer(name, question, _) -> + step_prompt( + "Enter the answer to question number " <> int.to_string(question), + fn() { view_named_keyed_input(question, name, GiveAnswer) }, + ) + _ -> html.h3([], [html.text("Waiting for next question")]) + }, + ]), + html.div([class("terminal-header")], [ + html.div([class("terminal-status")], [ + html.span([class("status-blink")], [html.text("●")]), + html.text(" SYSTEM READY"), + html.span([class("ml-8")], [ + case model.state { + Initial -> html.text("STATUS: Please input your name") + ReceiveName(_) -> html.text("STATUS: Please validate your name") + GiveQuestion(_, _) -> html.text("STATUS: Pick question to answer") + GiveAnswer(_, _, _) -> html.text("STATUS: Give your answer") + _ -> html.text("STATUS: Waiting for next question") + }, + ]), + ]), + ]), + terminal_section(model.answers, "[ACTIVE TRANSMISSIONS]", fn(answer) { + content_cell(answer) + }), + ]) +} + +fn terminal_section( + answers: List(#(Int, #(String, String))), + header: String, + extract: fn(#(Int, #(String, String))) -> Element(Msg), +) { + html.div([attribute.class("terminal-section")], [ + html.div([attribute.class("terminal-label mb-4")], [ + html.text(header), + ]), + html.div([attribute.class("participants-grid")], list.map(answers, extract)), + ]) +} + +fn content_cell(answer: #(Int, #(String, String))) -> Element(Msg) { + let #(question, #(question_text, answer)) = answer + html.div( + [ + class("participant-box"), + ], + [ + html.div([class("participant-name")], [ + html.text("► " <> int.to_string(question) <> " " <> question_text), + ]), + html.div([class("participant-answer")], [ + html.text(answer), + ]), + ], + ) +} diff --git a/server/src/web/components/card.gleam b/server/src/web/components/card.gleam new file mode 100644 index 0000000..db71267 --- /dev/null +++ b/server/src/web/components/card.gleam @@ -0,0 +1,275 @@ +import gleam/erlang/process.{type Subject} +import gleam/int +import gleam/list +import gleam/option.{type Option, None, Some} +import gleam/otp/actor.{type Started} +import group_registry.{type GroupRegistry} +import lustre +import lustre/attribute.{class} +import lustre/effect.{type Effect} +import lustre/element.{type Element} +import lustre/element/html +import lustre/server_component +import shared/message.{type NotifyClient, type NotifyServer, type User, User} +import web/components/shared.{ + step_prompt, view_input, view_named_input, view_yes_no, +} + +pub fn component() -> lustre.App(message.ClientsServer, Model, Msg) { + lustre.application(init, update, view) +} + +type State { + AskName + NameOk(String) + WaitForQuiz(String) + Answer(String) +} + +pub opaque type Model { + Model( + state: State, + lobby: #(String, List(User)), + registry: GroupRegistry(NotifyClient), + handler: Started(Subject(NotifyServer)), + ) +} + +fn init(handlers: message.ClientsServer) -> #(Model, Effect(Msg)) { + let #(registry, handler) = handlers + + let model = Model(AskName, #("", []), registry, handler) + #(model, subscribe(registry, SharedMessage)) +} + +fn subscribe( + registry: GroupRegistry(topic), + on_msg handle_msg: fn(topic) -> msg, +) -> Effect(msg) { + use _, _ <- server_component.select + let subject = group_registry.join(registry, "quiz", process.self()) + + let selector = + process.new_selector() + |> process.select_map(subject, handle_msg) + + selector +} + +pub opaque type Msg { + SharedMessage(message: NotifyClient) + ReceiveName(message: String) + AcceptName(accept: Option(String)) + GiveAnswer(name: String, answer: String) +} + +fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) { + let handler = model.handler + + case msg { + ReceiveName(name) -> #(Model(..model, state: NameOk(name)), effect.none()) + AcceptName(Some(name)) -> { + actor.send(handler.data, message.GiveName(name:)) + #(Model(..model, state: WaitForQuiz(name)), effect.none()) + } + AcceptName(None) -> #(Model(..model, state: AskName), effect.none()) + GiveAnswer(name, answer) -> { + actor.send(handler.data, message.GiveAnswer(name, Some(answer))) + #(Model(..model, state: WaitForQuiz(name)), effect.none()) + } + SharedMessage(shared_msg) -> #( + handle_server_message(model, shared_msg), + effect.none(), + ) + } +} + +fn handle_server_message(model: Model, notify_client) { + case notify_client { + message.Lobby(question, lobby) -> Model(..model, lobby: #(question, lobby)) + message.Exit -> Model(AskName, #("", []), model.registry, model.handler) + message.Answer -> + case model.state { + // We are currently waiting for next quiz question, ok to switch to answer mode + WaitForQuiz(name) -> Model(..model, state: Answer(name)) + // We are not in a state to react, ignore switch to answer mode. + _ -> model + } + message.Await -> + case model.state { + Answer(name) -> Model(..model, state: WaitForQuiz(name)) + _ -> model + } + message.Ping -> { + let has_name = case model.state { + Answer(name) -> Some(name) + WaitForQuiz(name) -> Some(name) + _ -> None + } + case has_name { + Some(name) -> actor.send(model.handler.data, message.Pong(name)) + _ -> Nil + } + model + } + } +} + +fn view(model: Model) -> Element(Msg) { + let #(question, lobby) = model.lobby + + element.fragment([ + html.div([attribute.class("terminal-prompt")], [ + case model.state { + AskName -> + step_prompt( + "Hello stranger. To join the quiz, I need to know your name", + fn() { view_input(ReceiveName) }, + ) + NameOk(name) -> + step_prompt( + "Your name is " <> name <> "? Are you absolutely sure???", + fn() { view_yes_no(name, AcceptName) }, + ) + Answer(name) -> + step_prompt( + "The Quiz Lead will now ask the question, and you may answer.", + fn() { view_named_input(name, GiveAnswer) }, + ) + _ -> html.h3([], [html.text("Waiting for next question")]) + }, + ]), + html.div([class("terminal-header")], [ + html.div([class("terminal-status")], [ + html.span([class("status-blink")], [html.text("●")]), + html.text(" SYSTEM READY"), + html.span([class("ml-8")], [ + case model.state { + AskName -> html.text("STATUS: Please input your name") + NameOk(_) -> html.text("STATUS: Please validate your name") + Answer(_) -> + html.div([], [ + html.div([], [html.text("STATUS: Answer the following:")]), + html.div([], [html.text(question)]), + ]) + _ -> html.text("STATUS: Waiting for next question") + }, + ]), + ]), + ]), + html.div([class("terminal-section")], case lobby { + [] -> [] + lobby -> { + let answered = + list.filter(lobby, fn(x) { + case x.answer { + message.IDontKnow | message.HasAnswered | message.GivenAnswer(_) -> + True + _ -> False + } + }) + |> list.length + |> int.to_string + let size = lobby |> list.length |> int.to_string + [ + html.div([attribute.class("terminal-box")], [ + html.span([attribute.class("terminal-label")], [ + html.text("[PROGRESS] "), + ]), + html.text("Answered: "), + case answered == size { + True -> html.text("Everyone!") + False -> html.text(answered <> "/" <> size) + }, + ]), + ] + } + }), + terminal_section( + lobby, + "[ACTIVE TRANSMISSIONS]", + fn(x) { + case x.answer { + message.GivenAnswer(_) | message.HasAnswered -> True + _ -> False + } + }, + fn(user) { + let User(name, ping_time, answer) = user + case answer { + message.GivenAnswer(answer) -> answer + message.HasAnswered -> "Answer Given" + _ -> "Odd State..." + } + |> content_cell(name, ping_time, _) + }, + ), + terminal_section( + lobby, + "[P A S S]", + fn(x) { + case x.answer { + message.IDontKnow -> True + _ -> False + } + }, + fn(user) { + let User(name, ping_time, _) = user + content_cell(name, ping_time, "P.A.S.S :(") + }, + ), + terminal_section( + lobby, + "[AWAITING RESPONSE]", + fn(x) { + case x.answer { + message.NotAnswered -> True + _ -> False + } + }, + fn(user) { + case user { + User(name, ping_time, _) -> + content_cell(name, ping_time, "Not Answered") + } + }, + ), + ]) +} + +fn terminal_section( + lobby: List(User), + header: String, + filter: fn(User) -> Bool, + extract: fn(User) -> Element(Msg), +) { + html.div([attribute.class("terminal-section")], [ + html.div([attribute.class("terminal-label mb-4")], [ + html.text(header), + ]), + html.div( + [attribute.class("participants-grid")], + list.filter(lobby, filter) + |> list.map(extract), + ), + ]) +} + +fn content_cell(header: String, ping_time: Int, content: String) -> Element(Msg) { + html.div( + [ + class(case ping_time > 1 { + True -> "participant-disconnect" + False -> "participant-box" + }), + ], + [ + html.div([class("participant-name")], [ + html.text("► " <> header), + ]), + html.div([class("participant-answer")], [ + html.text(content), + ]), + ], + ) +} diff --git a/src/components/control.gleam b/server/src/web/components/control.gleam similarity index 53% rename from src/components/control.gleam rename to server/src/web/components/control.gleam index 11ac002..8a12746 100644 --- a/src/components/control.gleam +++ b/server/src/web/components/control.gleam @@ -16,11 +16,7 @@ import shared/message.{ type NotifyClient, type NotifyServer, AnswerQuiz, RevealAnswer, } -pub fn component() -> lustre.App( - #(GroupRegistry(NotifyClient), Started(Subject(NotifyServer))), - Model, - Msg, -) { +pub fn component() -> lustre.App(message.ClientsServer, Model, Msg) { lustre.application(init, update, view) } @@ -40,13 +36,11 @@ pub opaque type Model { pub opaque type Msg { AnnounceQuiz AnnounceAnswer - End + PurgePlayers SharedMessage(message: message.NotifyClient) } -fn init( - handlers: #(GroupRegistry(NotifyClient), Started(Subject(NotifyServer))), -) -> #(Model, Effect(Msg)) { +fn init(handlers: message.ClientsServer) -> #(Model, Effect(Msg)) { let #(registry, handler) = handlers let model = Model(state: Quiz, registry:, handler:) @@ -68,42 +62,60 @@ fn subscribe( } fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) { - let registry = model.registry let handler = model.handler - case msg { - AnnounceQuiz -> { - actor.send(handler.data, AnswerQuiz) - #(Model(Quiz, registry:, handler:), effect.none()) - } - AnnounceAnswer -> { - actor.send(handler.data, RevealAnswer) - #(Model(Reveal, registry:, handler:), effect.none()) - } - End -> #(model, effect.none()) - SharedMessage(_) -> #(model, effect.none()) - } + #( + case msg { + PurgePlayers -> { + // Temp removed button to issue this action. + actor.send(handler.data, message.PurgePlayers) + model + } + AnnounceQuiz -> { + actor.send(handler.data, AnswerQuiz) + Model(..model, state: Quiz) + } + AnnounceAnswer -> { + actor.send(handler.data, RevealAnswer) + Model(..model, state: Reveal) + } + SharedMessage(message.Await) -> Model(..model, state: Reveal) + SharedMessage(_) -> model + }, + effect.none(), + ) } fn view(model: Model) -> Element(Msg) { - case model.state { - Quiz -> { + html.div([attribute.class("terminal-section")], [ + html.div([attribute.class("participants-grid")], [ element.fragment([ - keyed.div([attribute.class("control")], [ - #("reveal", view_input("Reveal answers", AnnounceAnswer)), + keyed.div([attribute.class("participand-hidden")], [ + #("reveal", html.text("")), ]), - ]) - } - Reveal -> { - element.fragment([ - keyed.div([attribute.class("control")], [ - #("next", view_input("Ask for next answer", AnnounceQuiz)), + keyed.div([attribute.class("participand-hidden")], [ + #("reveal", html.text("")), ]), - ]) - } - } + keyed.div([attribute.class("control")], [ + #("reveal", html.text("")), + ]), + case model.state { + Quiz -> { + keyed.div([attribute.class("control")], [ + #("reveal", view_button("Reveal answers", AnnounceAnswer)), + ]) + } + Reveal -> { + keyed.div([attribute.class("control")], [ + #("next", view_button("Ask next question", AnnounceQuiz)), + ]) + } + }, + ]), + ]), + ]) } -fn view_input(text: String, on_submit handle_keydown: msg) -> Element(msg) { +fn view_button(text: String, on_submit handle_keydown: msg) -> Element(msg) { let on_keydown = event.on("click", { decode.success(handle_keydown) }) html.button([attribute.class("controlbutton"), on_keydown], [ diff --git a/server/src/web/components/shared.gleam b/server/src/web/components/shared.gleam new file mode 100644 index 0000000..4177701 --- /dev/null +++ b/server/src/web/components/shared.gleam @@ -0,0 +1,104 @@ +import gleam/dynamic/decode +import gleam/option.{type Option, None, Some} +import lustre/attribute +import lustre/element.{type Element} +import lustre/element/html +import lustre/element/keyed +import lustre/event +import lustre/server_component + +// Components use "keyed.div" rather than "html.div" for input fields +// The value of the fields are uncontrolled, so this is needed to +// get a new state between each input, or else the value transfers between +// input fields. +// +// see: https://hexdocs.pm/lustre/lustre/element/keyed.html + +pub fn view_named_input( + name: String, + on_submit handle_keydown: fn(String, String) -> msg, +) -> Element(msg) { + prompt_input( + "nameinput", + key_down(fn(a: String) { decode.success(handle_keydown(name, a)) }, fn() { + decode.failure(handle_keydown(name, ""), "") + }), + ) +} + +pub fn view_named_keyed_input( + question: Int, + name: String, + on_submit handle_keydown: fn(String, Int, String) -> msg, +) -> Element(msg) { + prompt_input( + "keyput", + key_down( + fn(a: String) { decode.success(handle_keydown(name, question, a)) }, + fn() { decode.failure(handle_keydown(name, question, ""), "") }, + ), + ) +} + +pub fn view_input(on_submit handle_keydown: fn(String) -> msg) -> Element(msg) { + prompt_input( + "input", + key_down(fn(a: String) { decode.success(handle_keydown(a)) }, fn() { + decode.failure(handle_keydown(""), "") + }), + ) +} + +pub fn view_yes_no( + accepted: String, + on_submit handle_button: fn(Option(String)) -> msg, +) -> Element(msg) { + html.div([], [ + html.button([event.on_click(handle_button(Some(accepted)))], [ + html.text(" "), + ]), + html.text(" - "), + html.button([event.on_click(handle_button(None))], [html.text(" ")]), + ]) +} + +fn key_down( + success: fn(String) -> decode.Decoder(msg), + fail: fn() -> decode.Decoder(msg), +) { + event.on("keydown", { + use key <- decode.field("key", decode.string) + use value <- decode.subfield(["target", "value"], decode.string) + + case key { + "Enter" if value != "" -> success(value) + _ -> fail() + } + }) + |> server_component.include(["key", "target.value"]) +} + +fn prompt_input(key, on_keydown) { + keyed.div([], [ + #(key <> "header", html.text("$>")), + #( + key, + html.input([ + attribute.type_("text"), + on_keydown, + attribute.autofocus(True), + ]), + ), + ]) +} + +pub fn step_prompt(text: String, fetch: fn() -> Element(a)) { + html.div([attribute.class("prompt-line")], [ + html.div([attribute.class("prompt-text")], [ + html.div([], [ + html.text(text), + ]), + fetch(), + ]), + ]) +} diff --git a/server/src/web/handlers/serve.gleam b/server/src/web/handlers/serve.gleam new file mode 100644 index 0000000..1a5cc57 --- /dev/null +++ b/server/src/web/handlers/serve.gleam @@ -0,0 +1,123 @@ +import gleam/erlang/process.{type Subject} +import gleam/json +import gleam/int +import gleam/option.{None, Some} +import gleam/otp/actor.{type Started} +import lustre/attribute.{class} +import lustre/element +import lustre/element/html.{body, div, head, html, link, meta, script, title} +import lustre/server_component +import shared/message.{ + type ClientsServer, type RoomControl, CreateRoom, FetchRoom, +} +import wisp.{type Response} + +pub fn main_html(content: fn() -> element.Element(a)) -> Response { + html([], [ + head([], [ + meta([attribute.charset("utf-8")]), + meta([ + attribute.name("viewport"), + attribute.content("width=device-width, initial-scale=1.0"), + ]), + title([], "QUIZTERMINAL v1.0"), + script( + [attribute.type_("module"), attribute.src("/lustre/runtime.mjs")], + "", + ), + link([ + attribute.rel("stylesheet"), + attribute.type_("text/css"), + attribute.href("/static/layout.css"), + ]), + ]), + body([], [ + div([class("terminal-screen")], [ + div([class("terminal-glow")], [ + div([class("scanlines")], []), + + // title + div([class("terminal-header")], [ + html.pre([class("terminal-title")], [ + html.text( + " +╔═══════════════════════════════════════╗ +║ Q U I Z T E R M I N A L ║ +╚═══════════════════════════════════════╝ +", + ), + ]), + ]), + // Insert content + content(), + ]), + ]), + ]), + ]) + |> element.to_document_string + |> wisp.html_response(200) +} + +pub fn room(actor: Started(Subject(RoomControl(ClientsServer))), id: String) { + process.send(actor.data, CreateRoom(id)) + status_head("Created room with id " <> id) +} + +pub fn slow( + actor: Started(Subject(RoomControl(ClientsServer))), + id: String, +) -> fn() -> element.Element(a) { + let start_args = actor.call(actor.data, 1000, FetchRoom(id, _)) + case start_args { + Some(_) -> fn() { + div([], [ + server_component.element( + [server_component.route("/socket/slow/" <> id)], + [], + ), + ]) + } + None -> status_head("Could not find that room...") + } +} + +pub fn board( + actor: Started(Subject(RoomControl(ClientsServer))), + id: String, +) -> fn() -> element.Element(a) { + let start_args = actor.call(actor.data, 1000, FetchRoom(id, _)) + case start_args { + Some(_) -> fn() { + div([], [ + server_component.element( + [server_component.route("/socket/card/" <> id)], + [], + ), + server_component.element( + [server_component.route("/socket/control/" <> id)], + [], + ), + ]) + } + None -> status_head("Could not find that room...") + } +} + +pub fn create_json_response(response: #(Int, String, String)) { + let #(code, message, output) = response + wisp.log_info("[api][" <>int.to_string(code)<>"][" <> message<> "]") + json.object([#("response", json.string(output))]) + |> json.to_string + |> wisp.json_response(200) +} + +pub fn status_head(output: String) { + fn() -> element.Element(a) { + html.div([class("terminal-header")], [ + html.div([class("terminal-status")], [ + html.span([class("status-blink")], [html.text("●")]), + html.h2([class("ml-8")], [html.text(output)]), + ]), + ]) + } +} diff --git a/server/src/web/router.gleam b/server/src/web/router.gleam new file mode 100644 index 0000000..330092b --- /dev/null +++ b/server/src/web/router.gleam @@ -0,0 +1,124 @@ +import gleam/bit_array +import gleam/crypto +import gleam/dynamic/decode +import gleam/erlang/process.{type Subject} +import gleam/http +import gleam/int +import gleam/list +import gleam/otp/actor.{type Started} +import shared/message.{type ClientsServer, type RoomControl, type StateControl} +import web/handlers/serve.{board, main_html, room, slow, status_head} +import wisp.{type Request, type Response} + +pub fn handle_request( + room_handler: Started(Subject(RoomControl(ClientsServer))), + state_handler: Started(Subject(StateControl)), + req: Request, +) -> Response { + use req <- middleware(req) + case wisp.path_segments(req) { + ["api", ..path] -> handle_api(state_handler, req, path) + _ -> handle_html(room_handler, req) + } +} + +fn handle_html( + actor: Started(Subject(RoomControl(ClientsServer))), + req: Request, +) -> Response { + case wisp.path_segments(req) { + ["slow", id] -> slow(actor, id) + ["board", id] -> board(actor, id) + ["room", id] -> room(actor, id) + _ -> { + wisp.log_info("No match for request") + status_head("Nothing to see here") + } + } + |> main_html +} + +fn handle_api( + actor: Started(Subject(StateControl)), + req: Request, + path: List(String), +) { + use json <- wisp.require_json(req) + + case list.key_find(req.headers, "x-api-key") { + Ok(key) -> { + case + bit_array.base64_encode(crypto.hash(crypto.Sha256, <>), True) + == "1nIr1fQzs0K9UZAeUcG/67n12iRiviIS6gO5WXyI2+0=" + { + True -> + case req.method, path { + http.Post, ["info"] -> decode_info(actor, json) + http.Post, ["questions"] -> + decode_index_to_text(actor, json, message.SetQuestion) + http.Post, ["answers"] -> + decode_index_to_text(actor, json, message.SetAnswer) + _, _ -> #(404, "bad api apth","Resource not found") + } + False -> { + #(401, "invalid api key","unauthorized") + } + } + } + Error(_) -> { + #(401, "missing api key","unauthorized") + } + } + |> serve.create_json_response +} + +fn decode_info( + actor: Started(Subject(StateControl)), + json_string: decode.Dynamic, +) { + let decode_uri = { + use uri <- decode.field("teaserImage", decode.string) + decode.success(message.SetInfo(uri)) + } + case decode.run(json_string, decode_uri) { + Ok(info) -> { + actor.send(actor.data, info) + #(200, "Updated info", "Updated info") + } + Error(_) -> #(400, "Unable to update info","bad request") + } +} + +fn decode_index_to_text( + actor: Started(Subject(StateControl)), + json_string: decode.Dynamic, + message: fn(Int, String) -> StateControl, +) { + let decode_answer = { + use index <- decode.field("index", decode.int) + use text <- decode.field("text", decode.string) + decode.success(message(index, text)) + } + + case decode.run(json_string, decode.list(decode_answer)) { + Ok(answers) -> { + list.each(answers, fn(answer_question) { + actor.send(actor.data, answer_question) + }) + #(200, "imported " <> int.to_string(list.length(answers)) <> " items.","imported " <> int.to_string(list.length(answers)) <> " items.") + } + Error(_) -> #(400, "Failed to import","bad request") + } +} + +pub fn middleware( + req: wisp.Request, + handle_request: fn(wisp.Request) -> wisp.Response, +) -> wisp.Response { + let req = wisp.method_override(req) + use <- wisp.log_request(req) + use <- wisp.rescue_crashes + use req <- wisp.handle_head(req) + use req <- wisp.csrf_known_header_protection(req) + handle_request(req) +} diff --git a/server/test/live.gleam b/server/test/live.gleam new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/server/test/live.gleam @@ -0,0 +1 @@ + diff --git a/src/backend/statehandler.gleam b/src/backend/statehandler.gleam deleted file mode 100644 index 6edcd13..0000000 --- a/src/backend/statehandler.gleam +++ /dev/null @@ -1,86 +0,0 @@ -import gleam/erlang/process -import gleam/list -import gleam/option.{type Option, None, Some} -import gleam/otp/actor -import group_registry.{type GroupRegistry} -import shared/message.{ - type NotifyClient, Answer, AnswerQuiz, Await, GiveAnswer, GiveName, Lobby, - RevealAnswer, User, -} - -type State { - State(name_answers: List(#(String, Option(String))), hide_answers: Bool) -} - -pub fn initialize(registry: GroupRegistry(NotifyClient)) { - actor.new(State([], True)) - |> actor.on_message(fn(state: State, message) { - case message { - GiveName(name) -> { - // Let the new client know the current question state - case state.hide_answers { - True -> broadcast(registry, Answer) - False -> broadcast(registry, Await) - } - State(list.key_set(state.name_answers, name, None), state.hide_answers) - |> broadcast_lobby(registry) - } - GiveAnswer(name, answer) -> { - State( - list.key_set(state.name_answers, name, Some(answer)), - state.hide_answers, - ) - |> broadcast_lobby(registry) - } - AnswerQuiz -> { - broadcast(registry, Answer) - State( - list.map(state.name_answers, fn(user) { - let #(name, _) = user - #(name, None) - }), - True, - ) - |> broadcast_lobby(registry) - } - RevealAnswer -> { - broadcast(registry, Await) - State(state.name_answers, hide_answers: False) - |> broadcast_lobby(registry) - } - } - |> actor.continue() - }) - |> actor.start -} - -fn broadcast_lobby(state: State, registry: GroupRegistry(NotifyClient)) { - broadcast( - registry, - Lobby( - list.map(state.name_answers, fn(name_answer) { - let #(name, answer) = name_answer - User(name, case answer { - Some(answer) -> - Some(case state.hide_answers { - True -> "Answer" - False -> answer - }) - None -> - case state.hide_answers { - True -> None - False -> Some("No answer") - } - }) - }), - ), - ) - - state -} - -fn broadcast(registry: GroupRegistry(msg), msg) -> Nil { - use member <- list.each(group_registry.members(registry, "quiz")) - - process.send(member, msg) -} diff --git a/src/components/chat.gleam b/src/components/chat.gleam deleted file mode 100644 index 0a1ef34..0000000 --- a/src/components/chat.gleam +++ /dev/null @@ -1,239 +0,0 @@ -import gleam/dynamic/decode -import gleam/erlang/process.{type Subject} -import gleam/list -import gleam/option.{type Option, None, Some} -import gleam/otp/actor.{type Started} -import gleam/string -import group_registry.{type GroupRegistry} -import lustre -import lustre/attribute -import lustre/effect.{type Effect} -import lustre/element.{type Element} -import lustre/element/html -import lustre/element/keyed -import lustre/event -import lustre/server_component -import shared/message.{type NotifyClient, type NotifyServer, type User, User} - -pub fn component() -> lustre.App( - #(GroupRegistry(NotifyClient), Started(Subject(NotifyServer))), - Model, - Msg, -) { - lustre.application(init, update, view) -} - -type State { - AskName - NameOk(String) - WaitForQuiz(String) - Answer(String) -} - -pub opaque type Model { - Model( - state: State, - lobby: List(User), - registry: GroupRegistry(NotifyClient), - handler: Started(Subject(NotifyServer)), - ) -} - -fn init( - handlers: #(GroupRegistry(NotifyClient), Started(Subject(NotifyServer))), -) -> #(Model, Effect(Msg)) { - let #(registry, handler) = handlers - - let model = Model(AskName, [], registry, handler) - #(model, subscribe(registry, SharedMessage)) -} - -fn subscribe( - registry: GroupRegistry(topic), - on_msg handle_msg: fn(topic) -> msg, -) -> Effect(msg) { - use _, _ <- server_component.select - let subject = group_registry.join(registry, "quiz", process.self()) - - let selector = - process.new_selector() - |> process.select_map(subject, handle_msg) - - selector -} - -pub opaque type Msg { - SharedMessage(message: NotifyClient) - ReceiveName(message: String) - AcceptName(accept: Option(String)) - GiveAnswer(name: String, answer: String) -} - -fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) { - let handler = model.handler - - case msg { - ReceiveName(name) -> { - #(Model(..model, state: NameOk(name)), effect.none()) - } - AcceptName(name) -> { - case name { - Some(name) -> { - actor.send(handler.data, message.GiveName(name:)) - #(Model(..model, state: WaitForQuiz(name)), effect.none()) - } - _ -> #(Model(..model, state: AskName), effect.none()) - } - } - GiveAnswer(name, answer) -> { - actor.send(handler.data, message.GiveAnswer(name, answer)) - #(Model(..model, state: WaitForQuiz(name)), effect.none()) - } - - SharedMessage(message:) -> { - case message { - message.Lobby(lobby) -> #(Model(..model, lobby: lobby), effect.none()) - message.Answer -> - case model.state { - WaitForQuiz(name) -> #( - Model(..model, state: Answer(name)), - effect.none(), - ) - _ -> #(model, effect.none()) - } - message.Await -> - case model.state { - Answer(name) -> #( - Model(..model, state: WaitForQuiz(name)), - effect.none(), - ) - _ -> #(model, effect.none()) - } - } - } - } -} - -fn view(model: Model) -> Element(Msg) { - element.fragment([ - keyed.div([attribute.class("center")], [ - #("header", html.h1([], [html.text("QUIZTerminal")])), - case model.state { - AskName -> #( - "name", - view_input("Enter your name to join the quiz: ", ReceiveName), - ) - NameOk(name) -> { - #( - "accept", - view_accept( - "Are you ok with the name " <> name <> "? (y/n)", - name, - AcceptName, - ), - ) - } - Answer(name) -> { - #( - "answer", - view_named_input("Answer the question: ", name, GiveAnswer), - ) - } - _ -> { - #("history", view_ask_question("Waiting for next question")) - } - }, - ]), - html.div( - [attribute.class("under")], - list.map(model.lobby, fn(user) { - let User(name, answer) = user - let answer = case answer { - None -> "waiting..." - Some(answer) -> answer - } - html.div([attribute.class("under_cell")], [ - html.h3([], [ - html.text(name), - ]), - html.text(answer), - ]) - }), - ), - ]) -} - -fn view_ask_question(question: String) -> Element(msg) { - html.text(question) -} - -fn view_accept( - prompt: String, - accepted: String, - on_submit handle_keydown: fn(Option(String)) -> msg, -) -> Element(msg) { - let on_keydown = - event.on("keydown", { - use value <- decode.field("key", decode.string) - let result = case string.lowercase(value) { - "y" -> Some(accepted) - _ -> None - } - decode.success(handle_keydown(result)) - }) - |> server_component.include(["key"]) - - html.div([], [ - html.text(prompt), - html.input([ - attribute.class("input"), - on_keydown, - attribute.autofocus(True), - ]), - ]) -} - -fn view_input( - text: String, - on_submit handle_keydown: fn(String) -> msg, -) -> Element(msg) { - let on_keydown = - event.on("keydown", { - use key <- decode.field("key", decode.string) - use value <- decode.subfield(["target", "value"], decode.string) - - case key { - "Enter" if value != "" -> decode.success(handle_keydown(value)) - _ -> decode.failure(handle_keydown(""), "") - } - }) - |> server_component.include(["key", "target.value"]) - - html.div([], [ - html.text(text), - html.input([attribute.class("input"), on_keydown, attribute.autofocus(True)]), - ]) -} - -fn view_named_input( - text: String, - name: String, - on_submit handle_keydown: fn(String, String) -> msg, -) -> Element(msg) { - let on_keydown = - event.on("keydown", { - use key <- decode.field("key", decode.string) - use value <- decode.subfield(["target", "value"], decode.string) - - case key { - "Enter" if value != "" -> decode.success(handle_keydown(name, value)) - _ -> decode.failure(handle_keydown("", ""), "") - } - }) - |> server_component.include(["key", "target.value"]) - - html.div([], [ - html.text(text), - html.input([attribute.class("input"), on_keydown, attribute.autofocus(True)]), - ]) -} diff --git a/src/quizterm.gleam b/src/quizterm.gleam deleted file mode 100644 index 918f7fc..0000000 --- a/src/quizterm.gleam +++ /dev/null @@ -1,128 +0,0 @@ -import backend/sockethandler -import backend/statehandler -import components/chat -import components/control -import gleam/bytes_tree -import gleam/erlang/application -import gleam/erlang/process -import gleam/http/request.{type Request} -import gleam/http/response.{type Response} -import gleam/option.{None} -import gleam/otp/actor -import gleam/result -import group_registry -import lustre/attribute -import lustre/element -import lustre/element/html.{ - body, div, head, html, img, link, meta, script, title, -} -import lustre/server_component -import mist.{type Connection, type ResponseData} - -pub fn main() { - let name = process.new_name("quiz-registry") - let assert Ok(actor.Started(data: registry, ..)) = group_registry.start(name) - let assert Ok(actor) = statehandler.initialize(registry) - let assert Ok(_) = - fn(request: Request(Connection)) -> Response(ResponseData) { - case request.path_segments(request) { - [] -> serve_html(False) - ["control"] -> serve_html(True) - ["lustre", "runtime.mjs"] -> serve_runtime() - ["static", file] -> serve_static(file) - ["ws"] -> - sockethandler.serve(request, chat.component(), #(registry, actor)) - ["cws"] -> - sockethandler.serve(request, control.component(), #(registry, actor)) - _ -> response.set_body(response.new(404), mist.Bytes(bytes_tree.new())) - } - } - |> mist.new - |> mist.bind("localhost") - |> mist.port(1234) - |> mist.start - - process.sleep_forever() -} - -fn serve_html(control: Bool) -> Response(ResponseData) { - let html = - html([attribute.lang("en")], [ - head([], [ - link([ - attribute.rel("stylesheet"), - attribute.type_("text/css"), - attribute.href("/static/layout.css"), - ]), - meta([attribute.charset("utf-8")]), - meta([ - attribute.name("viewport"), - attribute.content("width=device-width, initial-scale=1"), - ]), - title([], "Quizterm"), - script( - [attribute.type_("module"), attribute.src("/lustre/runtime.mjs")], - "", - ), - ]), - body([], [ - case control { - False -> server_component.element([server_component.route("/ws")], []) - True -> - div([], [ - server_component.element([server_component.route("/ws")], []), - server_component.element([server_component.route("/cws")], []), - ]) - }, - div([attribute.class("under")], [ - div([attribute.class("under_cell_nb")], []), - div([attribute.class("under_cell_nb")], []), - div([attribute.class("under_cell_bn")], [ - img([ - attribute.src("https://gleam.run/images/lucy/lucydebugfail.svg"), - attribute.width(150), - ]), - ]), - ]), - ]), - ]) - |> element.to_document_string_tree - |> bytes_tree.from_string_tree - - response.new(200) - |> response.set_body(mist.Bytes(html)) - |> response.set_header("content-type", "text/html") -} - -fn serve_static(filename: String) { - let assert Ok(priv) = application.priv_directory("quizterm") - let path = priv <> "/" <> filename - mist.send_file(path, offset: 0, limit: None) - |> result.map(fn(file) { - response.new(200) - |> response.set_header("Content-Type", "text/css") - |> response.set_body(file) - }) - |> result.lazy_unwrap(fn() { - response.new(404) - |> response.set_body( - bytes_tree.from_string("Requested resource not found") |> mist.Bytes, - ) - }) -} - -fn serve_runtime() -> Response(ResponseData) { - let assert Ok(lustre_priv) = application.priv_directory("lustre") - let file_path = lustre_priv <> "/static/lustre-server-component.mjs" - - case mist.send_file(file_path, offset: 0, limit: None) { - Ok(file) -> - response.new(200) - |> response.prepend_header("content-type", "application/javascript") - |> response.set_body(file) - - Error(_) -> - response.new(404) - |> response.set_body(mist.Bytes(bytes_tree.new())) - } -} diff --git a/src/shared/message.gleam b/src/shared/message.gleam deleted file mode 100644 index 2968521..0000000 --- a/src/shared/message.gleam +++ /dev/null @@ -1,18 +0,0 @@ -import gleam/option.{type Option} - -pub type NotifyServer { - AnswerQuiz - RevealAnswer - GiveName(name: String) - GiveAnswer(name: String, answer: String) -} - -pub type NotifyClient { - Lobby(names: List(User)) - Answer - Await -} - -pub type User { - User(name: String, answer: Option(String)) -}