commit d741a30495036d04402b6cda2f6867e2fb1393f2 Author: gibbyDev Date: Thu Mar 26 19:12:59 2026 -0400 commit diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2e898b7 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +FROM golang:1.24.4-alpine AS build +RUN apk add --no-cache curl libstdc++ libgcc + +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 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..f8c3d67 --- /dev/null +++ b/Makefile @@ -0,0 +1,80 @@ +# 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: + @command -v tailwindcss >/dev/null || (echo "tailwindcss is not installed on your system" && exit 1) + +build: tailwind-install templ-install + @echo "Building..." + @templ generate + @tailwindcss -i cmd/web/styles/input.css -o cmd/web/assets/css/output.css + @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 +# Integrations Tests for the application +itest: + @echo "Running integration tests..." + @go test ./internal/database -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 docker-run docker-down itest templ-install diff --git a/README.md b/README.md new file mode 100644 index 0000000..bb34a40 --- /dev/null +++ b/README.md @@ -0,0 +1,53 @@ +# Project go-htmx + +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..f410539 --- /dev/null +++ b/cmd/api/main.go @@ -0,0 +1,58 @@ +package main + +import ( + "context" + "fmt" + "log" + "net/http" + "os/signal" + "syscall" + "time" + + "go-htmx/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/css/output.css b/cmd/web/assets/css/output.css new file mode 100644 index 0000000..253ef22 --- /dev/null +++ b/cmd/web/assets/css/output.css @@ -0,0 +1,1133 @@ +*, ::before, ::after { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; + --tw-contain-size: ; + --tw-contain-layout: ; + --tw-contain-paint: ; + --tw-contain-style: ; +} + +::backdrop { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; + --tw-contain-size: ; + --tw-contain-layout: ; + --tw-contain-paint: ; + --tw-contain-style: ; +} + +/* +! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com +*/ + +/* +1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) +2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) +*/ + +*, +::before, +::after { + box-sizing: border-box; + /* 1 */ + border-width: 0; + /* 2 */ + border-style: solid; + /* 2 */ + border-color: #e5e7eb; + /* 2 */ +} + +::before, +::after { + --tw-content: ''; +} + +/* +1. Use a consistent sensible line-height in all browsers. +2. Prevent adjustments of font size after orientation changes in iOS. +3. Use a more readable tab size. +4. Use the user's configured `sans` font-family by default. +5. Use the user's configured `sans` font-feature-settings by default. +6. Use the user's configured `sans` font-variation-settings by default. +7. Disable tap highlights on iOS +*/ + +html, +:host { + line-height: 1.5; + /* 1 */ + -webkit-text-size-adjust: 100%; + /* 2 */ + -moz-tab-size: 4; + /* 3 */ + -o-tab-size: 4; + tab-size: 4; + /* 3 */ + font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + /* 4 */ + font-feature-settings: normal; + /* 5 */ + font-variation-settings: normal; + /* 6 */ + -webkit-tap-highlight-color: transparent; + /* 7 */ +} + +/* +1. Remove the margin in all browsers. +2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. +*/ + +body { + margin: 0; + /* 1 */ + line-height: inherit; + /* 2 */ +} + +/* +1. Add the correct height in Firefox. +2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) +3. Ensure horizontal rules are visible by default. +*/ + +hr { + height: 0; + /* 1 */ + color: inherit; + /* 2 */ + border-top-width: 1px; + /* 3 */ +} + +/* +Add the correct text decoration in Chrome, Edge, and Safari. +*/ + +abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; +} + +/* +Remove the default font size and weight for headings. +*/ + +h1, +h2, +h3, +h4, +h5, +h6 { + font-size: inherit; + font-weight: inherit; +} + +/* +Reset links to optimize for opt-in styling instead of opt-out. +*/ + +a { + color: inherit; + text-decoration: inherit; +} + +/* +Add the correct font weight in Edge and Safari. +*/ + +b, +strong { + font-weight: bolder; +} + +/* +1. Use the user's configured `mono` font-family by default. +2. Use the user's configured `mono` font-feature-settings by default. +3. Use the user's configured `mono` font-variation-settings by default. +4. Correct the odd `em` font sizing in all browsers. +*/ + +code, +kbd, +samp, +pre { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + /* 1 */ + font-feature-settings: normal; + /* 2 */ + font-variation-settings: normal; + /* 3 */ + font-size: 1em; + /* 4 */ +} + +/* +Add the correct font size in all browsers. +*/ + +small { + font-size: 80%; +} + +/* +Prevent `sub` and `sup` elements from affecting the line height in all browsers. +*/ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* +1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) +2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) +3. Remove gaps between table borders by default. +*/ + +table { + text-indent: 0; + /* 1 */ + border-color: inherit; + /* 2 */ + border-collapse: collapse; + /* 3 */ +} + +/* +1. Change the font styles in all browsers. +2. Remove the margin in Firefox and Safari. +3. Remove default padding in all browsers. +*/ + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; + /* 1 */ + font-feature-settings: inherit; + /* 1 */ + font-variation-settings: inherit; + /* 1 */ + font-size: 100%; + /* 1 */ + font-weight: inherit; + /* 1 */ + line-height: inherit; + /* 1 */ + letter-spacing: inherit; + /* 1 */ + color: inherit; + /* 1 */ + margin: 0; + /* 2 */ + padding: 0; + /* 3 */ +} + +/* +Remove the inheritance of text transform in Edge and Firefox. +*/ + +button, +select { + text-transform: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Remove default button styles. +*/ + +button, +input:where([type='button']), +input:where([type='reset']), +input:where([type='submit']) { + -webkit-appearance: button; + /* 1 */ + background-color: transparent; + /* 2 */ + background-image: none; + /* 2 */ +} + +/* +Use the modern Firefox focus style for all focusable elements. +*/ + +:-moz-focusring { + outline: auto; +} + +/* +Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) +*/ + +:-moz-ui-invalid { + box-shadow: none; +} + +/* +Add the correct vertical alignment in Chrome and Firefox. +*/ + +progress { + vertical-align: baseline; +} + +/* +Correct the cursor style of increment and decrement buttons in Safari. +*/ + +::-webkit-inner-spin-button, +::-webkit-outer-spin-button { + height: auto; +} + +/* +1. Correct the odd appearance in Chrome and Safari. +2. Correct the outline style in Safari. +*/ + +[type='search'] { + -webkit-appearance: textfield; + /* 1 */ + outline-offset: -2px; + /* 2 */ +} + +/* +Remove the inner padding in Chrome and Safari on macOS. +*/ + +::-webkit-search-decoration { + -webkit-appearance: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Change font properties to `inherit` in Safari. +*/ + +::-webkit-file-upload-button { + -webkit-appearance: button; + /* 1 */ + font: inherit; + /* 2 */ +} + +/* +Add the correct display in Chrome and Safari. +*/ + +summary { + display: list-item; +} + +/* +Removes the default spacing and border for appropriate elements. +*/ + +blockquote, +dl, +dd, +h1, +h2, +h3, +h4, +h5, +h6, +hr, +figure, +p, +pre { + margin: 0; +} + +fieldset { + margin: 0; + padding: 0; +} + +legend { + padding: 0; +} + +ol, +ul, +menu { + list-style: none; + margin: 0; + padding: 0; +} + +/* +Reset default styling for dialogs. +*/ + +dialog { + padding: 0; +} + +/* +Prevent resizing textareas horizontally by default. +*/ + +textarea { + resize: vertical; +} + +/* +1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) +2. Set the default placeholder color to the user's configured gray 400 color. +*/ + +input::-moz-placeholder, textarea::-moz-placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +input::placeholder, +textarea::placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +/* +Set the default cursor for buttons. +*/ + +button, +[role="button"] { + cursor: pointer; +} + +/* +Make sure disabled buttons don't get the pointer cursor. +*/ + +:disabled { + cursor: default; +} + +/* +1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) +2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) + This can trigger a poorly considered lint error in some tools but is included by design. +*/ + +img, +svg, +video, +canvas, +audio, +iframe, +embed, +object { + display: block; + /* 1 */ + vertical-align: middle; + /* 2 */ +} + +/* +Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) +*/ + +img, +video { + max-width: 100%; + height: auto; +} + +/* Make elements with the HTML hidden attribute stay hidden by default */ + +[hidden]:where(:not([hidden="until-found"])) { + display: none; +} + +.container { + width: 100%; +} + +@media (min-width: 640px) { + .container { + max-width: 640px; + } +} + +@media (min-width: 768px) { + .container { + max-width: 768px; + } +} + +@media (min-width: 1024px) { + .container { + max-width: 1024px; + } +} + +@media (min-width: 1280px) { + .container { + max-width: 1280px; + } +} + +@media (min-width: 1536px) { + .container { + max-width: 1536px; + } +} + +.absolute { + position: absolute; +} + +.relative { + position: relative; +} + +.sticky { + position: sticky; +} + +.bottom-0 { + bottom: 0px; +} + +.left-0 { + left: 0px; +} + +.top-0 { + top: 0px; +} + +.z-50 { + z-index: 50; +} + +.mx-auto { + margin-left: auto; + margin-right: auto; +} + +.mb-12 { + margin-bottom: 3rem; +} + +.mb-16 { + margin-bottom: 4rem; +} + +.mb-2 { + margin-bottom: 0.5rem; +} + +.mb-4 { + margin-bottom: 1rem; +} + +.mb-6 { + margin-bottom: 1.5rem; +} + +.mb-8 { + margin-bottom: 2rem; +} + +.mt-4 { + margin-top: 1rem; +} + +.mt-6 { + margin-top: 1.5rem; +} + +.flex { + display: flex; +} + +.grid { + display: grid; +} + +.hidden { + display: none; +} + +.h-0\.5 { + height: 0.125rem; +} + +.h-48 { + height: 12rem; +} + +.h-full { + height: 100%; +} + +.h-screen { + height: 100vh; +} + +.min-h-screen { + min-height: 100vh; +} + +.w-0 { + width: 0px; +} + +.w-full { + width: 100%; +} + +.max-w-2xl { + max-width: 42rem; +} + +.max-w-3xl { + max-width: 48rem; +} + +.max-w-6xl { + max-width: 72rem; +} + +.max-w-xl { + max-width: 36rem; +} + +.flex-1 { + flex: 1 1 0%; +} + +.transform { + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.cursor-pointer { + cursor: pointer; +} + +.auto-rows-\[250px\] { + grid-auto-rows: 250px; +} + +.grid-cols-1 { + grid-template-columns: repeat(1, minmax(0, 1fr)); +} + +.flex-col { + flex-direction: column; +} + +.items-center { + align-items: center; +} + +.justify-between { + justify-content: space-between; +} + +.gap-4 { + gap: 1rem; +} + +.gap-6 { + gap: 1.5rem; +} + +.space-x-8 > :not([hidden]) ~ :not([hidden]) { + --tw-space-x-reverse: 0; + margin-right: calc(2rem * var(--tw-space-x-reverse)); + margin-left: calc(2rem * calc(1 - var(--tw-space-x-reverse))); +} + +.overflow-hidden { + overflow: hidden; +} + +.rounded-2xl { + border-radius: 1rem; +} + +.rounded-lg { + border-radius: 0.5rem; +} + +.rounded-xl { + border-radius: 0.75rem; +} + +.border { + border-width: 1px; +} + +.border-b { + border-bottom-width: 1px; +} + +.border-t { + border-top-width: 1px; +} + +.border-crema-200 { + --tw-border-opacity: 1; + border-color: rgb(232 223 207 / var(--tw-border-opacity, 1)); +} + +.bg-crema-100 { + --tw-bg-opacity: 1; + background-color: rgb(246 241 232 / var(--tw-bg-opacity, 1)); +} + +.bg-crema-50 { + --tw-bg-opacity: 1; + background-color: rgb(253 251 247 / var(--tw-bg-opacity, 1)); +} + +.bg-oliva-100 { + --tw-bg-opacity: 1; + background-color: rgb(223 232 220 / var(--tw-bg-opacity, 1)); +} + +.bg-terracotta-500 { + --tw-bg-opacity: 1; + background-color: rgb(191 100 54 / var(--tw-bg-opacity, 1)); +} + +.bg-white { + --tw-bg-opacity: 1; + background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1)); +} + +.bg-gradient-to-br { + background-image: linear-gradient(to bottom right, var(--tw-gradient-stops)); +} + +.to-crema-100 { + --tw-gradient-to: #f6f1e8 var(--tw-gradient-to-position); +} + +.object-cover { + -o-object-fit: cover; + object-fit: cover; +} + +.p-12 { + padding: 3rem; +} + +.p-3 { + padding: 0.75rem; +} + +.p-4 { + padding: 1rem; +} + +.p-6 { + padding: 1.5rem; +} + +.p-8 { + padding: 2rem; +} + +.px-6 { + padding-left: 1.5rem; + padding-right: 1.5rem; +} + +.px-8 { + padding-left: 2rem; + padding-right: 2rem; +} + +.py-10 { + padding-top: 2.5rem; + padding-bottom: 2.5rem; +} + +.py-16 { + padding-top: 4rem; + padding-bottom: 4rem; +} + +.py-2 { + padding-top: 0.5rem; + padding-bottom: 0.5rem; +} + +.py-3 { + padding-top: 0.75rem; + padding-bottom: 0.75rem; +} + +.py-4 { + padding-top: 1rem; + padding-bottom: 1rem; +} + +.py-8 { + padding-top: 2rem; + padding-bottom: 2rem; +} + +.text-center { + text-align: center; +} + +.font-latin { + font-family: ui-serif, Georgia, serif; +} + +.text-2xl { + font-size: 1.5rem; + line-height: 2rem; +} + +.text-3xl { + font-size: 1.875rem; + line-height: 2.25rem; +} + +.text-4xl { + font-size: 2.25rem; + line-height: 2.5rem; +} + +.text-lg { + font-size: 1.125rem; + line-height: 1.75rem; +} + +.text-sm { + font-size: 0.875rem; + line-height: 1.25rem; +} + +.text-xl { + font-size: 1.25rem; + line-height: 1.75rem; +} + +.font-semibold { + font-weight: 600; +} + +.leading-relaxed { + line-height: 1.625; +} + +.tracking-wide { + letter-spacing: 0.025em; +} + +.text-ink-700 { + --tw-text-opacity: 1; + color: rgb(58 58 55 / var(--tw-text-opacity, 1)); +} + +.text-ink-800 { + --tw-text-opacity: 1; + color: rgb(42 42 40 / var(--tw-text-opacity, 1)); +} + +.text-terracotta-500 { + --tw-text-opacity: 1; + color: rgb(191 100 54 / var(--tw-text-opacity, 1)); +} + +.text-white { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity, 1)); +} + +.shadow-md { + --tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.shadow-sm { + --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); + --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.transition-all { + transition-property: all; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +.transition-colors { + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +.transition-shadow { + transition-property: box-shadow; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +.transition-transform { + transition-property: transform; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +.duration-200 { + transition-duration: 200ms; +} + +.duration-300 { + transition-duration: 300ms; +} + +.duration-500 { + transition-duration: 500ms; +} + +.hover\:-translate-y-2:hover { + --tw-translate-y: -0.5rem; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.hover\:bg-oliva-200:hover { + --tw-bg-opacity: 1; + background-color: rgb(183 201 177 / var(--tw-bg-opacity, 1)); +} + +.hover\:bg-terracotta-600:hover { + --tw-bg-opacity: 1; + background-color: rgb(169 83 44 / var(--tw-bg-opacity, 1)); +} + +.hover\:text-terracotta-500:hover { + --tw-text-opacity: 1; + color: rgb(191 100 54 / var(--tw-text-opacity, 1)); +} + +.hover\:text-terracotta-600:hover { + --tw-text-opacity: 1; + color: rgb(169 83 44 / var(--tw-text-opacity, 1)); +} + +.hover\:shadow-lg:hover { + --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.hover\:shadow-md:hover { + --tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.hover\:shadow-xl:hover { + --tw-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.focus\:outline-none:focus { + outline: 2px solid transparent; + outline-offset: 2px; +} + +.focus\:ring-2:focus { + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); +} + +.focus\:ring-oliva-300:focus { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(143 167 137 / var(--tw-ring-opacity, 1)); +} + +.active\:scale-95:active { + --tw-scale-x: .95; + --tw-scale-y: .95; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.group:hover .group-hover\:w-full { + width: 100%; +} + +.group:hover .group-hover\:scale-110 { + --tw-scale-x: 1.1; + --tw-scale-y: 1.1; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.group:hover .group-hover\:brightness-110 { + --tw-brightness: brightness(1.1); + filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); +} + +@media (min-width: 640px) { + .sm\:flex-row { + flex-direction: row; + } +} + +@media (min-width: 768px) { + .md\:col-span-2 { + grid-column: span 2 / span 2; + } + + .md\:row-span-2 { + grid-row: span 2 / span 2; + } + + .md\:flex { + display: flex; + } + + .md\:hidden { + display: none; + } + + .md\:grid-cols-2 { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .md\:grid-cols-3 { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + + .md\:py-12 { + padding-top: 3rem; + padding-bottom: 3rem; + } + + .md\:text-6xl { + font-size: 3.75rem; + line-height: 1; + } +} + +@media (min-width: 1024px) { + .lg\:grid-cols-3 { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } +} 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..646802d --- /dev/null +++ b/cmd/web/base.templ @@ -0,0 +1,57 @@ +package web + +templ Base() { + + + + + + Latin Garden + + + + + + + + + + +
+ { children... } +
+ + + +} diff --git a/cmd/web/base_templ.go b/cmd/web/base_templ.go new file mode 100644 index 0000000..16ce096 --- /dev/null +++ b/cmd/web/base_templ.go @@ -0,0 +1,48 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.977 +package web + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +func Base() templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "Latin Garden
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templ_7745c5c3_Var1.Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate 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..daf239c --- /dev/null +++ b/cmd/web/hello.templ @@ -0,0 +1,169 @@ +package web + +templ HelloForm() { + @Base() { + + +
+

+ Welcome to the Garden +

+

+ A calm collection of plants inspired by southern light and warm earth. Discover the beauty of nature's finest specimens. +

+
+ + +
+ + +
+ Garden landscape +
+ + +
+

Sage

+

+ Soft greens that cool the room and bring serenity to your space. +

+
+ + +
+ Green plants +
+ + +
+

+ Say Hello +

+ +
+ + + + +
+ +
+
+ +
+ + +
+
+

Featured Plants

+

Curated selections for your garden

+
+ +
+ +
+
+ Monstera plant +
+
+

Monstera Deliciosa

+

A stunning tropical plant with iconic split leaves. Perfect for bright, indirect light.

+ +
+
+ + +
+
+ Snake plant +
+
+

Snake Plant

+

Low-maintenance and air-purifying. Thrives in various light conditions with minimal care.

+ +
+
+ + +
+
+ Pothos plant +
+
+

Pothos

+

The ultimate climbing vine plant. Beautiful trailing foliage that adapts to any environment.

+ +
+
+
+
+ + +
+
+

About Our Garden

+

+ We believe that plants aren't just decorations—they're living companions that transform your space and improve your wellbeing. Our mission is to help you discover the perfect plants for your home or office. +

+

+ Each plant in our collection is carefully selected for its aesthetic beauty and resilience. Whether you're a seasoned gardener or just starting your plant journey, we have something for everyone. +

+

+ From tropical wonders to hardy succulents, explore the natural beauty of our curated selection and bring life to your surroundings. +

+
+
+ + +
+

Ready to Start Your Garden?

+

+ Join our community of plant enthusiasts and receive care tips, exclusive plant recommendations, and seasonal updates. +

+ +
+ + } +} + +templ HelloPost(name string) { +
+

Hello, { name }

+
+} diff --git a/cmd/web/hello_templ.go b/cmd/web/hello_templ.go new file mode 100644 index 0000000..732188b --- /dev/null +++ b/cmd/web/hello_templ.go @@ -0,0 +1,100 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.977 +package web + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +func HelloForm() templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "

