commit 39c61b7790e05f520d90ca42c2acba5a2dbfc1d6 Author: Arkaprabha Chakraborty Date: Mon Dec 1 08:29:49 2025 +0530 Initial commit diff --git a/.air.toml b/.air.toml new file mode 100644 index 0000000..8aa5cb8 --- /dev/null +++ b/.air.toml @@ -0,0 +1,46 @@ +root = "." +testdata_dir = "testdata" +tmp_dir = "tmp" + +[build] + args_bin = [] + bin = "./main" + cmd = "make build" + delay = 1000 + exclude_dir = ["assets", "tmp", "vendor", "testdata", "node_modules"] + exclude_file = [] + exclude_regex = ["_test.go", ".*_templ.go"] + exclude_unchanged = false + follow_symlink = false + full_bin = "" + include_dir = [] + include_ext = ["go", "tpl", "tmpl", "html", "templ"] + include_file = [] + kill_delay = "0s" + log = "build-errors.log" + poll = false + poll_interval = 0 + post_cmd = [] + pre_cmd = [] + rerun = false + rerun_delay = 500 + send_interrupt = false + stop_on_error = false + +[color] + app = "" + build = "yellow" + main = "magenta" + runner = "green" + watcher = "cyan" + +[log] + main_only = false + time = false + +[misc] + clean_on_exit = false + +[screen] + clear_on_rebuild = false + keep_scroll = true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..65b8690 --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with "go test -c" +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work +tmp/ + +# IDE specific files +.vscode +.idea + +# .env file +.env + +# Project build +main +*templ.go + +# OS X generated file +.DS_Store + + +# Tailwind CSS +cmd/web/assets/css/output.css +tailwindcss + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..10c438e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +FROM golang:1.24.4-alpine AS build +RUN apk add --no-cache curl libstdc++ libgcc alpine-sdk + +WORKDIR /app + +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . +RUN go install github.com/a-h/templ/cmd/templ@latest && \ + templ generate && \ + curl -sL https://github.com/tailwindlabs/tailwindcss/releases/latest/download/tailwindcss-linux-x64-musl -o tailwindcss && \ + chmod +x tailwindcss && \ + ./tailwindcss -i cmd/web/styles/input.css -o cmd/web/assets/css/output.css + +RUN CGO_ENABLED=1 GOOS=linux go build -o main cmd/api/main.go + +FROM alpine:3.20.1 AS prod +WORKDIR /app +COPY --from=build /app/main /app/main +EXPOSE ${PORT} +CMD ["./main"] + + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d23af9a --- /dev/null +++ b/Makefile @@ -0,0 +1,78 @@ +# Simple Makefile for a Go project + +# Build the application +all: build test +templ-install: + @if ! command -v templ > /dev/null; then \ + read -p "Go's 'templ' is not installed on your machine. Do you want to install it? [Y/n] " choice; \ + if [ "$$choice" != "n" ] && [ "$$choice" != "N" ]; then \ + go install github.com/a-h/templ/cmd/templ@latest; \ + if [ ! -x "$$(command -v templ)" ]; then \ + echo "templ installation failed. Exiting..."; \ + exit 1; \ + fi; \ + else \ + echo "You chose not to install templ. Exiting..."; \ + exit 1; \ + fi; \ + fi +tailwind-install: + + @if [ ! -f tailwindcss ]; then curl -sL https://github.com/tailwindlabs/tailwindcss/releases/latest/download/tailwindcss-macos-x64 -o tailwindcss; fi + @chmod +x tailwindcss + +build: tailwind-install templ-install + @echo "Building..." + @templ generate + @./tailwindcss -i cmd/web/styles/input.css -o cmd/web/assets/css/output.css + @CGO_ENABLED=1 GOOS=linux go build -o main cmd/api/main.go + +# Run the application +run: + @go run cmd/api/main.go +# Create DB container +docker-run: + @if docker compose up --build 2>/dev/null; then \ + : ; \ + else \ + echo "Falling back to Docker Compose V1"; \ + docker-compose up --build; \ + fi + +# Shutdown DB container +docker-down: + @if docker compose down 2>/dev/null; then \ + : ; \ + else \ + echo "Falling back to Docker Compose V1"; \ + docker-compose down; \ + fi + +# Test the application +test: + @echo "Testing..." + @go test ./... -v + +# Clean the binary +clean: + @echo "Cleaning..." + @rm -f main + +# Live Reload +watch: + @if command -v air > /dev/null; then \ + air; \ + echo "Watching...";\ + else \ + read -p "Go's 'air' is not installed on your machine. Do you want to install it? [Y/n] " choice; \ + if [ "$$choice" != "n" ] && [ "$$choice" != "N" ]; then \ + go install github.com/air-verse/air@latest; \ + air; \ + echo "Watching...";\ + else \ + echo "You chose not to install air. Exiting..."; \ + exit 1; \ + fi; \ + fi + +.PHONY: all build run test clean watch tailwind-install templ-install diff --git a/README.md b/README.md new file mode 100644 index 0000000..e960282 --- /dev/null +++ b/README.md @@ -0,0 +1,53 @@ +# Project billit + +One Paragraph of project description goes here + +## Getting Started + +These instructions will get you a copy of the project up and running on your local machine for development and testing purposes. See deployment for notes on how to deploy the project on a live system. + +## MakeFile + +Run build make command with tests +```bash +make all +``` + +Build the application +```bash +make build +``` + +Run the application +```bash +make run +``` +Create DB container +```bash +make docker-run +``` + +Shutdown DB Container +```bash +make docker-down +``` + +DB Integrations Test: +```bash +make itest +``` + +Live reload the application: +```bash +make watch +``` + +Run the test suite: +```bash +make test +``` + +Clean up binary from the last build: +```bash +make clean +``` diff --git a/cmd/api/main.go b/cmd/api/main.go new file mode 100644 index 0000000..11e753b --- /dev/null +++ b/cmd/api/main.go @@ -0,0 +1,58 @@ +package main + +import ( + "context" + "fmt" + "log" + "net/http" + "os/signal" + "syscall" + "time" + + "billit/internal/server" +) + +func gracefulShutdown(apiServer *http.Server, done chan bool) { + // Create context that listens for the interrupt signal from the OS. + ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + defer stop() + + // Listen for the interrupt signal. + <-ctx.Done() + + log.Println("shutting down gracefully, press Ctrl+C again to force") + stop() // Allow Ctrl+C to force shutdown + + // The context is used to inform the server it has 5 seconds to finish + // the request it is currently handling + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := apiServer.Shutdown(ctx); err != nil { + log.Printf("Server forced to shutdown with error: %v", err) + } + + log.Println("Server exiting") + + // Notify the main goroutine that the shutdown is complete + done <- true +} + +func main() { + + server := server.NewServer() + + // Create a done channel to signal when the shutdown is complete + done := make(chan bool, 1) + + // Run graceful shutdown in a separate goroutine + go gracefulShutdown(server, done) + + err := server.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + panic(fmt.Sprintf("http server error: %s", err)) + } + + // Wait for the graceful shutdown to complete + <-done + log.Println("Graceful shutdown complete.") +} diff --git a/cmd/web/assets/js/htmx.min.js b/cmd/web/assets/js/htmx.min.js new file mode 100644 index 0000000..39250e0 --- /dev/null +++ b/cmd/web/assets/js/htmx.min.js @@ -0,0 +1,3521 @@ +var htmx = (function () { + "use strict"; + const Q = { + onLoad: null, + process: null, + on: null, + off: null, + trigger: null, + ajax: null, + find: null, + findAll: null, + closest: null, + values: function (e, t) { + const n = dn(e, t || "post"); + return n.values; + }, + remove: null, + addClass: null, + removeClass: null, + toggleClass: null, + takeClass: null, + swap: null, + defineExtension: null, + removeExtension: null, + logAll: null, + logNone: null, + logger: null, + config: { + historyEnabled: true, + historyCacheSize: 10, + refreshOnHistoryMiss: false, + defaultSwapStyle: "innerHTML", + defaultSwapDelay: 0, + defaultSettleDelay: 20, + includeIndicatorStyles: true, + indicatorClass: "htmx-indicator", + requestClass: "htmx-request", + addedClass: "htmx-added", + settlingClass: "htmx-settling", + swappingClass: "htmx-swapping", + allowEval: true, + allowScriptTags: true, + inlineScriptNonce: "", + inlineStyleNonce: "", + attributesToSettle: ["class", "style", "width", "height"], + withCredentials: false, + timeout: 0, + wsReconnectDelay: "full-jitter", + wsBinaryType: "blob", + disableSelector: "[hx-disable], [data-hx-disable]", + scrollBehavior: "instant", + defaultFocusScroll: false, + getCacheBusterParam: false, + globalViewTransitions: false, + methodsThatUseUrlParams: ["get", "delete"], + selfRequestsOnly: true, + ignoreTitle: false, + scrollIntoViewOnBoost: true, + triggerSpecsCache: null, + disableInheritance: false, + responseHandling: [ + { code: "204", swap: false }, + { code: "[23]..", swap: true }, + { code: "[45]..", swap: false, error: true }, + ], + allowNestedOobSwaps: true, + historyRestoreAsHxRequest: true, + }, + parseInterval: null, + location: location, + _: null, + version: "2.0.6", + }; + Q.onLoad = j; + Q.process = Ft; + Q.on = xe; + Q.off = be; + Q.trigger = ae; + Q.ajax = Ln; + Q.find = f; + Q.findAll = x; + Q.closest = g; + Q.remove = z; + Q.addClass = K; + Q.removeClass = G; + Q.toggleClass = W; + Q.takeClass = Z; + Q.swap = $e; + Q.defineExtension = zn; + Q.removeExtension = $n; + Q.logAll = V; + Q.logNone = _; + Q.parseInterval = d; + Q._ = e; + const n = { + addTriggerHandler: St, + bodyContains: se, + canAccessLocalStorage: B, + findThisElement: Se, + filterValues: yn, + swap: $e, + hasAttribute: s, + getAttributeValue: a, + getClosestAttributeValue: ne, + getClosestMatch: q, + getExpressionVars: Tn, + getHeaders: mn, + getInputValues: dn, + getInternalData: oe, + getSwapSpecification: bn, + getTriggerSpecs: st, + getTarget: Ee, + makeFragment: P, + mergeObjects: le, + makeSettleInfo: Sn, + oobSwap: He, + querySelectorExt: ue, + settleImmediately: Yt, + shouldCancel: ht, + triggerEvent: ae, + triggerErrorEvent: fe, + withExtensions: jt, + }; + const de = ["get", "post", "put", "delete", "patch"]; + const T = de + .map(function (e) { + return "[hx-" + e + "], [data-hx-" + e + "]"; + }) + .join(", "); + function d(e) { + if (e == undefined) { + return undefined; + } + let t = NaN; + if (e.slice(-2) == "ms") { + t = parseFloat(e.slice(0, -2)); + } else if (e.slice(-1) == "s") { + t = parseFloat(e.slice(0, -1)) * 1e3; + } else if (e.slice(-1) == "m") { + t = parseFloat(e.slice(0, -1)) * 1e3 * 60; + } else { + t = parseFloat(e); + } + return isNaN(t) ? undefined : t; + } + function ee(e, t) { + return e instanceof Element && e.getAttribute(t); + } + function s(e, t) { + return ( + !!e.hasAttribute && (e.hasAttribute(t) || e.hasAttribute("data-" + t)) + ); + } + function a(e, t) { + return ee(e, t) || ee(e, "data-" + t); + } + function u(e) { + const t = e.parentElement; + if (!t && e.parentNode instanceof ShadowRoot) return e.parentNode; + return t; + } + function te() { + return document; + } + function y(e, t) { + return e.getRootNode ? e.getRootNode({ composed: t }) : te(); + } + function q(e, t) { + while (e && !t(e)) { + e = u(e); + } + return e || null; + } + function o(e, t, n) { + const r = a(t, n); + const o = a(t, "hx-disinherit"); + var i = a(t, "hx-inherit"); + if (e !== t) { + if (Q.config.disableInheritance) { + if (i && (i === "*" || i.split(" ").indexOf(n) >= 0)) { + return r; + } else { + return null; + } + } + if (o && (o === "*" || o.split(" ").indexOf(n) >= 0)) { + return "unset"; + } + } + return r; + } + function ne(t, n) { + let r = null; + q(t, function (e) { + return !!(r = o(t, ce(e), n)); + }); + if (r !== "unset") { + return r; + } + } + function h(e, t) { + return e instanceof Element && e.matches(t); + } + function A(e) { + const t = /<([a-z][^\/\0>\x20\t\r\n\f]*)/i; + const n = t.exec(e); + if (n) { + return n[1].toLowerCase(); + } else { + return ""; + } + } + function L(e) { + const t = new DOMParser(); + return t.parseFromString(e, "text/html"); + } + function N(e, t) { + while (t.childNodes.length > 0) { + e.append(t.childNodes[0]); + } + } + function r(e) { + const t = te().createElement("script"); + ie(e.attributes, function (e) { + t.setAttribute(e.name, e.value); + }); + t.textContent = e.textContent; + t.async = false; + if (Q.config.inlineScriptNonce) { + t.nonce = Q.config.inlineScriptNonce; + } + return t; + } + function i(e) { + return ( + e.matches("script") && + (e.type === "text/javascript" || e.type === "module" || e.type === "") + ); + } + function I(e) { + Array.from(e.querySelectorAll("script")).forEach((e) => { + if (i(e)) { + const t = r(e); + const n = e.parentNode; + try { + n.insertBefore(t, e); + } catch (e) { + R(e); + } finally { + e.remove(); + } + } + }); + } + function P(e) { + const t = e.replace(/]*)?>[\s\S]*?<\/head>/i, ""); + const n = A(t); + let r; + if (n === "html") { + r = new DocumentFragment(); + const i = L(e); + N(r, i.body); + r.title = i.title; + } else if (n === "body") { + r = new DocumentFragment(); + const i = L(t); + N(r, i.body); + r.title = i.title; + } else { + const i = L( + '", + ); + r = i.querySelector("template").content; + r.title = i.title; + var o = r.querySelector("title"); + if (o && o.parentNode === r) { + o.remove(); + r.title = o.innerText; + } + } + if (r) { + if (Q.config.allowScriptTags) { + I(r); + } else { + r.querySelectorAll("script").forEach((e) => e.remove()); + } + } + return r; + } + function re(e) { + if (e) { + e(); + } + } + function t(e, t) { + return Object.prototype.toString.call(e) === "[object " + t + "]"; + } + function D(e) { + return typeof e === "function"; + } + function k(e) { + return t(e, "Object"); + } + function oe(e) { + const t = "htmx-internal-data"; + let n = e[t]; + if (!n) { + n = e[t] = {}; + } + return n; + } + function M(t) { + const n = []; + if (t) { + for (let e = 0; e < t.length; e++) { + n.push(t[e]); + } + } + return n; + } + function ie(t, n) { + if (t) { + for (let e = 0; e < t.length; e++) { + n(t[e]); + } + } + } + function F(e) { + const t = e.getBoundingClientRect(); + const n = t.top; + const r = t.bottom; + return n < window.innerHeight && r >= 0; + } + function se(e) { + return e.getRootNode({ composed: true }) === document; + } + function X(e) { + return e.trim().split(/\s+/); + } + function le(e, t) { + for (const n in t) { + if (t.hasOwnProperty(n)) { + e[n] = t[n]; + } + } + return e; + } + function v(e) { + try { + return JSON.parse(e); + } catch (e) { + R(e); + return null; + } + } + function B() { + const e = "htmx:sessionStorageTest"; + try { + sessionStorage.setItem(e, e); + sessionStorage.removeItem(e); + return true; + } catch (e) { + return false; + } + } + function U(e) { + const t = new URL(e, "http://x"); + if (t) { + e = t.pathname + t.search; + } + if (e != "/") { + e = e.replace(/\/+$/, ""); + } + return e; + } + function e(e) { + return On(te().body, function () { + return eval(e); + }); + } + function j(t) { + const e = Q.on("htmx:load", function (e) { + t(e.detail.elt); + }); + return e; + } + function V() { + Q.logger = function (e, t, n) { + if (console) { + console.log(t, e, n); + } + }; + } + function _() { + Q.logger = null; + } + function f(e, t) { + if (typeof e !== "string") { + return e.querySelector(t); + } else { + return f(te(), e); + } + } + function x(e, t) { + if (typeof e !== "string") { + return e.querySelectorAll(t); + } else { + return x(te(), e); + } + } + function b() { + return window; + } + function z(e, t) { + e = w(e); + if (t) { + b().setTimeout(function () { + z(e); + e = null; + }, t); + } else { + u(e).removeChild(e); + } + } + function ce(e) { + return e instanceof Element ? e : null; + } + function $(e) { + return e instanceof HTMLElement ? e : null; + } + function J(e) { + return typeof e === "string" ? e : null; + } + function p(e) { + return e instanceof Element || + e instanceof Document || + e instanceof DocumentFragment + ? e + : null; + } + function K(e, t, n) { + e = ce(w(e)); + if (!e) { + return; + } + if (n) { + b().setTimeout(function () { + K(e, t); + e = null; + }, n); + } else { + e.classList && e.classList.add(t); + } + } + function G(e, t, n) { + let r = ce(w(e)); + if (!r) { + return; + } + if (n) { + b().setTimeout(function () { + G(r, t); + r = null; + }, n); + } else { + if (r.classList) { + r.classList.remove(t); + if (r.classList.length === 0) { + r.removeAttribute("class"); + } + } + } + } + function W(e, t) { + e = w(e); + e.classList.toggle(t); + } + function Z(e, t) { + e = w(e); + ie(e.parentElement.children, function (e) { + G(e, t); + }); + K(ce(e), t); + } + function g(e, t) { + e = ce(w(e)); + if (e) { + return e.closest(t); + } + return null; + } + function l(e, t) { + return e.substring(0, t.length) === t; + } + function Y(e, t) { + return e.substring(e.length - t.length) === t; + } + function pe(e) { + const t = e.trim(); + if (l(t, "<") && Y(t, "/>")) { + return t.substring(1, t.length - 2); + } else { + return t; + } + } + function m(t, r, n) { + if (r.indexOf("global ") === 0) { + return m(t, r.slice(7), true); + } + t = w(t); + const o = []; + { + let t = 0; + let n = 0; + for (let e = 0; e < r.length; e++) { + const l = r[e]; + if (l === "," && t === 0) { + o.push(r.substring(n, e)); + n = e + 1; + continue; + } + if (l === "<") { + t++; + } else if (l === "/" && e < r.length - 1 && r[e + 1] === ">") { + t--; + } + } + if (n < r.length) { + o.push(r.substring(n)); + } + } + const i = []; + const s = []; + while (o.length > 0) { + const r = pe(o.shift()); + let e; + if (r.indexOf("closest ") === 0) { + e = g(ce(t), pe(r.slice(8))); + } else if (r.indexOf("find ") === 0) { + e = f(p(t), pe(r.slice(5))); + } else if (r === "next" || r === "nextElementSibling") { + e = ce(t).nextElementSibling; + } else if (r.indexOf("next ") === 0) { + e = ge(t, pe(r.slice(5)), !!n); + } else if (r === "previous" || r === "previousElementSibling") { + e = ce(t).previousElementSibling; + } else if (r.indexOf("previous ") === 0) { + e = me(t, pe(r.slice(9)), !!n); + } else if (r === "document") { + e = document; + } else if (r === "window") { + e = window; + } else if (r === "body") { + e = document.body; + } else if (r === "root") { + e = y(t, !!n); + } else if (r === "host") { + e = t.getRootNode().host; + } else { + s.push(r); + } + if (e) { + i.push(e); + } + } + if (s.length > 0) { + const e = s.join(","); + const c = p(y(t, !!n)); + i.push(...M(c.querySelectorAll(e))); + } + return i; + } + var ge = function (t, e, n) { + const r = p(y(t, n)).querySelectorAll(e); + for (let e = 0; e < r.length; e++) { + const o = r[e]; + if (o.compareDocumentPosition(t) === Node.DOCUMENT_POSITION_PRECEDING) { + return o; + } + } + }; + var me = function (t, e, n) { + const r = p(y(t, n)).querySelectorAll(e); + for (let e = r.length - 1; e >= 0; e--) { + const o = r[e]; + if (o.compareDocumentPosition(t) === Node.DOCUMENT_POSITION_FOLLOWING) { + return o; + } + } + }; + function ue(e, t) { + if (typeof e !== "string") { + return m(e, t)[0]; + } else { + return m(te().body, e)[0]; + } + } + function w(e, t) { + if (typeof e === "string") { + return f(p(t) || document, e); + } else { + return e; + } + } + function ye(e, t, n, r) { + if (D(t)) { + return { target: te().body, event: J(e), listener: t, options: n }; + } else { + return { target: w(e), event: J(t), listener: n, options: r }; + } + } + function xe(t, n, r, o) { + Gn(function () { + const e = ye(t, n, r, o); + e.target.addEventListener(e.event, e.listener, e.options); + }); + const e = D(n); + return e ? n : r; + } + function be(t, n, r) { + Gn(function () { + const e = ye(t, n, r); + e.target.removeEventListener(e.event, e.listener); + }); + return D(n) ? n : r; + } + const ve = te().createElement("output"); + function we(t, n) { + const e = ne(t, n); + if (e) { + if (e === "this") { + return [Se(t, n)]; + } else { + const r = m(t, e); + const o = /(^|,)(\s*)inherit(\s*)($|,)/.test(e); + if (o) { + const i = ce( + q(t, function (e) { + return e !== t && s(ce(e), n); + }), + ); + if (i) { + r.push(...we(i, n)); + } + } + if (r.length === 0) { + R('The selector "' + e + '" on ' + n + " returned no matches!"); + return [ve]; + } else { + return r; + } + } + } + } + function Se(e, t) { + return ce( + q(e, function (e) { + return a(ce(e), t) != null; + }), + ); + } + function Ee(e) { + const t = ne(e, "hx-target"); + if (t) { + if (t === "this") { + return Se(e, "hx-target"); + } else { + return ue(e, t); + } + } else { + const n = oe(e); + if (n.boosted) { + return te().body; + } else { + return e; + } + } + } + function Ce(e) { + return Q.config.attributesToSettle.includes(e); + } + function Oe(t, n) { + ie(t.attributes, function (e) { + if (!n.hasAttribute(e.name) && Ce(e.name)) { + t.removeAttribute(e.name); + } + }); + ie(n.attributes, function (e) { + if (Ce(e.name)) { + t.setAttribute(e.name, e.value); + } + }); + } + function Re(t, e) { + const n = Jn(e); + for (let e = 0; e < n.length; e++) { + const r = n[e]; + try { + if (r.isInlineSwap(t)) { + return true; + } + } catch (e) { + R(e); + } + } + return t === "outerHTML"; + } + function He(e, o, i, t) { + t = t || te(); + let n = "#" + CSS.escape(ee(o, "id")); + let s = "outerHTML"; + if (e === "true") { + } else if (e.indexOf(":") > 0) { + s = e.substring(0, e.indexOf(":")); + n = e.substring(e.indexOf(":") + 1); + } else { + s = e; + } + o.removeAttribute("hx-swap-oob"); + o.removeAttribute("data-hx-swap-oob"); + const r = m(t, n, false); + if (r.length) { + ie(r, function (e) { + let t; + const n = o.cloneNode(true); + t = te().createDocumentFragment(); + t.appendChild(n); + if (!Re(s, e)) { + t = p(n); + } + const r = { shouldSwap: true, target: e, fragment: t }; + if (!ae(e, "htmx:oobBeforeSwap", r)) return; + e = r.target; + if (r.shouldSwap) { + qe(t); + _e(s, e, e, t, i); + Te(); + } + ie(i.elts, function (e) { + ae(e, "htmx:oobAfterSwap", r); + }); + }); + o.parentNode.removeChild(o); + } else { + o.parentNode.removeChild(o); + fe(te().body, "htmx:oobErrorNoTarget", { content: o }); + } + return e; + } + function Te() { + const e = f("#--htmx-preserve-pantry--"); + if (e) { + for (const t of [...e.children]) { + const n = f("#" + t.id); + n.parentNode.moveBefore(t, n); + n.remove(); + } + e.remove(); + } + } + function qe(e) { + ie(x(e, "[hx-preserve], [data-hx-preserve]"), function (e) { + const t = a(e, "id"); + const n = te().getElementById(t); + if (n != null) { + if (e.moveBefore) { + let e = f("#--htmx-preserve-pantry--"); + if (e == null) { + te().body.insertAdjacentHTML( + "afterend", + "
", + ); + e = f("#--htmx-preserve-pantry--"); + } + e.moveBefore(n, null); + } else { + e.parentNode.replaceChild(n, e); + } + } + }); + } + function Ae(l, e, c) { + ie(e.querySelectorAll("[id]"), function (t) { + const n = ee(t, "id"); + if (n && n.length > 0) { + const r = n.replace("'", "\\'"); + const o = t.tagName.replace(":", "\\:"); + const e = p(l); + const i = e && e.querySelector(o + "[id='" + r + "']"); + if (i && i !== e) { + const s = t.cloneNode(); + Oe(t, i); + c.tasks.push(function () { + Oe(t, s); + }); + } + } + }); + } + function Le(e) { + return function () { + G(e, Q.config.addedClass); + Ft(ce(e)); + Ne(p(e)); + ae(e, "htmx:load"); + }; + } + function Ne(e) { + const t = "[autofocus]"; + const n = $(h(e, t) ? e : e.querySelector(t)); + if (n != null) { + n.focus(); + } + } + function c(e, t, n, r) { + Ae(e, n, r); + while (n.childNodes.length > 0) { + const o = n.firstChild; + K(ce(o), Q.config.addedClass); + e.insertBefore(o, t); + if (o.nodeType !== Node.TEXT_NODE && o.nodeType !== Node.COMMENT_NODE) { + r.tasks.push(Le(o)); + } + } + } + function Ie(e, t) { + let n = 0; + while (n < e.length) { + t = ((t << 5) - t + e.charCodeAt(n++)) | 0; + } + return t; + } + function Pe(t) { + let n = 0; + for (let e = 0; e < t.attributes.length; e++) { + const r = t.attributes[e]; + if (r.value) { + n = Ie(r.name, n); + n = Ie(r.value, n); + } + } + return n; + } + function De(t) { + const n = oe(t); + if (n.onHandlers) { + for (let e = 0; e < n.onHandlers.length; e++) { + const r = n.onHandlers[e]; + be(t, r.event, r.listener); + } + delete n.onHandlers; + } + } + function ke(e) { + const t = oe(e); + if (t.timeout) { + clearTimeout(t.timeout); + } + if (t.listenerInfos) { + ie(t.listenerInfos, function (e) { + if (e.on) { + be(e.on, e.trigger, e.listener); + } + }); + } + De(e); + ie(Object.keys(t), function (e) { + if (e !== "firstInitCompleted") delete t[e]; + }); + } + function S(e) { + ae(e, "htmx:beforeCleanupElement"); + ke(e); + ie(e.children, function (e) { + S(e); + }); + } + function Me(t, e, n) { + if (t.tagName === "BODY") { + return Ve(t, e, n); + } + let r; + const o = t.previousSibling; + const i = u(t); + if (!i) { + return; + } + c(i, t, e, n); + if (o == null) { + r = i.firstChild; + } else { + r = o.nextSibling; + } + n.elts = n.elts.filter(function (e) { + return e !== t; + }); + while (r && r !== t) { + if (r instanceof Element) { + n.elts.push(r); + } + r = r.nextSibling; + } + S(t); + t.remove(); + } + function Fe(e, t, n) { + return c(e, e.firstChild, t, n); + } + function Xe(e, t, n) { + return c(u(e), e, t, n); + } + function Be(e, t, n) { + return c(e, null, t, n); + } + function Ue(e, t, n) { + return c(u(e), e.nextSibling, t, n); + } + function je(e) { + S(e); + const t = u(e); + if (t) { + return t.removeChild(e); + } + } + function Ve(e, t, n) { + const r = e.firstChild; + c(e, r, t, n); + if (r) { + while (r.nextSibling) { + S(r.nextSibling); + e.removeChild(r.nextSibling); + } + S(r); + e.removeChild(r); + } + } + function _e(t, e, n, r, o) { + switch (t) { + case "none": + return; + case "outerHTML": + Me(n, r, o); + return; + case "afterbegin": + Fe(n, r, o); + return; + case "beforebegin": + Xe(n, r, o); + return; + case "beforeend": + Be(n, r, o); + return; + case "afterend": + Ue(n, r, o); + return; + case "delete": + je(n); + return; + default: + var i = Jn(e); + for (let e = 0; e < i.length; e++) { + const s = i[e]; + try { + const l = s.handleSwap(t, n, r, o); + if (l) { + if (Array.isArray(l)) { + for (let e = 0; e < l.length; e++) { + const c = l[e]; + if ( + c.nodeType !== Node.TEXT_NODE && + c.nodeType !== Node.COMMENT_NODE + ) { + o.tasks.push(Le(c)); + } + } + } + return; + } + } catch (e) { + R(e); + } + } + if (t === "innerHTML") { + Ve(n, r, o); + } else { + _e(Q.config.defaultSwapStyle, e, n, r, o); + } + } + } + function ze(e, n, r) { + var t = x(e, "[hx-swap-oob], [data-hx-swap-oob]"); + ie(t, function (e) { + if (Q.config.allowNestedOobSwaps || e.parentElement === null) { + const t = a(e, "hx-swap-oob"); + if (t != null) { + He(t, e, n, r); + } + } else { + e.removeAttribute("hx-swap-oob"); + e.removeAttribute("data-hx-swap-oob"); + } + }); + return t.length > 0; + } + function $e(h, d, p, g) { + if (!g) { + g = {}; + } + let m = null; + let n = null; + let e = function () { + re(g.beforeSwapCallback); + h = w(h); + const r = g.contextElement ? y(g.contextElement, false) : te(); + const e = document.activeElement; + let t = {}; + t = { + elt: e, + start: e ? e.selectionStart : null, + end: e ? e.selectionEnd : null, + }; + const o = Sn(h); + if (p.swapStyle === "textContent") { + h.textContent = d; + } else { + let n = P(d); + o.title = g.title || n.title; + if (g.historyRequest) { + n = n.querySelector("[hx-history-elt],[data-hx-history-elt]") || n; + } + if (g.selectOOB) { + const i = g.selectOOB.split(","); + for (let t = 0; t < i.length; t++) { + const s = i[t].split(":", 2); + let e = s[0].trim(); + if (e.indexOf("#") === 0) { + e = e.substring(1); + } + const l = s[1] || "true"; + const c = n.querySelector("#" + e); + if (c) { + He(l, c, o, r); + } + } + } + ze(n, o, r); + ie(x(n, "template"), function (e) { + if (e.content && ze(e.content, o, r)) { + e.remove(); + } + }); + if (g.select) { + const u = te().createDocumentFragment(); + ie(n.querySelectorAll(g.select), function (e) { + u.appendChild(e); + }); + n = u; + } + qe(n); + _e(p.swapStyle, g.contextElement, h, n, o); + Te(); + } + if (t.elt && !se(t.elt) && ee(t.elt, "id")) { + const f = document.getElementById(ee(t.elt, "id")); + const a = { + preventScroll: + p.focusScroll !== undefined + ? !p.focusScroll + : !Q.config.defaultFocusScroll, + }; + if (f) { + if (t.start && f.setSelectionRange) { + try { + f.setSelectionRange(t.start, t.end); + } catch (e) {} + } + f.focus(a); + } + } + h.classList.remove(Q.config.swappingClass); + ie(o.elts, function (e) { + if (e.classList) { + e.classList.add(Q.config.settlingClass); + } + ae(e, "htmx:afterSwap", g.eventInfo); + }); + re(g.afterSwapCallback); + if (!p.ignoreTitle) { + Bn(o.title); + } + const n = function () { + ie(o.tasks, function (e) { + e.call(); + }); + ie(o.elts, function (e) { + if (e.classList) { + e.classList.remove(Q.config.settlingClass); + } + ae(e, "htmx:afterSettle", g.eventInfo); + }); + if (g.anchor) { + const e = ce(w("#" + g.anchor)); + if (e) { + e.scrollIntoView({ block: "start", behavior: "auto" }); + } + } + En(o.elts, p); + re(g.afterSettleCallback); + re(m); + }; + if (p.settleDelay > 0) { + b().setTimeout(n, p.settleDelay); + } else { + n(); + } + }; + let t = Q.config.globalViewTransitions; + if (p.hasOwnProperty("transition")) { + t = p.transition; + } + const r = g.contextElement || te(); + if ( + t && + ae(r, "htmx:beforeTransition", g.eventInfo) && + typeof Promise !== "undefined" && + document.startViewTransition + ) { + const o = new Promise(function (e, t) { + m = e; + n = t; + }); + const i = e; + e = function () { + document.startViewTransition(function () { + i(); + return o; + }); + }; + } + try { + if (p?.swapDelay && p.swapDelay > 0) { + b().setTimeout(e, p.swapDelay); + } else { + e(); + } + } catch (e) { + fe(r, "htmx:swapError", g.eventInfo); + re(n); + throw e; + } + } + function Je(e, t, n) { + const r = e.getResponseHeader(t); + if (r.indexOf("{") === 0) { + const o = v(r); + for (const i in o) { + if (o.hasOwnProperty(i)) { + let e = o[i]; + if (k(e)) { + n = e.target !== undefined ? e.target : n; + } else { + e = { value: e }; + } + ae(n, i, e); + } + } + } else { + const s = r.split(","); + for (let e = 0; e < s.length; e++) { + ae(n, s[e].trim(), []); + } + } + } + const Ke = /\s/; + const E = /[\s,]/; + const Ge = /[_$a-zA-Z]/; + const We = /[_$a-zA-Z0-9]/; + const Ze = ['"', "'", "/"]; + const C = /[^\s]/; + const Ye = /[{(]/; + const Qe = /[})]/; + function et(e) { + const t = []; + let n = 0; + while (n < e.length) { + if (Ge.exec(e.charAt(n))) { + var r = n; + while (We.exec(e.charAt(n + 1))) { + n++; + } + t.push(e.substring(r, n + 1)); + } else if (Ze.indexOf(e.charAt(n)) !== -1) { + const o = e.charAt(n); + var r = n; + n++; + while (n < e.length && e.charAt(n) !== o) { + if (e.charAt(n) === "\\") { + n++; + } + n++; + } + t.push(e.substring(r, n + 1)); + } else { + const i = e.charAt(n); + t.push(i); + } + n++; + } + return t; + } + function tt(e, t, n) { + return ( + Ge.exec(e.charAt(0)) && + e !== "true" && + e !== "false" && + e !== "this" && + e !== n && + t !== "." + ); + } + function nt(r, o, i) { + if (o[0] === "[") { + o.shift(); + let e = 1; + let t = " return (function(" + i + "){ return ("; + let n = null; + while (o.length > 0) { + const s = o[0]; + if (s === "]") { + e--; + if (e === 0) { + if (n === null) { + t = t + "true"; + } + o.shift(); + t += ")})"; + try { + const l = On( + r, + function () { + return Function(t)(); + }, + function () { + return true; + }, + ); + l.source = t; + return l; + } catch (e) { + fe(te().body, "htmx:syntax:error", { error: e, source: t }); + return null; + } + } + } else if (s === "[") { + e++; + } + if (tt(s, n, i)) { + t += + "((" + + i + + "." + + s + + ") ? (" + + i + + "." + + s + + ") : (window." + + s + + "))"; + } else { + t = t + s; + } + n = o.shift(); + } + } + } + function O(e, t) { + let n = ""; + while (e.length > 0 && !t.test(e[0])) { + n += e.shift(); + } + return n; + } + function rt(e) { + let t; + if (e.length > 0 && Ye.test(e[0])) { + e.shift(); + t = O(e, Qe).trim(); + e.shift(); + } else { + t = O(e, E); + } + return t; + } + const ot = "input, textarea, select"; + function it(e, t, n) { + const r = []; + const o = et(t); + do { + O(o, C); + const l = o.length; + const c = O(o, /[,\[\s]/); + if (c !== "") { + if (c === "every") { + const u = { trigger: "every" }; + O(o, C); + u.pollInterval = d(O(o, /[,\[\s]/)); + O(o, C); + var i = nt(e, o, "event"); + if (i) { + u.eventFilter = i; + } + r.push(u); + } else { + const f = { trigger: c }; + var i = nt(e, o, "event"); + if (i) { + f.eventFilter = i; + } + O(o, C); + while (o.length > 0 && o[0] !== ",") { + const a = o.shift(); + if (a === "changed") { + f.changed = true; + } else if (a === "once") { + f.once = true; + } else if (a === "consume") { + f.consume = true; + } else if (a === "delay" && o[0] === ":") { + o.shift(); + f.delay = d(O(o, E)); + } else if (a === "from" && o[0] === ":") { + o.shift(); + if (Ye.test(o[0])) { + var s = rt(o); + } else { + var s = O(o, E); + if ( + s === "closest" || + s === "find" || + s === "next" || + s === "previous" + ) { + o.shift(); + const h = rt(o); + if (h.length > 0) { + s += " " + h; + } + } + } + f.from = s; + } else if (a === "target" && o[0] === ":") { + o.shift(); + f.target = rt(o); + } else if (a === "throttle" && o[0] === ":") { + o.shift(); + f.throttle = d(O(o, E)); + } else if (a === "queue" && o[0] === ":") { + o.shift(); + f.queue = O(o, E); + } else if (a === "root" && o[0] === ":") { + o.shift(); + f[a] = rt(o); + } else if (a === "threshold" && o[0] === ":") { + o.shift(); + f[a] = O(o, E); + } else { + fe(e, "htmx:syntax:error", { token: o.shift() }); + } + O(o, C); + } + r.push(f); + } + } + if (o.length === l) { + fe(e, "htmx:syntax:error", { token: o.shift() }); + } + O(o, C); + } while (o[0] === "," && o.shift()); + if (n) { + n[t] = r; + } + return r; + } + function st(e) { + const t = a(e, "hx-trigger"); + let n = []; + if (t) { + const r = Q.config.triggerSpecsCache; + n = (r && r[t]) || it(e, t, r); + } + if (n.length > 0) { + return n; + } else if (h(e, "form")) { + return [{ trigger: "submit" }]; + } else if (h(e, 'input[type="button"], input[type="submit"]')) { + return [{ trigger: "click" }]; + } else if (h(e, ot)) { + return [{ trigger: "change" }]; + } else { + return [{ trigger: "click" }]; + } + } + function lt(e) { + oe(e).cancelled = true; + } + function ct(e, t, n) { + const r = oe(e); + r.timeout = b().setTimeout(function () { + if (se(e) && r.cancelled !== true) { + if (!pt(n, e, Bt("hx:poll:trigger", { triggerSpec: n, target: e }))) { + t(e); + } + ct(e, t, n); + } + }, n.pollInterval); + } + function ut(e) { + return ( + location.hostname === e.hostname && + ee(e, "href") && + ee(e, "href").indexOf("#") !== 0 + ); + } + function ft(e) { + return g(e, Q.config.disableSelector); + } + function at(t, n, e) { + if ( + (t instanceof HTMLAnchorElement && + ut(t) && + (t.target === "" || t.target === "_self")) || + (t.tagName === "FORM" && + String(ee(t, "method")).toLowerCase() !== "dialog") + ) { + n.boosted = true; + let r, o; + if (t.tagName === "A") { + r = "get"; + o = ee(t, "href"); + } else { + const i = ee(t, "method"); + r = i ? i.toLowerCase() : "get"; + o = ee(t, "action"); + if (o == null || o === "") { + o = location.href; + } + if (r === "get" && o.includes("?")) { + o = o.replace(/\?[^#]+/, ""); + } + } + e.forEach(function (e) { + gt( + t, + function (e, t) { + const n = ce(e); + if (ft(n)) { + S(n); + return; + } + he(r, o, n, t); + }, + n, + e, + true, + ); + }); + } + } + function ht(e, t) { + if (e.type === "submit" || e.type === "click") { + t = ce(e.target) || t; + if (t.tagName === "FORM") { + return true; + } + if (t.form && t.type === "submit") { + return true; + } + t = t.closest("a"); + if ( + t && + t.href && + (t.getAttribute("href") === "#" || + t.getAttribute("href").indexOf("#") !== 0) + ) { + return true; + } + } + return false; + } + function dt(e, t) { + return ( + oe(e).boosted && + e instanceof HTMLAnchorElement && + t.type === "click" && + (t.ctrlKey || t.metaKey) + ); + } + function pt(e, t, n) { + const r = e.eventFilter; + if (r) { + try { + return r.call(t, n) !== true; + } catch (e) { + const o = r.source; + fe(te().body, "htmx:eventFilter:error", { error: e, source: o }); + return true; + } + } + return false; + } + function gt(l, c, e, u, f) { + const a = oe(l); + let t; + if (u.from) { + t = m(l, u.from); + } else { + t = [l]; + } + if (u.changed) { + if (!("lastValue" in a)) { + a.lastValue = new WeakMap(); + } + t.forEach(function (e) { + if (!a.lastValue.has(u)) { + a.lastValue.set(u, new WeakMap()); + } + a.lastValue.get(u).set(e, e.value); + }); + } + ie(t, function (i) { + const s = function (e) { + if (!se(l)) { + i.removeEventListener(u.trigger, s); + return; + } + if (dt(l, e)) { + return; + } + if (f || ht(e, l)) { + e.preventDefault(); + } + if (pt(u, l, e)) { + return; + } + const t = oe(e); + t.triggerSpec = u; + if (t.handledFor == null) { + t.handledFor = []; + } + if (t.handledFor.indexOf(l) < 0) { + t.handledFor.push(l); + if (u.consume) { + e.stopPropagation(); + } + if (u.target && e.target) { + if (!h(ce(e.target), u.target)) { + return; + } + } + if (u.once) { + if (a.triggeredOnce) { + return; + } else { + a.triggeredOnce = true; + } + } + if (u.changed) { + const n = e.target; + const r = n.value; + const o = a.lastValue.get(u); + if (o.has(n) && o.get(n) === r) { + return; + } + o.set(n, r); + } + if (a.delayed) { + clearTimeout(a.delayed); + } + if (a.throttle) { + return; + } + if (u.throttle > 0) { + if (!a.throttle) { + ae(l, "htmx:trigger"); + c(l, e); + a.throttle = b().setTimeout(function () { + a.throttle = null; + }, u.throttle); + } + } else if (u.delay > 0) { + a.delayed = b().setTimeout(function () { + ae(l, "htmx:trigger"); + c(l, e); + }, u.delay); + } else { + ae(l, "htmx:trigger"); + c(l, e); + } + } + }; + if (e.listenerInfos == null) { + e.listenerInfos = []; + } + e.listenerInfos.push({ trigger: u.trigger, listener: s, on: i }); + i.addEventListener(u.trigger, s); + }); + } + let mt = false; + let yt = null; + function xt() { + if (!yt) { + yt = function () { + mt = true; + }; + window.addEventListener("scroll", yt); + window.addEventListener("resize", yt); + setInterval(function () { + if (mt) { + mt = false; + ie( + te().querySelectorAll( + "[hx-trigger*='revealed'],[data-hx-trigger*='revealed']", + ), + function (e) { + bt(e); + }, + ); + } + }, 200); + } + } + function bt(e) { + if (!s(e, "data-hx-revealed") && F(e)) { + e.setAttribute("data-hx-revealed", "true"); + const t = oe(e); + if (t.initHash) { + ae(e, "revealed"); + } else { + e.addEventListener( + "htmx:afterProcessNode", + function () { + ae(e, "revealed"); + }, + { once: true }, + ); + } + } + } + function vt(e, t, n, r) { + const o = function () { + if (!n.loaded) { + n.loaded = true; + ae(e, "htmx:trigger"); + t(e); + } + }; + if (r > 0) { + b().setTimeout(o, r); + } else { + o(); + } + } + function wt(t, n, e) { + let i = false; + ie(de, function (r) { + if (s(t, "hx-" + r)) { + const o = a(t, "hx-" + r); + i = true; + n.path = o; + n.verb = r; + e.forEach(function (e) { + St(t, e, n, function (e, t) { + const n = ce(e); + if (ft(n)) { + S(n); + return; + } + he(r, o, n, t); + }); + }); + } + }); + return i; + } + function St(r, e, t, n) { + if (e.trigger === "revealed") { + xt(); + gt(r, n, t, e); + bt(ce(r)); + } else if (e.trigger === "intersect") { + const o = {}; + if (e.root) { + o.root = ue(r, e.root); + } + if (e.threshold) { + o.threshold = parseFloat(e.threshold); + } + const i = new IntersectionObserver(function (t) { + for (let e = 0; e < t.length; e++) { + const n = t[e]; + if (n.isIntersecting) { + ae(r, "intersect"); + break; + } + } + }, o); + i.observe(ce(r)); + gt(ce(r), n, t, e); + } else if (!t.firstInitCompleted && e.trigger === "load") { + if (!pt(e, r, Bt("load", { elt: r }))) { + vt(ce(r), n, t, e.delay); + } + } else if (e.pollInterval > 0) { + t.polling = true; + ct(ce(r), n, e); + } else { + gt(r, n, t, e); + } + } + function Et(e) { + const t = ce(e); + if (!t) { + return false; + } + const n = t.attributes; + for (let e = 0; e < n.length; e++) { + const r = n[e].name; + if ( + l(r, "hx-on:") || + l(r, "data-hx-on:") || + l(r, "hx-on-") || + l(r, "data-hx-on-") + ) { + return true; + } + } + return false; + } + const Ct = new XPathEvaluator().createExpression( + './/*[@*[ starts-with(name(), "hx-on:") or starts-with(name(), "data-hx-on:") or' + + ' starts-with(name(), "hx-on-") or starts-with(name(), "data-hx-on-") ]]', + ); + function Ot(e, t) { + if (Et(e)) { + t.push(ce(e)); + } + const n = Ct.evaluate(e); + let r = null; + while ((r = n.iterateNext())) t.push(ce(r)); + } + function Rt(e) { + const t = []; + if (e instanceof DocumentFragment) { + for (const n of e.childNodes) { + Ot(n, t); + } + } else { + Ot(e, t); + } + return t; + } + function Ht(e) { + if (e.querySelectorAll) { + const n = + ", [hx-boost] a, [data-hx-boost] a, a[hx-boost], a[data-hx-boost]"; + const r = []; + for (const i in Vn) { + const s = Vn[i]; + if (s.getSelectors) { + var t = s.getSelectors(); + if (t) { + r.push(t); + } + } + } + const o = e.querySelectorAll( + T + + n + + ", form, [type='submit']," + + " [hx-ext], [data-hx-ext], [hx-trigger], [data-hx-trigger]" + + r + .flat() + .map((e) => ", " + e) + .join(""), + ); + return o; + } else { + return []; + } + } + function Tt(e) { + const t = At(e.target); + const n = Nt(e); + if (n) { + n.lastButtonClicked = t; + } + } + function qt(e) { + const t = Nt(e); + if (t) { + t.lastButtonClicked = null; + } + } + function At(e) { + return g(ce(e), "button, input[type='submit']"); + } + function Lt(e) { + return e.form || g(e, "form"); + } + function Nt(e) { + const t = At(e.target); + if (!t) { + return; + } + const n = Lt(t); + return oe(n); + } + function It(e) { + e.addEventListener("click", Tt); + e.addEventListener("focusin", Tt); + e.addEventListener("focusout", qt); + } + function Pt(t, e, n) { + const r = oe(t); + if (!Array.isArray(r.onHandlers)) { + r.onHandlers = []; + } + let o; + const i = function (e) { + On(t, function () { + if (ft(t)) { + return; + } + if (!o) { + o = new Function("event", n); + } + o.call(t, e); + }); + }; + t.addEventListener(e, i); + r.onHandlers.push({ event: e, listener: i }); + } + function Dt(t) { + De(t); + for (let e = 0; e < t.attributes.length; e++) { + const n = t.attributes[e].name; + const r = t.attributes[e].value; + if (l(n, "hx-on") || l(n, "data-hx-on")) { + const o = n.indexOf("-on") + 3; + const i = n.slice(o, o + 1); + if (i === "-" || i === ":") { + let e = n.slice(o + 1); + if (l(e, ":")) { + e = "htmx" + e; + } else if (l(e, "-")) { + e = "htmx:" + e.slice(1); + } else if (l(e, "htmx-")) { + e = "htmx:" + e.slice(5); + } + Pt(t, e, r); + } + } + } + } + function kt(t) { + ae(t, "htmx:beforeProcessNode"); + const n = oe(t); + const e = st(t); + const r = wt(t, n, e); + if (!r) { + if (ne(t, "hx-boost") === "true") { + at(t, n, e); + } else if (s(t, "hx-trigger")) { + e.forEach(function (e) { + St(t, e, n, function () {}); + }); + } + } + if (t.tagName === "FORM" || (ee(t, "type") === "submit" && s(t, "form"))) { + It(t); + } + n.firstInitCompleted = true; + ae(t, "htmx:afterProcessNode"); + } + function Mt(e) { + if (!(e instanceof Element)) { + return false; + } + const t = oe(e); + const n = Pe(e); + if (t.initHash !== n) { + ke(e); + t.initHash = n; + return true; + } + return false; + } + function Ft(e) { + e = w(e); + if (ft(e)) { + S(e); + return; + } + const t = []; + if (Mt(e)) { + t.push(e); + } + ie(Ht(e), function (e) { + if (ft(e)) { + S(e); + return; + } + if (Mt(e)) { + t.push(e); + } + }); + ie(Rt(e), Dt); + ie(t, kt); + } + function Xt(e) { + return e.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase(); + } + function Bt(e, t) { + return new CustomEvent(e, { + bubbles: true, + cancelable: true, + composed: true, + detail: t, + }); + } + function fe(e, t, n) { + ae(e, t, le({ error: t }, n)); + } + function Ut(e) { + return e === "htmx:afterProcessNode"; + } + function jt(e, t, n) { + ie(Jn(e, [], n), function (e) { + try { + t(e); + } catch (e) { + R(e); + } + }); + } + function R(e) { + console.error(e); + } + function ae(e, t, n) { + e = w(e); + if (n == null) { + n = {}; + } + n.elt = e; + const r = Bt(t, n); + if (Q.logger && !Ut(t)) { + Q.logger(e, t, n); + } + if (n.error) { + R(n.error); + ae(e, "htmx:error", { errorInfo: n }); + } + let o = e.dispatchEvent(r); + const i = Xt(t); + if (o && i !== t) { + const s = Bt(i, r.detail); + o = o && e.dispatchEvent(s); + } + jt(ce(e), function (e) { + o = o && e.onEvent(t, r) !== false && !r.defaultPrevented; + }); + return o; + } + let Vt = location.pathname + location.search; + function _t(e) { + Vt = e; + if (B()) { + sessionStorage.setItem("htmx-current-path-for-history", e); + } + } + function zt() { + const e = te().querySelector("[hx-history-elt],[data-hx-history-elt]"); + return e || te().body; + } + function $t(t, e) { + if (!B()) { + return; + } + const n = Kt(e); + const r = te().title; + const o = window.scrollY; + if (Q.config.historyCacheSize <= 0) { + sessionStorage.removeItem("htmx-history-cache"); + return; + } + t = U(t); + const i = v(sessionStorage.getItem("htmx-history-cache")) || []; + for (let e = 0; e < i.length; e++) { + if (i[e].url === t) { + i.splice(e, 1); + break; + } + } + const s = { url: t, content: n, title: r, scroll: o }; + ae(te().body, "htmx:historyItemCreated", { item: s, cache: i }); + i.push(s); + while (i.length > Q.config.historyCacheSize) { + i.shift(); + } + while (i.length > 0) { + try { + sessionStorage.setItem("htmx-history-cache", JSON.stringify(i)); + break; + } catch (e) { + fe(te().body, "htmx:historyCacheError", { cause: e, cache: i }); + i.shift(); + } + } + } + function Jt(t) { + if (!B()) { + return null; + } + t = U(t); + const n = v(sessionStorage.getItem("htmx-history-cache")) || []; + for (let e = 0; e < n.length; e++) { + if (n[e].url === t) { + return n[e]; + } + } + return null; + } + function Kt(e) { + const t = Q.config.requestClass; + const n = e.cloneNode(true); + ie(x(n, "." + t), function (e) { + G(e, t); + }); + ie(x(n, "[data-disabled-by-htmx]"), function (e) { + e.removeAttribute("disabled"); + }); + return n.innerHTML; + } + function Gt() { + const e = zt(); + let t = Vt; + if (B()) { + t = sessionStorage.getItem("htmx-current-path-for-history"); + } + t = t || location.pathname + location.search; + const n = te().querySelector( + '[hx-history="false" i],[data-hx-history="false" i]', + ); + if (!n) { + ae(te().body, "htmx:beforeHistorySave", { path: t, historyElt: e }); + $t(t, e); + } + if (Q.config.historyEnabled) + history.replaceState({ htmx: true }, te().title, location.href); + } + function Wt(e) { + if (Q.config.getCacheBusterParam) { + e = e.replace(/org\.htmx\.cache-buster=[^&]*&?/, ""); + if (Y(e, "&") || Y(e, "?")) { + e = e.slice(0, -1); + } + } + if (Q.config.historyEnabled) { + history.pushState({ htmx: true }, "", e); + } + _t(e); + } + function Zt(e) { + if (Q.config.historyEnabled) history.replaceState({ htmx: true }, "", e); + _t(e); + } + function Yt(e) { + ie(e, function (e) { + e.call(undefined); + }); + } + function Qt(e) { + const t = new XMLHttpRequest(); + const n = { swapStyle: "innerHTML", swapDelay: 0, settleDelay: 0 }; + const r = { path: e, xhr: t, historyElt: zt(), swapSpec: n }; + t.open("GET", e, true); + if (Q.config.historyRestoreAsHxRequest) { + t.setRequestHeader("HX-Request", "true"); + } + t.setRequestHeader("HX-History-Restore-Request", "true"); + t.setRequestHeader("HX-Current-URL", location.href); + t.onload = function () { + if (this.status >= 200 && this.status < 400) { + r.response = this.response; + ae(te().body, "htmx:historyCacheMissLoad", r); + $e(r.historyElt, r.response, n, { + contextElement: r.historyElt, + historyRequest: true, + }); + _t(r.path); + ae(te().body, "htmx:historyRestore", { + path: e, + cacheMiss: true, + serverResponse: r.response, + }); + } else { + fe(te().body, "htmx:historyCacheMissLoadError", r); + } + }; + if (ae(te().body, "htmx:historyCacheMiss", r)) { + t.send(); + } + } + function en(e) { + Gt(); + e = e || location.pathname + location.search; + const t = Jt(e); + if (t) { + const n = { + swapStyle: "innerHTML", + swapDelay: 0, + settleDelay: 0, + scroll: t.scroll, + }; + const r = { path: e, item: t, historyElt: zt(), swapSpec: n }; + if (ae(te().body, "htmx:historyCacheHit", r)) { + $e(r.historyElt, t.content, n, { + contextElement: r.historyElt, + title: t.title, + }); + _t(r.path); + ae(te().body, "htmx:historyRestore", r); + } + } else { + if (Q.config.refreshOnHistoryMiss) { + Q.location.reload(true); + } else { + Qt(e); + } + } + } + function tn(e) { + let t = we(e, "hx-indicator"); + if (t == null) { + t = [e]; + } + ie(t, function (e) { + const t = oe(e); + t.requestCount = (t.requestCount || 0) + 1; + e.classList.add.call(e.classList, Q.config.requestClass); + }); + return t; + } + function nn(e) { + let t = we(e, "hx-disabled-elt"); + if (t == null) { + t = []; + } + ie(t, function (e) { + const t = oe(e); + t.requestCount = (t.requestCount || 0) + 1; + e.setAttribute("disabled", ""); + e.setAttribute("data-disabled-by-htmx", ""); + }); + return t; + } + function rn(e, t) { + ie(e.concat(t), function (e) { + const t = oe(e); + t.requestCount = (t.requestCount || 1) - 1; + }); + ie(e, function (e) { + const t = oe(e); + if (t.requestCount === 0) { + e.classList.remove.call(e.classList, Q.config.requestClass); + } + }); + ie(t, function (e) { + const t = oe(e); + if (t.requestCount === 0) { + e.removeAttribute("disabled"); + e.removeAttribute("data-disabled-by-htmx"); + } + }); + } + function on(t, n) { + for (let e = 0; e < t.length; e++) { + const r = t[e]; + if (r.isSameNode(n)) { + return true; + } + } + return false; + } + function sn(e) { + const t = e; + if ( + t.name === "" || + t.name == null || + t.disabled || + g(t, "fieldset[disabled]") + ) { + return false; + } + if ( + t.type === "button" || + t.type === "submit" || + t.tagName === "image" || + t.tagName === "reset" || + t.tagName === "file" + ) { + return false; + } + if (t.type === "checkbox" || t.type === "radio") { + return t.checked; + } + return true; + } + function ln(t, e, n) { + if (t != null && e != null) { + if (Array.isArray(e)) { + e.forEach(function (e) { + n.append(t, e); + }); + } else { + n.append(t, e); + } + } + } + function cn(t, n, r) { + if (t != null && n != null) { + let e = r.getAll(t); + if (Array.isArray(n)) { + e = e.filter((e) => n.indexOf(e) < 0); + } else { + e = e.filter((e) => e !== n); + } + r.delete(t); + ie(e, (e) => r.append(t, e)); + } + } + function un(e) { + if (e instanceof HTMLSelectElement && e.multiple) { + return M(e.querySelectorAll("option:checked")).map(function (e) { + return e.value; + }); + } + if (e instanceof HTMLInputElement && e.files) { + return M(e.files); + } + return e.value; + } + function fn(t, n, r, e, o) { + if (e == null || on(t, e)) { + return; + } else { + t.push(e); + } + if (sn(e)) { + const i = ee(e, "name"); + ln(i, un(e), n); + if (o) { + an(e, r); + } + } + if (e instanceof HTMLFormElement) { + ie(e.elements, function (e) { + if (t.indexOf(e) >= 0) { + cn(e.name, un(e), n); + } else { + t.push(e); + } + if (o) { + an(e, r); + } + }); + new FormData(e).forEach(function (e, t) { + if (e instanceof File && e.name === "") { + return; + } + ln(t, e, n); + }); + } + } + function an(e, t) { + const n = e; + if (n.willValidate) { + ae(n, "htmx:validation:validate"); + if (!n.checkValidity()) { + t.push({ elt: n, message: n.validationMessage, validity: n.validity }); + ae(n, "htmx:validation:failed", { + message: n.validationMessage, + validity: n.validity, + }); + } + } + } + function hn(n, e) { + for (const t of e.keys()) { + n.delete(t); + } + e.forEach(function (e, t) { + n.append(t, e); + }); + return n; + } + function dn(e, t) { + const n = []; + const r = new FormData(); + const o = new FormData(); + const i = []; + const s = oe(e); + if (s.lastButtonClicked && !se(s.lastButtonClicked)) { + s.lastButtonClicked = null; + } + let l = + (e instanceof HTMLFormElement && e.noValidate !== true) || + a(e, "hx-validate") === "true"; + if (s.lastButtonClicked) { + l = l && s.lastButtonClicked.formNoValidate !== true; + } + if (t !== "get") { + fn(n, o, i, Lt(e), l); + } + fn(n, r, i, e, l); + if ( + s.lastButtonClicked || + e.tagName === "BUTTON" || + (e.tagName === "INPUT" && ee(e, "type") === "submit") + ) { + const u = s.lastButtonClicked || e; + const f = ee(u, "name"); + ln(f, u.value, o); + } + const c = we(e, "hx-include"); + ie(c, function (e) { + fn(n, r, i, ce(e), l); + if (!h(e, "form")) { + ie(p(e).querySelectorAll(ot), function (e) { + fn(n, r, i, e, l); + }); + } + }); + hn(r, o); + return { errors: i, formData: r, values: kn(r) }; + } + function pn(e, t, n) { + if (e !== "") { + e += "&"; + } + if (String(n) === "[object Object]") { + n = JSON.stringify(n); + } + const r = encodeURIComponent(n); + e += encodeURIComponent(t) + "=" + r; + return e; + } + function gn(e) { + e = Pn(e); + let n = ""; + e.forEach(function (e, t) { + n = pn(n, t, e); + }); + return n; + } + function mn(e, t, n) { + const r = { + "HX-Request": "true", + "HX-Trigger": ee(e, "id"), + "HX-Trigger-Name": ee(e, "name"), + "HX-Target": a(t, "id"), + "HX-Current-URL": location.href, + }; + Cn(e, "hx-headers", false, r); + if (n !== undefined) { + r["HX-Prompt"] = n; + } + if (oe(e).boosted) { + r["HX-Boosted"] = "true"; + } + return r; + } + function yn(n, e) { + const t = ne(e, "hx-params"); + if (t) { + if (t === "none") { + return new FormData(); + } else if (t === "*") { + return n; + } else if (t.indexOf("not ") === 0) { + ie(t.slice(4).split(","), function (e) { + e = e.trim(); + n.delete(e); + }); + return n; + } else { + const r = new FormData(); + ie(t.split(","), function (t) { + t = t.trim(); + if (n.has(t)) { + n.getAll(t).forEach(function (e) { + r.append(t, e); + }); + } + }); + return r; + } + } else { + return n; + } + } + function xn(e) { + return !!ee(e, "href") && ee(e, "href").indexOf("#") >= 0; + } + function bn(e, t) { + const n = t || ne(e, "hx-swap"); + const r = { + swapStyle: oe(e).boosted ? "innerHTML" : Q.config.defaultSwapStyle, + swapDelay: Q.config.defaultSwapDelay, + settleDelay: Q.config.defaultSettleDelay, + }; + if (Q.config.scrollIntoViewOnBoost && oe(e).boosted && !xn(e)) { + r.show = "top"; + } + if (n) { + const s = X(n); + if (s.length > 0) { + for (let e = 0; e < s.length; e++) { + const l = s[e]; + if (l.indexOf("swap:") === 0) { + r.swapDelay = d(l.slice(5)); + } else if (l.indexOf("settle:") === 0) { + r.settleDelay = d(l.slice(7)); + } else if (l.indexOf("transition:") === 0) { + r.transition = l.slice(11) === "true"; + } else if (l.indexOf("ignoreTitle:") === 0) { + r.ignoreTitle = l.slice(12) === "true"; + } else if (l.indexOf("scroll:") === 0) { + const c = l.slice(7); + var o = c.split(":"); + const u = o.pop(); + var i = o.length > 0 ? o.join(":") : null; + r.scroll = u; + r.scrollTarget = i; + } else if (l.indexOf("show:") === 0) { + const f = l.slice(5); + var o = f.split(":"); + const a = o.pop(); + var i = o.length > 0 ? o.join(":") : null; + r.show = a; + r.showTarget = i; + } else if (l.indexOf("focus-scroll:") === 0) { + const h = l.slice("focus-scroll:".length); + r.focusScroll = h == "true"; + } else if (e == 0) { + r.swapStyle = l; + } else { + R("Unknown modifier in hx-swap: " + l); + } + } + } + } + return r; + } + function vn(e) { + return ( + ne(e, "hx-encoding") === "multipart/form-data" || + (h(e, "form") && ee(e, "enctype") === "multipart/form-data") + ); + } + function wn(t, n, r) { + let o = null; + jt(n, function (e) { + if (o == null) { + o = e.encodeParameters(t, r, n); + } + }); + if (o != null) { + return o; + } else { + if (vn(n)) { + return hn(new FormData(), Pn(r)); + } else { + return gn(r); + } + } + } + function Sn(e) { + return { tasks: [], elts: [e] }; + } + function En(e, t) { + const n = e[0]; + const r = e[e.length - 1]; + if (t.scroll) { + var o = null; + if (t.scrollTarget) { + o = ce(ue(n, t.scrollTarget)); + } + if (t.scroll === "top" && (n || o)) { + o = o || n; + o.scrollTop = 0; + } + if (t.scroll === "bottom" && (r || o)) { + o = o || r; + o.scrollTop = o.scrollHeight; + } + if (typeof t.scroll === "number") { + b().setTimeout(function () { + window.scrollTo(0, t.scroll); + }, 0); + } + } + if (t.show) { + var o = null; + if (t.showTarget) { + let e = t.showTarget; + if (t.showTarget === "window") { + e = "body"; + } + o = ce(ue(n, e)); + } + if (t.show === "top" && (n || o)) { + o = o || n; + o.scrollIntoView({ block: "start", behavior: Q.config.scrollBehavior }); + } + if (t.show === "bottom" && (r || o)) { + o = o || r; + o.scrollIntoView({ block: "end", behavior: Q.config.scrollBehavior }); + } + } + } + function Cn(r, e, o, i, s) { + if (i == null) { + i = {}; + } + if (r == null) { + return i; + } + const l = a(r, e); + if (l) { + let e = l.trim(); + let t = o; + if (e === "unset") { + return null; + } + if (e.indexOf("javascript:") === 0) { + e = e.slice(11); + t = true; + } else if (e.indexOf("js:") === 0) { + e = e.slice(3); + t = true; + } + if (e.indexOf("{") !== 0) { + e = "{" + e + "}"; + } + let n; + if (t) { + n = On( + r, + function () { + if (s) { + return Function("event", "return (" + e + ")").call(r, s); + } else { + return Function("return (" + e + ")").call(r); + } + }, + {}, + ); + } else { + n = v(e); + } + for (const c in n) { + if (n.hasOwnProperty(c)) { + if (i[c] == null) { + i[c] = n[c]; + } + } + } + } + return Cn(ce(u(r)), e, o, i, s); + } + function On(e, t, n) { + if (Q.config.allowEval) { + return t(); + } else { + fe(e, "htmx:evalDisallowedError"); + return n; + } + } + function Rn(e, t, n) { + return Cn(e, "hx-vars", true, n, t); + } + function Hn(e, t, n) { + return Cn(e, "hx-vals", false, n, t); + } + function Tn(e, t) { + return le(Rn(e, t), Hn(e, t)); + } + function qn(t, n, r) { + if (r !== null) { + try { + t.setRequestHeader(n, r); + } catch (e) { + t.setRequestHeader(n, encodeURIComponent(r)); + t.setRequestHeader(n + "-URI-AutoEncoded", "true"); + } + } + } + function An(t) { + if (t.responseURL) { + try { + const e = new URL(t.responseURL); + return e.pathname + e.search; + } catch (e) { + fe(te().body, "htmx:badResponseUrl", { url: t.responseURL }); + } + } + } + function H(e, t) { + return t.test(e.getAllResponseHeaders()); + } + function Ln(t, n, r) { + t = t.toLowerCase(); + if (r) { + if (r instanceof Element || typeof r === "string") { + return he(t, n, null, null, { + targetOverride: w(r) || ve, + returnPromise: true, + }); + } else { + let e = w(r.target); + if ((r.target && !e) || (r.source && !e && !w(r.source))) { + e = ve; + } + return he(t, n, w(r.source), r.event, { + handler: r.handler, + headers: r.headers, + values: r.values, + targetOverride: e, + swapOverride: r.swap, + select: r.select, + returnPromise: true, + }); + } + } else { + return he(t, n, null, null, { returnPromise: true }); + } + } + function Nn(e) { + const t = []; + while (e) { + t.push(e); + e = e.parentElement; + } + return t; + } + function In(e, t, n) { + const r = new URL( + t, + location.protocol !== "about:" ? location.href : window.origin, + ); + const o = location.protocol !== "about:" ? location.origin : window.origin; + const i = o === r.origin; + if (Q.config.selfRequestsOnly) { + if (!i) { + return false; + } + } + return ae(e, "htmx:validateUrl", le({ url: r, sameHost: i }, n)); + } + function Pn(e) { + if (e instanceof FormData) return e; + const t = new FormData(); + for (const n in e) { + if (e.hasOwnProperty(n)) { + if (e[n] && typeof e[n].forEach === "function") { + e[n].forEach(function (e) { + t.append(n, e); + }); + } else if (typeof e[n] === "object" && !(e[n] instanceof Blob)) { + t.append(n, JSON.stringify(e[n])); + } else { + t.append(n, e[n]); + } + } + } + return t; + } + function Dn(r, o, e) { + return new Proxy(e, { + get: function (t, e) { + if (typeof e === "number") return t[e]; + if (e === "length") return t.length; + if (e === "push") { + return function (e) { + t.push(e); + r.append(o, e); + }; + } + if (typeof t[e] === "function") { + return function () { + t[e].apply(t, arguments); + r.delete(o); + t.forEach(function (e) { + r.append(o, e); + }); + }; + } + if (t[e] && t[e].length === 1) { + return t[e][0]; + } else { + return t[e]; + } + }, + set: function (e, t, n) { + e[t] = n; + r.delete(o); + e.forEach(function (e) { + r.append(o, e); + }); + return true; + }, + }); + } + function kn(o) { + return new Proxy(o, { + get: function (e, t) { + if (typeof t === "symbol") { + const r = Reflect.get(e, t); + if (typeof r === "function") { + return function () { + return r.apply(o, arguments); + }; + } else { + return r; + } + } + if (t === "toJSON") { + return () => Object.fromEntries(o); + } + if (t in e) { + if (typeof e[t] === "function") { + return function () { + return o[t].apply(o, arguments); + }; + } + } + const n = o.getAll(t); + if (n.length === 0) { + return undefined; + } else if (n.length === 1) { + return n[0]; + } else { + return Dn(e, t, n); + } + }, + set: function (t, n, e) { + if (typeof n !== "string") { + return false; + } + t.delete(n); + if (e && typeof e.forEach === "function") { + e.forEach(function (e) { + t.append(n, e); + }); + } else if (typeof e === "object" && !(e instanceof Blob)) { + t.append(n, JSON.stringify(e)); + } else { + t.append(n, e); + } + return true; + }, + deleteProperty: function (e, t) { + if (typeof t === "string") { + e.delete(t); + } + return true; + }, + ownKeys: function (e) { + return Reflect.ownKeys(Object.fromEntries(e)); + }, + getOwnPropertyDescriptor: function (e, t) { + return Reflect.getOwnPropertyDescriptor(Object.fromEntries(e), t); + }, + }); + } + function he(t, n, r, o, i, k) { + let s = null; + let l = null; + i = i != null ? i : {}; + if (i.returnPromise && typeof Promise !== "undefined") { + var e = new Promise(function (e, t) { + s = e; + l = t; + }); + } + if (r == null) { + r = te().body; + } + const M = i.handler || jn; + const F = i.select || null; + if (!se(r)) { + re(s); + return e; + } + const c = i.targetOverride || ce(Ee(r)); + if (c == null || c == ve) { + fe(r, "htmx:targetError", { target: ne(r, "hx-target") }); + re(l); + return e; + } + let u = oe(r); + const f = u.lastButtonClicked; + if (f) { + const A = ee(f, "formaction"); + if (A != null) { + n = A; + } + const L = ee(f, "formmethod"); + if (L != null) { + if (de.includes(L.toLowerCase())) { + t = L; + } else { + re(s); + return e; + } + } + } + const a = ne(r, "hx-confirm"); + if (k === undefined) { + const K = function (e) { + return he(t, n, r, o, i, !!e); + }; + const G = { + target: c, + elt: r, + path: n, + verb: t, + triggeringEvent: o, + etc: i, + issueRequest: K, + question: a, + }; + if (ae(r, "htmx:confirm", G) === false) { + re(s); + return e; + } + } + let h = r; + let d = ne(r, "hx-sync"); + let p = null; + let X = false; + if (d) { + const N = d.split(":"); + const I = N[0].trim(); + if (I === "this") { + h = Se(r, "hx-sync"); + } else { + h = ce(ue(r, I)); + } + d = (N[1] || "drop").trim(); + u = oe(h); + if (d === "drop" && u.xhr && u.abortable !== true) { + re(s); + return e; + } else if (d === "abort") { + if (u.xhr) { + re(s); + return e; + } else { + X = true; + } + } else if (d === "replace") { + ae(h, "htmx:abort"); + } else if (d.indexOf("queue") === 0) { + const W = d.split(" "); + p = (W[1] || "last").trim(); + } + } + if (u.xhr) { + if (u.abortable) { + ae(h, "htmx:abort"); + } else { + if (p == null) { + if (o) { + const P = oe(o); + if (P && P.triggerSpec && P.triggerSpec.queue) { + p = P.triggerSpec.queue; + } + } + if (p == null) { + p = "last"; + } + } + if (u.queuedRequests == null) { + u.queuedRequests = []; + } + if (p === "first" && u.queuedRequests.length === 0) { + u.queuedRequests.push(function () { + he(t, n, r, o, i); + }); + } else if (p === "all") { + u.queuedRequests.push(function () { + he(t, n, r, o, i); + }); + } else if (p === "last") { + u.queuedRequests = []; + u.queuedRequests.push(function () { + he(t, n, r, o, i); + }); + } + re(s); + return e; + } + } + const g = new XMLHttpRequest(); + u.xhr = g; + u.abortable = X; + const m = function () { + u.xhr = null; + u.abortable = false; + if (u.queuedRequests != null && u.queuedRequests.length > 0) { + const e = u.queuedRequests.shift(); + e(); + } + }; + const B = ne(r, "hx-prompt"); + if (B) { + var y = prompt(B); + if (y === null || !ae(r, "htmx:prompt", { prompt: y, target: c })) { + re(s); + m(); + return e; + } + } + if (a && !k) { + if (!confirm(a)) { + re(s); + m(); + return e; + } + } + let x = mn(r, c, y); + if (t !== "get" && !vn(r)) { + x["Content-Type"] = "application/x-www-form-urlencoded"; + } + if (i.headers) { + x = le(x, i.headers); + } + const U = dn(r, t); + let b = U.errors; + const j = U.formData; + if (i.values) { + hn(j, Pn(i.values)); + } + const V = Pn(Tn(r, o)); + const v = hn(j, V); + let w = yn(v, r); + if (Q.config.getCacheBusterParam && t === "get") { + w.set("org.htmx.cache-buster", ee(c, "id") || "true"); + } + if (n == null || n === "") { + n = location.href; + } + const S = Cn(r, "hx-request"); + const _ = oe(r).boosted; + let E = Q.config.methodsThatUseUrlParams.indexOf(t) >= 0; + const C = { + boosted: _, + useUrlParams: E, + formData: w, + parameters: kn(w), + unfilteredFormData: v, + unfilteredParameters: kn(v), + headers: x, + elt: r, + target: c, + verb: t, + errors: b, + withCredentials: + i.credentials || S.credentials || Q.config.withCredentials, + timeout: i.timeout || S.timeout || Q.config.timeout, + path: n, + triggeringEvent: o, + }; + if (!ae(r, "htmx:configRequest", C)) { + re(s); + m(); + return e; + } + n = C.path; + t = C.verb; + x = C.headers; + w = Pn(C.parameters); + b = C.errors; + E = C.useUrlParams; + if (b && b.length > 0) { + ae(r, "htmx:validation:halted", C); + re(s); + m(); + return e; + } + const z = n.split("#"); + const $ = z[0]; + const O = z[1]; + let R = n; + if (E) { + R = $; + const Z = !w.keys().next().done; + if (Z) { + if (R.indexOf("?") < 0) { + R += "?"; + } else { + R += "&"; + } + R += gn(w); + if (O) { + R += "#" + O; + } + } + } + if (!In(r, R, C)) { + fe(r, "htmx:invalidPath", C); + re(l); + m(); + return e; + } + g.open(t.toUpperCase(), R, true); + g.overrideMimeType("text/html"); + g.withCredentials = C.withCredentials; + g.timeout = C.timeout; + if (S.noHeaders) { + } else { + for (const D in x) { + if (x.hasOwnProperty(D)) { + const Y = x[D]; + qn(g, D, Y); + } + } + } + const H = { + xhr: g, + target: c, + requestConfig: C, + etc: i, + boosted: _, + select: F, + pathInfo: { + requestPath: n, + finalRequestPath: R, + responsePath: null, + anchor: O, + }, + }; + g.onload = function () { + try { + const t = Nn(r); + H.pathInfo.responsePath = An(g); + M(r, H); + if (H.keepIndicators !== true) { + rn(T, q); + } + ae(r, "htmx:afterRequest", H); + ae(r, "htmx:afterOnLoad", H); + if (!se(r)) { + let e = null; + while (t.length > 0 && e == null) { + const n = t.shift(); + if (se(n)) { + e = n; + } + } + if (e) { + ae(e, "htmx:afterRequest", H); + ae(e, "htmx:afterOnLoad", H); + } + } + re(s); + } catch (e) { + fe(r, "htmx:onLoadError", le({ error: e }, H)); + throw e; + } finally { + m(); + } + }; + g.onerror = function () { + rn(T, q); + fe(r, "htmx:afterRequest", H); + fe(r, "htmx:sendError", H); + re(l); + m(); + }; + g.onabort = function () { + rn(T, q); + fe(r, "htmx:afterRequest", H); + fe(r, "htmx:sendAbort", H); + re(l); + m(); + }; + g.ontimeout = function () { + rn(T, q); + fe(r, "htmx:afterRequest", H); + fe(r, "htmx:timeout", H); + re(l); + m(); + }; + if (!ae(r, "htmx:beforeRequest", H)) { + re(s); + m(); + return e; + } + var T = tn(r); + var q = nn(r); + ie(["loadstart", "loadend", "progress", "abort"], function (t) { + ie([g, g.upload], function (e) { + e.addEventListener(t, function (e) { + ae(r, "htmx:xhr:" + t, { + lengthComputable: e.lengthComputable, + loaded: e.loaded, + total: e.total, + }); + }); + }); + }); + ae(r, "htmx:beforeSend", H); + const J = E ? null : wn(g, r, w); + g.send(J); + return e; + } + function Mn(e, t) { + const n = t.xhr; + let r = null; + let o = null; + if (H(n, /HX-Push:/i)) { + r = n.getResponseHeader("HX-Push"); + o = "push"; + } else if (H(n, /HX-Push-Url:/i)) { + r = n.getResponseHeader("HX-Push-Url"); + o = "push"; + } else if (H(n, /HX-Replace-Url:/i)) { + r = n.getResponseHeader("HX-Replace-Url"); + o = "replace"; + } + if (r) { + if (r === "false") { + return {}; + } else { + return { type: o, path: r }; + } + } + const i = t.pathInfo.finalRequestPath; + const s = t.pathInfo.responsePath; + const l = ne(e, "hx-push-url"); + const c = ne(e, "hx-replace-url"); + const u = oe(e).boosted; + let f = null; + let a = null; + if (l) { + f = "push"; + a = l; + } else if (c) { + f = "replace"; + a = c; + } else if (u) { + f = "push"; + a = s || i; + } + if (a) { + if (a === "false") { + return {}; + } + if (a === "true") { + a = s || i; + } + if (t.pathInfo.anchor && a.indexOf("#") === -1) { + a = a + "#" + t.pathInfo.anchor; + } + return { type: f, path: a }; + } else { + return {}; + } + } + function Fn(e, t) { + var n = new RegExp(e.code); + return n.test(t.toString(10)); + } + function Xn(e) { + for (var t = 0; t < Q.config.responseHandling.length; t++) { + var n = Q.config.responseHandling[t]; + if (Fn(n, e.status)) { + return n; + } + } + return { swap: false }; + } + function Bn(e) { + if (e) { + const t = f("title"); + if (t) { + t.textContent = e; + } else { + window.document.title = e; + } + } + } + function Un(e, t) { + if (t === "this") { + return e; + } + const n = ce(ue(e, t)); + if (n == null) { + fe(e, "htmx:targetError", { target: t }); + throw new Error(`Invalid re-target ${t}`); + } + return n; + } + function jn(t, e) { + const n = e.xhr; + let r = e.target; + const o = e.etc; + const i = e.select; + if (!ae(t, "htmx:beforeOnLoad", e)) return; + if (H(n, /HX-Trigger:/i)) { + Je(n, "HX-Trigger", t); + } + if (H(n, /HX-Location:/i)) { + Gt(); + let e = n.getResponseHeader("HX-Location"); + var s; + if (e.indexOf("{") === 0) { + s = v(e); + e = s.path; + delete s.path; + } + Ln("get", e, s).then(function () { + Wt(e); + }); + return; + } + const l = + H(n, /HX-Refresh:/i) && n.getResponseHeader("HX-Refresh") === "true"; + if (H(n, /HX-Redirect:/i)) { + e.keepIndicators = true; + Q.location.href = n.getResponseHeader("HX-Redirect"); + l && Q.location.reload(); + return; + } + if (l) { + e.keepIndicators = true; + Q.location.reload(); + return; + } + const c = Mn(t, e); + const u = Xn(n); + const f = u.swap; + let a = !!u.error; + let h = Q.config.ignoreTitle || u.ignoreTitle; + let d = u.select; + if (u.target) { + e.target = Un(t, u.target); + } + var p = o.swapOverride; + if (p == null && u.swapOverride) { + p = u.swapOverride; + } + if (H(n, /HX-Retarget:/i)) { + e.target = Un(t, n.getResponseHeader("HX-Retarget")); + } + if (H(n, /HX-Reswap:/i)) { + p = n.getResponseHeader("HX-Reswap"); + } + var g = n.response; + var m = le( + { + shouldSwap: f, + serverResponse: g, + isError: a, + ignoreTitle: h, + selectOverride: d, + swapOverride: p, + }, + e, + ); + if (u.event && !ae(r, u.event, m)) return; + if (!ae(r, "htmx:beforeSwap", m)) return; + r = m.target; + g = m.serverResponse; + a = m.isError; + h = m.ignoreTitle; + d = m.selectOverride; + p = m.swapOverride; + e.target = r; + e.failed = a; + e.successful = !a; + if (m.shouldSwap) { + if (n.status === 286) { + lt(t); + } + jt(t, function (e) { + g = e.transformResponse(g, n, t); + }); + if (c.type) { + Gt(); + } + var y = bn(t, p); + if (!y.hasOwnProperty("ignoreTitle")) { + y.ignoreTitle = h; + } + r.classList.add(Q.config.swappingClass); + if (i) { + d = i; + } + if (H(n, /HX-Reselect:/i)) { + d = n.getResponseHeader("HX-Reselect"); + } + const x = ne(t, "hx-select-oob"); + const b = ne(t, "hx-select"); + $e(r, g, y, { + select: d === "unset" ? null : d || b, + selectOOB: x, + eventInfo: e, + anchor: e.pathInfo.anchor, + contextElement: t, + afterSwapCallback: function () { + if (H(n, /HX-Trigger-After-Swap:/i)) { + let e = t; + if (!se(t)) { + e = te().body; + } + Je(n, "HX-Trigger-After-Swap", e); + } + }, + afterSettleCallback: function () { + if (H(n, /HX-Trigger-After-Settle:/i)) { + let e = t; + if (!se(t)) { + e = te().body; + } + Je(n, "HX-Trigger-After-Settle", e); + } + }, + beforeSwapCallback: function () { + if (c.type) { + ae(te().body, "htmx:beforeHistoryUpdate", le({ history: c }, e)); + if (c.type === "push") { + Wt(c.path); + ae(te().body, "htmx:pushedIntoHistory", { path: c.path }); + } else { + Zt(c.path); + ae(te().body, "htmx:replacedInHistory", { path: c.path }); + } + } + }, + }); + } + if (a) { + fe( + t, + "htmx:responseError", + le( + { + error: + "Response Status Error Code " + + n.status + + " from " + + e.pathInfo.requestPath, + }, + e, + ), + ); + } + } + const Vn = {}; + function _n() { + return { + init: function (e) { + return null; + }, + getSelectors: function () { + return null; + }, + onEvent: function (e, t) { + return true; + }, + transformResponse: function (e, t, n) { + return e; + }, + isInlineSwap: function (e) { + return false; + }, + handleSwap: function (e, t, n, r) { + return false; + }, + encodeParameters: function (e, t, n) { + return null; + }, + }; + } + function zn(e, t) { + if (t.init) { + t.init(n); + } + Vn[e] = le(_n(), t); + } + function $n(e) { + delete Vn[e]; + } + function Jn(e, n, r) { + if (n == undefined) { + n = []; + } + if (e == undefined) { + return n; + } + if (r == undefined) { + r = []; + } + const t = a(e, "hx-ext"); + if (t) { + ie(t.split(","), function (e) { + e = e.replace(/ /g, ""); + if (e.slice(0, 7) == "ignore:") { + r.push(e.slice(7)); + return; + } + if (r.indexOf(e) < 0) { + const t = Vn[e]; + if (t && n.indexOf(t) < 0) { + n.push(t); + } + } + }); + } + return Jn(ce(u(e)), n, r); + } + var Kn = false; + te().addEventListener("DOMContentLoaded", function () { + Kn = true; + }); + function Gn(e) { + if (Kn || te().readyState === "complete") { + e(); + } else { + te().addEventListener("DOMContentLoaded", e); + } + } + function Wn() { + if (Q.config.includeIndicatorStyles !== false) { + const e = Q.config.inlineStyleNonce + ? ` nonce="${Q.config.inlineStyleNonce}"` + : ""; + te().head.insertAdjacentHTML( + "beforeend", + " ." + + Q.config.indicatorClass + + "{opacity:0} ." + + Q.config.requestClass + + " ." + + Q.config.indicatorClass + + "{opacity:1; transition: opacity 200ms ease-in;} ." + + Q.config.requestClass + + "." + + Q.config.indicatorClass + + "{opacity:1; transition: opacity 200ms ease-in;} ", + ); + } + } + function Zn() { + const e = te().querySelector('meta[name="htmx-config"]'); + if (e) { + return v(e.content); + } else { + return null; + } + } + function Yn() { + const e = Zn(); + if (e) { + Q.config = le(Q.config, e); + } + } + Gn(function () { + Yn(); + Wn(); + let e = te().body; + Ft(e); + const t = te().querySelectorAll( + "[hx-trigger='restored'],[data-hx-trigger='restored']", + ); + e.addEventListener("htmx:abort", function (e) { + const t = e.target; + const n = oe(t); + if (n && n.xhr) { + n.xhr.abort(); + } + }); + const n = window.onpopstate ? window.onpopstate.bind(window) : null; + window.onpopstate = function (e) { + if (e.state && e.state.htmx) { + en(); + ie(t, function (e) { + ae(e, "htmx:restored", { document: te(), triggerEvent: ae }); + }); + } else { + if (n) { + n(e); + } + } + }; + b().setTimeout(function () { + ae(e, "htmx:load", {}); + e = null; + }, 0); + }); + return Q; +})(); diff --git a/cmd/web/base.templ b/cmd/web/base.templ new file mode 100644 index 0000000..eaea35b --- /dev/null +++ b/cmd/web/base.templ @@ -0,0 +1,19 @@ +package web + +templ Base() { + + + + + + Go Blueprint Hello + + + + +
+ { children... } +
+ + +} diff --git a/cmd/web/efs.go b/cmd/web/efs.go new file mode 100644 index 0000000..57b9761 --- /dev/null +++ b/cmd/web/efs.go @@ -0,0 +1,6 @@ +package web + +import "embed" + +//go:embed "assets" +var Files embed.FS diff --git a/cmd/web/hello.go b/cmd/web/hello.go new file mode 100644 index 0000000..98cd24e --- /dev/null +++ b/cmd/web/hello.go @@ -0,0 +1,21 @@ +package web + +import ( + "log" + "net/http" +) + +func HelloWebHandler(w http.ResponseWriter, r *http.Request) { + err := r.ParseForm() + if err != nil { + http.Error(w, "Bad Request", http.StatusBadRequest) + } + + name := r.FormValue("name") + component := HelloPost(name) + err = component.Render(r.Context(), w) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + log.Fatalf("Error rendering in HelloWebHandler: %e", err) + } +} diff --git a/cmd/web/hello.templ b/cmd/web/hello.templ new file mode 100644 index 0000000..f5f5d72 --- /dev/null +++ b/cmd/web/hello.templ @@ -0,0 +1,17 @@ +package web + +templ HelloForm() { + @Base() { +
+ + +
+
+ } +} + +templ HelloPost(name string) { +
+

Hello, { name }

+
+} diff --git a/cmd/web/styles/input.css b/cmd/web/styles/input.css new file mode 100644 index 0000000..73a943c --- /dev/null +++ b/cmd/web/styles/input.css @@ -0,0 +1 @@ +@import "tailwindcss" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..8e0db6b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,17 @@ +services: + app: + build: + context: . + dockerfile: Dockerfile + target: prod + restart: unless-stopped + ports: + - ${PORT}:${PORT} + environment: + APP_ENV: ${APP_ENV} + PORT: ${PORT} + BLUEPRINT_DB_URL: ${BLUEPRINT_DB_URL} + volumes: + - sqlite_bp:/app/db +volumes: + sqlite_bp: diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..d3b6d64 --- /dev/null +++ b/go.mod @@ -0,0 +1,23 @@ +module billit + +go 1.25.1 + +require ( + github.com/a-h/templ v0.3.960 + github.com/joho/godotenv v1.5.1 + github.com/labstack/echo/v4 v4.13.4 + github.com/mattn/go-sqlite3 v1.14.32 +) + +require ( + github.com/labstack/gommon v0.4.2 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect + golang.org/x/crypto v0.45.0 // indirect + golang.org/x/net v0.47.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/text v0.31.0 // indirect + golang.org/x/time v0.14.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..349e24a --- /dev/null +++ b/go.sum @@ -0,0 +1,39 @@ +github.com/a-h/templ v0.3.960 h1:trshEpGa8clF5cdI39iY4ZrZG8Z/QixyzEyUnA7feTM= +github.com/a-h/templ v0.3.960/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA= +github.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ= +github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= +github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= +github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/database/database.go b/internal/database/database.go new file mode 100644 index 0000000..5edb5ff --- /dev/null +++ b/internal/database/database.go @@ -0,0 +1,113 @@ +package database + +import ( + "context" + "database/sql" + "fmt" + "log" + "os" + "strconv" + "time" + + _ "github.com/joho/godotenv/autoload" + _ "github.com/mattn/go-sqlite3" +) + +// Service represents a service that interacts with a database. +type Service interface { + // Health returns a map of health status information. + // The keys and values in the map are service-specific. + Health() map[string]string + + // Close terminates the database connection. + // It returns an error if the connection cannot be closed. + Close() error +} + +type service struct { + db *sql.DB +} + +var ( + dburl = os.Getenv("BLUEPRINT_DB_URL") + dbInstance *service +) + +func New() Service { + // Reuse Connection + if dbInstance != nil { + return dbInstance + } + + db, err := sql.Open("sqlite3", dburl) + if err != nil { + // This will not be a connection error, but a DSN parse error or + // another initialization error. + log.Fatal(err) + } + + dbInstance = &service{ + db: db, + } + return dbInstance +} + +// Health checks the health of the database connection by pinging the database. +// It returns a map with keys indicating various health statistics. +func (s *service) Health() map[string]string { + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + + stats := make(map[string]string) + + // Ping the database + err := s.db.PingContext(ctx) + if err != nil { + stats["status"] = "down" + stats["error"] = fmt.Sprintf("db down: %v", err) + log.Fatalf("db down: %v", err) // Log the error and terminate the program + return stats + } + + // Database is up, add more statistics + stats["status"] = "up" + stats["message"] = "It's healthy" + + // Get database stats (like open connections, in use, idle, etc.) + dbStats := s.db.Stats() + stats["open_connections"] = strconv.Itoa(dbStats.OpenConnections) + stats["in_use"] = strconv.Itoa(dbStats.InUse) + stats["idle"] = strconv.Itoa(dbStats.Idle) + stats["wait_count"] = strconv.FormatInt(dbStats.WaitCount, 10) + stats["wait_duration"] = dbStats.WaitDuration.String() + stats["max_idle_closed"] = strconv.FormatInt(dbStats.MaxIdleClosed, 10) + stats["max_lifetime_closed"] = strconv.FormatInt(dbStats.MaxLifetimeClosed, 10) + + // Evaluate stats to provide a health message + if dbStats.OpenConnections > 40 { // Assuming 50 is the max for this example + stats["message"] = "The database is experiencing heavy load." + } + + if dbStats.WaitCount > 1000 { + stats["message"] = "The database has a high number of wait events, indicating potential bottlenecks." + } + + if dbStats.MaxIdleClosed > int64(dbStats.OpenConnections)/2 { + stats["message"] = "Many idle connections are being closed, consider revising the connection pool settings." + } + + if dbStats.MaxLifetimeClosed > int64(dbStats.OpenConnections)/2 { + stats["message"] = "Many connections are being closed due to max lifetime, consider increasing max lifetime or revising the connection usage pattern." + } + + return stats +} + +// Close closes the database connection. +// It logs a message indicating the disconnection from the specific database. +// If the connection is successfully closed, it returns nil. +// If an error occurs while closing the connection, it returns the error. +func (s *service) Close() error { + log.Printf("Disconnected from database: %s", dburl) + return s.db.Close() +} diff --git a/internal/server/routes.go b/internal/server/routes.go new file mode 100644 index 0000000..a34efab --- /dev/null +++ b/internal/server/routes.go @@ -0,0 +1,48 @@ +package server + +import ( + "net/http" + + "billit/cmd/web" + "github.com/a-h/templ" + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" +) + +func (s *Server) RegisterRoutes() http.Handler { + e := echo.New() + e.Use(middleware.Logger()) + e.Use(middleware.Recover()) + + e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ + AllowOrigins: []string{"https://*", "http://*"}, + AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"}, + AllowHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"}, + AllowCredentials: true, + MaxAge: 300, + })) + + fileServer := http.FileServer(http.FS(web.Files)) + e.GET("/assets/*", echo.WrapHandler(fileServer)) + + e.GET("/web", echo.WrapHandler(templ.Handler(web.HelloForm()))) + e.POST("/hello", echo.WrapHandler(http.HandlerFunc(web.HelloWebHandler))) + + e.GET("/", s.HelloWorldHandler) + + e.GET("/health", s.healthHandler) + + return e +} + +func (s *Server) HelloWorldHandler(c echo.Context) error { + resp := map[string]string{ + "message": "Hello World", + } + + return c.JSON(http.StatusOK, resp) +} + +func (s *Server) healthHandler(c echo.Context) error { + return c.JSON(http.StatusOK, s.db.Health()) +} diff --git a/internal/server/routes_test.go b/internal/server/routes_test.go new file mode 100644 index 0000000..913a5d9 --- /dev/null +++ b/internal/server/routes_test.go @@ -0,0 +1,39 @@ +package server + +import ( + "encoding/json" + "github.com/labstack/echo/v4" + "net/http" + "net/http/httptest" + "reflect" + "testing" +) + +func TestHandler(t *testing.T) { + e := echo.New() + req := httptest.NewRequest(http.MethodGet, "/", nil) + resp := httptest.NewRecorder() + c := e.NewContext(req, resp) + s := &Server{} + // Assertions + if err := s.HelloWorldHandler(c); err != nil { + t.Errorf("handler() error = %v", err) + return + } + if resp.Code != http.StatusOK { + t.Errorf("handler() wrong status code = %v", resp.Code) + return + } + expected := map[string]string{"message": "Hello World"} + var actual map[string]string + // Decode the response body into the actual map + if err := json.NewDecoder(resp.Body).Decode(&actual); err != nil { + t.Errorf("handler() error decoding response body: %v", err) + return + } + // Compare the decoded response with the expected value + if !reflect.DeepEqual(expected, actual) { + t.Errorf("handler() wrong response body. expected = %v, actual = %v", expected, actual) + return + } +} diff --git a/internal/server/server.go b/internal/server/server.go new file mode 100644 index 0000000..a20f784 --- /dev/null +++ b/internal/server/server.go @@ -0,0 +1,39 @@ +package server + +import ( + "fmt" + "net/http" + "os" + "strconv" + "time" + + _ "github.com/joho/godotenv/autoload" + + "billit/internal/database" +) + +type Server struct { + port int + + db database.Service +} + +func NewServer() *http.Server { + port, _ := strconv.Atoi(os.Getenv("PORT")) + NewServer := &Server{ + port: port, + + db: database.New(), + } + + // Declare Server config + server := &http.Server{ + Addr: fmt.Sprintf(":%d", NewServer.port), + Handler: NewServer.RegisterRoutes(), + IdleTimeout: time.Minute, + ReadTimeout: 10 * time.Second, + WriteTimeout: 30 * time.Second, + } + + return server +} diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..fc44685 --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,5 @@ +module.exports = { + content: ["./**/*.html", "./**/*.templ", "./**/*.go",], + theme: { extend: {}, }, + plugins: [], +}