Welcome to the Garden

A calm collection of plants inspired by southern light and warm earth. Discover the beauty of nature's finest specimens.

\"Garden

Sage

Soft greens that cool the room and bring serenity to your space.

\"Green

Say Hello

Featured Plants

Curated selections for your garden

\"Monstera

Monstera Deliciosa

A stunning tropical plant with iconic split leaves. Perfect for bright, indirect light.

\"Snake

Snake Plant

Low-maintenance and air-purifying. Thrives in various light conditions with minimal care.

\"Pothos

Pothos

The ultimate climbing vine plant. Beautiful trailing foliage that adapts to any environment.

About Our Garden

We believe that plants aren't just decorations—they're living companions that transform your space and improve your wellbeing. Our mission is to help you discover the perfect plants for your home or office.

Each plant in our collection is carefully selected for its aesthetic beauty and resilience. Whether you're a seasoned gardener or just starting your plant journey, we have something for everyone.

From tropical wonders to hardy succulents, explore the natural beauty of our curated selection and bring life to your surroundings.

Ready to Start Your Garden?

Join our community of plant enthusiasts and receive care tips, exclusive plant recommendations, and seasonal updates.

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) + templ_7745c5c3_Err = Base().Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func HelloPost(name string) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var3 := templ.GetChildren(ctx) + if templ_7745c5c3_Var3 == nil { + templ_7745c5c3_Var3 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "

Hello, ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var4 string + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(name) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/web/hello.templ`, Line: 167, Col: 39} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/cmd/web/styles/input.css b/cmd/web/styles/input.css new file mode 100644 index 0000000..b5c61c9 --- /dev/null +++ b/cmd/web/styles/input.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..f8b712a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,47 @@ +services: + app: + build: + context: . + dockerfile: Dockerfile + target: prod + restart: unless-stopped + ports: + - ${PORT}:${PORT} + environment: + APP_ENV: ${APP_ENV} + PORT: ${PORT} + BLUEPRINT_DB_HOST: ${BLUEPRINT_DB_HOST} + BLUEPRINT_DB_PORT: ${BLUEPRINT_DB_PORT} + BLUEPRINT_DB_DATABASE: ${BLUEPRINT_DB_DATABASE} + BLUEPRINT_DB_USERNAME: ${BLUEPRINT_DB_USERNAME} + BLUEPRINT_DB_PASSWORD: ${BLUEPRINT_DB_PASSWORD} + BLUEPRINT_DB_SCHEMA: ${BLUEPRINT_DB_SCHEMA} + depends_on: + psql_bp: + condition: service_healthy + networks: + - blueprint + psql_bp: + image: postgres:latest + restart: unless-stopped + environment: + POSTGRES_DB: ${BLUEPRINT_DB_DATABASE} + POSTGRES_USER: ${BLUEPRINT_DB_USERNAME} + POSTGRES_PASSWORD: ${BLUEPRINT_DB_PASSWORD} + ports: + - "${BLUEPRINT_DB_PORT}:5432" + volumes: + - psql_volume_bp:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "sh -c 'pg_isready -U ${BLUEPRINT_DB_USERNAME} -d ${BLUEPRINT_DB_DATABASE}'"] + interval: 5s + timeout: 5s + retries: 3 + start_period: 15s + networks: + - blueprint + +volumes: + psql_volume_bp: +networks: + blueprint: diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..285e70e --- /dev/null +++ b/go.mod @@ -0,0 +1,74 @@ +module go-htmx + +go 1.25.5 + +require ( + github.com/a-h/templ v0.3.977 + github.com/jackc/pgx/v5 v5.8.0 + github.com/joho/godotenv v1.5.1 + github.com/testcontainers/testcontainers-go v0.40.0 + github.com/testcontainers/testcontainers-go/modules/postgres v0.40.0 +) + +require ( + dario.cat/mergo v1.0.2 // indirect + github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v0.2.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/docker v28.5.1+incompatible // indirect + github.com/docker/go-connections v0.6.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/ebitengine/purego v0.8.4 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/magiconair/properties v1.8.10 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/go-archive v0.1.0 // indirect + github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/sys/sequential v0.6.0 // indirect + github.com/moby/sys/user v0.4.0 // indirect + github.com/moby/sys/userns v0.1.0 // indirect + github.com/moby/term v0.5.0 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/shirou/gopsutil/v4 v4.25.6 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/stretchr/testify v1.11.1 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect + go.opentelemetry.io/otel v1.39.0 // indirect + go.opentelemetry.io/otel/metric v1.39.0 // indirect + go.opentelemetry.io/otel/sdk v1.39.0 // indirect + go.opentelemetry.io/otel/trace v1.39.0 // indirect + golang.org/x/crypto v0.43.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/text v0.34.0 // indirect + google.golang.org/grpc v1.79.1 // indirect + google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..ed72583 --- /dev/null +++ b/go.sum @@ -0,0 +1,185 @@ +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/a-h/templ v0.3.977 h1:kiKAPXTZE2Iaf8JbtM21r54A8bCNsncrfnokZZSrSDg= +github.com/a-h/templ v0.3.977/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM= +github.com/docker/docker v28.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= +github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= +github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo= +github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= +github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI= +github.com/mdelapenya/tlscert v0.2.0/go.mod h1:O4njj3ELLnJjGdkN7M/vIVCpZ+Cf0L6muqOG4tLSl8o= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ= +github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= +github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= +github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +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/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs= +github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/testcontainers/testcontainers-go v0.40.0 h1:pSdJYLOVgLE8YdUY2FHQ1Fxu+aMnb6JfVz1mxk7OeMU= +github.com/testcontainers/testcontainers-go v0.40.0/go.mod h1:FSXV5KQtX2HAMlm7U3APNyLkkap35zNLxukw9oBi/MY= +github.com/testcontainers/testcontainers-go/modules/postgres v0.40.0 h1:s2bIayFXlbDFexo96y+htn7FzuhpXLYJNnIuglNKqOk= +github.com/testcontainers/testcontainers-go/modules/postgres v0.40.0/go.mod h1:h+u/2KoREGTnTl9UwrQ/g+XhasAT8E6dClclAADeXoQ= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= +go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0= +google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= +google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= +google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= diff --git a/internal/database/database.go b/internal/database/database.go new file mode 100644 index 0000000..3d6ed18 --- /dev/null +++ b/internal/database/database.go @@ -0,0 +1,115 @@ +package database + +import ( + "context" + "database/sql" + "fmt" + "log" + "os" + "strconv" + "time" + + _ "github.com/jackc/pgx/v5/stdlib" + _ "github.com/joho/godotenv/autoload" +) + +// 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 ( + database = os.Getenv("BLUEPRINT_DB_DATABASE") + password = os.Getenv("BLUEPRINT_DB_PASSWORD") + username = os.Getenv("BLUEPRINT_DB_USERNAME") + port = os.Getenv("BLUEPRINT_DB_PORT") + host = os.Getenv("BLUEPRINT_DB_HOST") + schema = os.Getenv("BLUEPRINT_DB_SCHEMA") + dbInstance *service +) + +func New() Service { + // Reuse Connection + if dbInstance != nil { + return dbInstance + } + connStr := fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable&search_path=%s", username, password, host, port, database, schema) + db, err := sql.Open("pgx", connStr) + if err != nil { + 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", database) + return s.db.Close() +} diff --git a/internal/database/database_test.go b/internal/database/database_test.go new file mode 100644 index 0000000..c36b626 --- /dev/null +++ b/internal/database/database_test.go @@ -0,0 +1,100 @@ +package database + +import ( + "context" + "log" + "testing" + "time" + + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/modules/postgres" + "github.com/testcontainers/testcontainers-go/wait" +) + +func mustStartPostgresContainer() (func(context.Context, ...testcontainers.TerminateOption) error, error) { + var ( + dbName = "database" + dbPwd = "password" + dbUser = "user" + ) + + dbContainer, err := postgres.Run( + context.Background(), + "postgres:latest", + postgres.WithDatabase(dbName), + postgres.WithUsername(dbUser), + postgres.WithPassword(dbPwd), + testcontainers.WithWaitStrategy( + wait.ForLog("database system is ready to accept connections"). + WithOccurrence(2). + WithStartupTimeout(5*time.Second)), + ) + if err != nil { + return nil, err + } + + database = dbName + password = dbPwd + username = dbUser + + dbHost, err := dbContainer.Host(context.Background()) + if err != nil { + return dbContainer.Terminate, err + } + + dbPort, err := dbContainer.MappedPort(context.Background(), "5432/tcp") + if err != nil { + return dbContainer.Terminate, err + } + + host = dbHost + port = dbPort.Port() + + return dbContainer.Terminate, err +} + +func TestMain(m *testing.M) { + teardown, err := mustStartPostgresContainer() + if err != nil { + log.Fatalf("could not start postgres container: %v", err) + } + + m.Run() + + if teardown != nil && teardown(context.Background()) != nil { + log.Fatalf("could not teardown postgres container: %v", err) + } +} + +func TestNew(t *testing.T) { + srv := New() + if srv == nil { + t.Fatal("New() returned nil") + } +} + +func TestHealth(t *testing.T) { + srv := New() + + stats := srv.Health() + + if stats["status"] != "up" { + t.Fatalf("expected status to be up, got %s", stats["status"]) + } + + if _, ok := stats["error"]; ok { + t.Fatalf("expected error not to be present") + } + + if stats["message"] != "It's healthy" { + t.Fatalf("expected message to be 'It's healthy', got %s", stats["message"]) + } +} + +func TestClose(t *testing.T) { + srv := New() + + if srv.Close() != nil { + t.Fatalf("expected Close() to return nil") + } +} diff --git a/internal/server/routes.go b/internal/server/routes.go new file mode 100644 index 0000000..156dbb5 --- /dev/null +++ b/internal/server/routes.go @@ -0,0 +1,71 @@ +package server + +import ( + "encoding/json" + "log" + "net/http" + + "github.com/a-h/templ" + "go-htmx/cmd/web" +) + +func (s *Server) RegisterRoutes() http.Handler { + mux := http.NewServeMux() + + // Register routes + mux.HandleFunc("/", s.HelloWorldHandler) + + mux.HandleFunc("/health", s.healthHandler) + + fileServer := http.FileServer(http.FS(web.Files)) + mux.Handle("/assets/", fileServer) + mux.Handle("/web", templ.Handler(web.HelloForm())) + mux.HandleFunc("/hello", web.HelloWebHandler) + + // Wrap the mux with CORS middleware + return s.corsMiddleware(mux) +} + +func (s *Server) corsMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Set CORS headers + w.Header().Set("Access-Control-Allow-Origin", "*") // Replace "*" with specific origins if needed + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, PATCH") + w.Header().Set("Access-Control-Allow-Headers", "Accept, Authorization, Content-Type, X-CSRF-Token") + w.Header().Set("Access-Control-Allow-Credentials", "false") // Set to "true" if credentials are required + + // Handle preflight OPTIONS requests + if r.Method == http.MethodOptions { + w.WriteHeader(http.StatusNoContent) + return + } + + // Proceed with the next handler + next.ServeHTTP(w, r) + }) +} + +func (s *Server) HelloWorldHandler(w http.ResponseWriter, r *http.Request) { + resp := map[string]string{"message": "Hello World"} + jsonResp, err := json.Marshal(resp) + if err != nil { + http.Error(w, "Failed to marshal response", http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + if _, err := w.Write(jsonResp); err != nil { + log.Printf("Failed to write response: %v", err) + } +} + +func (s *Server) healthHandler(w http.ResponseWriter, r *http.Request) { + resp, err := json.Marshal(s.db.Health()) + if err != nil { + http.Error(w, "Failed to marshal health check response", http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + if _, err := w.Write(resp); err != nil { + log.Printf("Failed to write response: %v", err) + } +} diff --git a/internal/server/routes_test.go b/internal/server/routes_test.go new file mode 100644 index 0000000..602ae81 --- /dev/null +++ b/internal/server/routes_test.go @@ -0,0 +1,31 @@ +package server + +import ( + "io" + "net/http" + "net/http/httptest" + "testing" +) + +func TestHandler(t *testing.T) { + s := &Server{} + server := httptest.NewServer(http.HandlerFunc(s.HelloWorldHandler)) + defer server.Close() + resp, err := http.Get(server.URL) + if err != nil { + t.Fatalf("error making request to server. Err: %v", err) + } + defer resp.Body.Close() + // Assertions + if resp.StatusCode != http.StatusOK { + t.Errorf("expected status OK; got %v", resp.Status) + } + expected := "{\"message\":\"Hello World\"}" + body, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatalf("error reading response body. Err: %v", err) + } + if expected != string(body) { + t.Errorf("expected response body to be %v; got %v", expected, string(body)) + } +} diff --git a/internal/server/server.go b/internal/server/server.go new file mode 100644 index 0000000..0ac2fdb --- /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" + + "go-htmx/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/main b/main new file mode 100755 index 0000000..45cf51d Binary files /dev/null and b/main differ diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..4941889 --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,40 @@ +// module.exports = { +// content: ["./**/*.html", "./**/*.templ", "./**/*.go",], +// theme: { extend: {}, }, +// plugins: [], +// } +// +module.exports = { + content: ["./**/*.html", "./**/*.templ", "./**/*.go"], + theme: { + extend: { + colors: { + crema: { + 50: "#fdfbf7", + 100: "#f6f1e8", + 200: "#e8dfcf", + 300: "#d8cbb3", + }, + oliva: { + 100: "#dfe8dc", + 200: "#b7c9b1", + 300: "#8fa789", + 400: "#6f8c6a", + }, + terracotta: { + 400: "#d17b49", + 500: "#bf6436", + 600: "#a9532c", + }, + ink: { + 700: "#3a3a37", + 800: "#2a2a28", + } + }, + fontFamily: { + latin: ["ui-serif", "Georgia", "serif"], + } + }, + }, + plugins: [], +} diff --git a/tailwindcss b/tailwindcss new file mode 100755 index 0000000..8bd1760 Binary files /dev/null and b/tailwindcss differ