commit c1f386ef44ac249d03592bad026a3fd8a31ab154 Author: Per Magnus Petersen Date: Sat Feb 14 11:45:09 2026 +0100 Initial Kit project with PostgreSQL, HTMX, Hiccup, and Tailwind Co-Authored-By: Claude Opus 4.6 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7b95c66 --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +kit.git-config.edn +.DS_Store +.nrepl-port +hs_err_pid*.log +.cpcache/ +target +log/ +.shadow-cljs/ +node_modules/ +modules/ +resources/public/css/output.css + +# IntelliJ +*.iml +.idea/ +*.bak + +# clj-kondo +.clj-kondo/.cache/ +.calva/output-window/ +.lsp/.cache/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5973d48 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +# syntax = docker/dockerfile:1.2 +FROM clojure:openjdk-17 AS build + +WORKDIR / +COPY . / + +RUN clj -Sforce -T:build all + +FROM azul/zulu-openjdk-alpine:17 + +COPY --from=build /target/elprice-standalone.jar /elprice/elprice-standalone.jar + +EXPOSE $PORT + +ENTRYPOINT exec java $JAVA_OPTS -jar /elprice/elprice-standalone.jar diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..34e64b5 --- /dev/null +++ b/Makefile @@ -0,0 +1,14 @@ +clean: + rm -rf target + +run: + clj -M:dev + +repl: + clj -M:dev:nrepl + +test: + clj -M:test + +uberjar: + clj -T:build all diff --git a/README.md b/README.md new file mode 100644 index 0000000..b2f135f --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +# elprice + +Start a [REPL](#repls) in your editor or terminal of choice. + +Start the server with: + +```clojure +(go) +``` + +The default API is available under http://localhost:3000/api + +System configuration is available under `resources/system.edn`. + +To reload changes: + +```clojure +(reset) +``` + +## REPLs + +### Cursive + +Configure a [REPL following the Cursive documentation](https://cursive-ide.com/userguide/repl.html). Using the default "Run with IntelliJ project classpath" option will let you select an alias from the ["Clojure deps" aliases selection](https://cursive-ide.com/userguide/deps.html#refreshing-deps-dependencies). + +### CIDER + +Use the `cider` alias for CIDER nREPL support (run `clj -M:dev:cider`). See the [CIDER docs](https://docs.cider.mx/cider/basics/up_and_running.html) for more help. + +Note that this alias runs nREPL during development. To run nREPL in production (typically when the system starts), use the kit-nrepl library through the +nrepl profile as described in [the documentation](https://kit-clj.github.io/docs/profiles.html#profiles). + +### Command Line + +Run `clj -M:dev:nrepl` or `make repl`. + +Note that, just like with [CIDER](#cider), this alias runs nREPL during development. To run nREPL in production (typically when the system starts), use the kit-nrepl library through the +nrepl profile as described in [the documentation](https://kit-clj.github.io/docs/profiles.html#profiles). diff --git a/bb.edn b/bb.edn new file mode 100644 index 0000000..c336062 --- /dev/null +++ b/bb.edn @@ -0,0 +1,27 @@ +{:min-bb-version "0.8.156" + :deps {failjure/failjure {:mvn/version "2.3.0"}} + :tasks {:requires ([babashka.fs :as fs] + [babashka.tasks :refer [shell]]) + + run {:doc "starts the app" + :task (if (fs/windows?) + (clojure {:dir "."} "-M:dev") + (shell {:dir "."} "clj -M:dev"))} + + nrepl {:doc "starts the nREPL" + :task (clojure {:dir "."} "-M:dev:nrepl")} + + cider {:doc "starts the cider" + :task (clojure {:dir "."} "-M:dev:cider")} + + test {:doc "runs tests" + :task (clojure {:dir "."} "-M:test")} + + uberjar {:doc "builds the uberjar" + :task (clojure {:dir "."} "-T:build all")} + + format {:doc "Formats codebase" + :task (try + (shell {:dir "src"} "cljstyle fix") + (catch Exception _ + (clojure {:dir "src"} "-M:dev -m cljstyle.main fix")))}}} diff --git a/build.clj b/build.clj new file mode 100644 index 0000000..ac8a55d --- /dev/null +++ b/build.clj @@ -0,0 +1,41 @@ +(ns build + (:require [clojure.string :as string] + [clojure.tools.build.api :as b])) + +(def lib 'pmagnus/elprice) +(def main-cls (string/join "." (filter some? [(namespace lib) (name lib) "core"]))) +(def version (format "0.0.1-SNAPSHOT")) +(def target-dir "target") +(def class-dir (str target-dir "/" "classes")) +(def uber-file (format "%s/%s-standalone.jar" target-dir (name lib))) +(def basis (b/create-basis {:project "deps.edn"})) + +(defn clean + "Delete the build target directory" + [_] + (println (str "Cleaning " target-dir)) + (b/delete {:path target-dir})) + +(defn prep [_] + (println "Writing Pom...") + (b/write-pom {:class-dir class-dir + :lib lib + :version version + :basis basis + :src-dirs ["src/clj"]}) + (b/copy-dir {:src-dirs ["src/clj" "resources" "env/prod/resources" "env/prod/clj"] + :target-dir class-dir})) + +(defn uber [_] + (println "Compiling Clojure...") + (b/compile-clj {:basis basis + :src-dirs ["src/clj" "resources" "env/prod/resources" "env/prod/clj"] + :class-dir class-dir}) + (println "Making uberjar...") + (b/uber {:class-dir class-dir + :uber-file uber-file + :main main-cls + :basis basis})) + +(defn all [_] + (do (clean nil) (prep nil) (uber nil))) diff --git a/deps.edn b/deps.edn new file mode 100644 index 0000000..439858f --- /dev/null +++ b/deps.edn @@ -0,0 +1,69 @@ +{:paths ["src/clj" + "resources"] + + :deps {org.clojure/clojure {:mvn/version "1.12.3"} + + ;; Routing + metosin/reitit {:mvn/version "0.9.2"} + + ;; Ring + metosin/ring-http-response {:mvn/version "0.9.5"} + ring/ring-core {:mvn/version "1.15.3"} + ring/ring-defaults {:mvn/version "0.7.0"} + + ;; Logging + ch.qos.logback/logback-classic {:mvn/version "1.5.20"} + + ;; Data coercion + luminus-transit/luminus-transit {:mvn/version "0.1.6" + :exclusions [com.cognitect/transit-clj]} + metosin/muuntaja {:mvn/version "0.6.11"} + + ;; kit Libs + io.github.kit-clj/kit-core {:mvn/version "1.0.9"} + io.github.kit-clj/kit-undertow {:mvn/version "1.0.10"} + io.github.kit-clj/kit-postgres {:mvn/version "1.0.7"} + io.github.kit-clj/kit-sql-conman {:mvn/version "1.10.5"} + io.github.kit-clj/kit-sql-migratus {:mvn/version "1.0.5"} + hiccup/hiccup {:mvn/version "2.0.0"}} + :aliases {:build {:deps {io.github.clojure/tools.build {:mvn/version "0.10.11"}} + :ns-default build} + + + :dev {:extra-deps {com.lambdaisland/classpath {:mvn/version "0.6.58"} + criterium/criterium {:mvn/version "0.4.6"} + expound/expound {:mvn/version "0.9.0"} + integrant/repl {:mvn/version "0.5.0"} + mvxcvi/cljstyle {:mvn/version "0.17.642"} + pjstadig/humane-test-output {:mvn/version "0.11.0"} + ring/ring-devel {:mvn/version "1.15.3"} + ring/ring-mock {:mvn/version "0.6.2"} + io.github.kit-clj/kit-generator {:mvn/version "0.2.5"} + org.clojure/tools.namespace {:mvn/version "1.5.0"} + } + :extra-paths ["env/dev/clj" "env/dev/resources" "test/clj"]} + :nrepl {:extra-deps {nrepl/nrepl {:mvn/version "1.5.1"}} + :main-opts ["-m" "nrepl.cmdline" "-i"]} + :cider {:extra-deps {nrepl/nrepl {:mvn/version "1.5.1"} + cider/cider-nrepl {:mvn/version "0.58.0"}} + :main-opts ["-m" "nrepl.cmdline" "--middleware" "[cider.nrepl/cider-middleware]" "-i"]} + + :test {:extra-deps {criterium/criterium {:mvn/version "0.4.6"} + expound/expound {:mvn/version "0.9.0"} + integrant/repl {:mvn/version "0.5.0"} + io.github.cognitect-labs/test-runner {:git/url "https://github.com/cognitect-labs/test-runner.git" + :git/tag "v0.5.1" + :git/sha "dfb30dd"} + pjstadig/humane-test-output {:mvn/version "0.11.0"} + ring/ring-devel {:mvn/version "1.15.3"} + ring/ring-mock {:mvn/version "0.6.2"} + io.github.kit-clj/kit-generator {:mvn/version "0.2.5"} + org.clojure/tools.namespace {:mvn/version "1.5.0"} + peridot/peridot {:mvn/version "0.5.4"} + org.clj-commons/byte-streams {:mvn/version "0.3.4"} + com.lambdaisland/classpath {:mvn/version "0.6.58"}} + :exec-fn cognitect.test-runner.api/test + :extra-paths ["env/dev/clj" "env/dev/resources" "env/test/resources" "test/clj"] + :main-opts ["-e" "(require 'pjstadig.humane-test-output) (pjstadig.humane-test-output/activate!)" + "-m" "cognitect.test-runner"]}} + } diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..94c9542 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,14 @@ +services: + postgres: + image: postgres:16-alpine + environment: + POSTGRES_DB: elprice + POSTGRES_USER: elprice + POSTGRES_PASSWORD: elprice + ports: + - "5432:5432" + volumes: + - pgdata:/var/lib/postgresql/data + +volumes: + pgdata: diff --git a/env/dev/clj/pmagnus/elprice/dev_middleware.clj b/env/dev/clj/pmagnus/elprice/dev_middleware.clj new file mode 100644 index 0000000..a63844c --- /dev/null +++ b/env/dev/clj/pmagnus/elprice/dev_middleware.clj @@ -0,0 +1,5 @@ +(ns pmagnus.elprice.dev-middleware) + +(defn wrap-dev [handler _opts] + (-> handler + )) diff --git a/env/dev/clj/pmagnus/elprice/env.clj b/env/dev/clj/pmagnus/elprice/env.clj new file mode 100644 index 0000000..daf5725 --- /dev/null +++ b/env/dev/clj/pmagnus/elprice/env.clj @@ -0,0 +1,14 @@ +(ns pmagnus.elprice.env + (:require + [clojure.tools.logging :as log] + [pmagnus.elprice.dev-middleware :refer [wrap-dev]])) + +(def defaults + {:init (fn [] + (log/info "\n-=[elprice starting using the development or test profile]=-")) + :start (fn [] + (log/info "\n-=[elprice started successfully using the development or test profile]=-")) + :stop (fn [] + (log/info "\n-=[elprice has shut down successfully]=-")) + :middleware wrap-dev + :opts {:profile :dev}}) diff --git a/env/dev/clj/user.clj b/env/dev/clj/user.clj new file mode 100644 index 0000000..25a9817 --- /dev/null +++ b/env/dev/clj/user.clj @@ -0,0 +1,59 @@ +(ns user + "Userspace functions you can run by default in your local REPL." + (:require + [clojure.pprint] + [clojure.spec.alpha :as s] + [clojure.tools.namespace.repl :as repl] + [criterium.core :as c] ;; benchmarking + [expound.alpha :as expound] + [integrant.core :as ig] + [integrant.repl :refer [clear go halt prep init reset reset-all]] + [integrant.repl.state :as state] + [kit.api :as kit] + [lambdaisland.classpath.watch-deps :as watch-deps] ;; hot loading for deps + [pmagnus.elprice.core :refer [start-app]])) + +;; uncomment to enable hot loading for deps +(watch-deps/start! {:aliases [:dev :test]}) + +(alter-var-root #'s/*explain-out* (constantly expound/printer)) + +(add-tap (bound-fn* clojure.pprint/pprint)) + +(defn dev-prep! + [] + (integrant.repl/set-prep! (fn [] + (-> (pmagnus.elprice.config/system-config {:profile :dev}) + (ig/expand))))) + +(defn test-prep! + [] + (integrant.repl/set-prep! (fn [] + (-> (pmagnus.elprice.config/system-config {:profile :test}) + (ig/expand))))) + +;; Can change this to test-prep! if want to run tests as the test profile in your repl +;; You can run tests in the dev profile, too, but there are some differences between +;; the two profiles. +(dev-prep!) + +(repl/set-refresh-dirs "src/clj") + +(def refresh repl/refresh) + + +(defn reset-db [] + (migratus.core/reset (:db.sql/migrations state/system))) + +(defn rollback [] + (migratus.core/rollback (:db.sql/migrations state/system))) + +(defn migrate [] + (migratus.core/migrate (:db.sql/migrations state/system))) + +(def query-fn (:db.sql/query-fn state/system)) + + +(comment + (go) + (reset)) diff --git a/env/dev/resources/logback.xml b/env/dev/resources/logback.xml new file mode 100644 index 0000000..d487bb6 --- /dev/null +++ b/env/dev/resources/logback.xml @@ -0,0 +1,38 @@ + + + + + + + UTF-8 + %date{ISO8601} [%thread] %-5level %logger{36} - %msg %n + + + + log/pmagnus.elprice.log + + log/pmagnus.elprice.%d{yyyy-MM-dd}.%i.log + + 100MB + + + 30 + + + UTF-8 + %date{ISO8601} [%thread] %-5level %logger{36} - %msg %n + + + + + + + + + + + + + + diff --git a/env/prod/clj/pmagnus/elprice/env.clj b/env/prod/clj/pmagnus/elprice/env.clj new file mode 100644 index 0000000..e2a02fd --- /dev/null +++ b/env/prod/clj/pmagnus/elprice/env.clj @@ -0,0 +1,12 @@ +(ns pmagnus.elprice.env + (:require [clojure.tools.logging :as log])) + +(def defaults + {:init (fn [] + (log/info "\n-=[elprice starting]=-")) + :start (fn [] + (log/info "\n-=[elprice started successfully]=-")) + :stop (fn [] + (log/info "\n-=[elprice has shut down successfully]=-")) + :middleware (fn [handler _] handler) + :opts {:profile :prod}}) diff --git a/env/prod/resources/logback.xml b/env/prod/resources/logback.xml new file mode 100644 index 0000000..2fade8d --- /dev/null +++ b/env/prod/resources/logback.xml @@ -0,0 +1,16 @@ + + + + + + %-5relative %-5level %logger{35} - %msg%n + + + + + + + + + + diff --git a/env/test/resources/logback.xml b/env/test/resources/logback.xml new file mode 100644 index 0000000..2fade8d --- /dev/null +++ b/env/test/resources/logback.xml @@ -0,0 +1,16 @@ + + + + + + %-5relative %-5level %logger{35} - %msg%n + + + + + + + + + + diff --git a/kit.edn b/kit.edn new file mode 100644 index 0000000..c8c3423 --- /dev/null +++ b/kit.edn @@ -0,0 +1,8 @@ +{:full-name "pmagnus/elprice" + :ns-name "pmagnus.elprice" + :sanitized "pmagnus/elprice" + :name "elprice" + :modules {:root "modules" + :repositories [{:url "https://github.com/kit-clj/modules.git" + :tag "master" + :name "kit-modules"}]}} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..16d7489 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1055 @@ +{ + "name": "elprice", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "elprice", + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "@tailwindcss/cli": "^4.1.18", + "tailwindcss": "^4.1.18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/cli": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.1.18.tgz", + "integrity": "sha512-sMZ+lZbDyxwjD2E0L7oRUjJ01Ffjtme5OtjvvnC+cV4CEDcbqzbp25TCpxHj6kWLU9+DlqJOiNgSOgctC2aZmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/watcher": "^2.5.1", + "@tailwindcss/node": "4.1.18", + "@tailwindcss/oxide": "4.1.18", + "enhanced-resolve": "^5.18.3", + "mri": "^1.2.0", + "picocolors": "^1.1.1", + "tailwindcss": "4.1.18" + }, + "bin": { + "tailwindcss": "dist/index.mjs" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz", + "integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.6.1", + "lightningcss": "1.30.2", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.18" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz", + "integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-x64": "4.1.18", + "@tailwindcss/oxide-freebsd-x64": "4.1.18", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-x64-musl": "4.1.18", + "@tailwindcss/oxide-wasm32-wasi": "4.1.18", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz", + "integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz", + "integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz", + "integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz", + "integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz", + "integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz", + "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz", + "integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz", + "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz", + "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz", + "integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.0", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", + "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz", + "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", + "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/lightningcss": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", + "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.30.2", + "lightningcss-darwin-arm64": "1.30.2", + "lightningcss-darwin-x64": "1.30.2", + "lightningcss-freebsd-x64": "1.30.2", + "lightningcss-linux-arm-gnueabihf": "1.30.2", + "lightningcss-linux-arm64-gnu": "1.30.2", + "lightningcss-linux-arm64-musl": "1.30.2", + "lightningcss-linux-x64-gnu": "1.30.2", + "lightningcss-linux-x64-musl": "1.30.2", + "lightningcss-win32-arm64-msvc": "1.30.2", + "lightningcss-win32-x64-msvc": "1.30.2" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", + "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", + "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", + "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", + "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", + "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", + "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", + "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", + "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", + "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", + "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", + "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", + "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..01bb524 --- /dev/null +++ b/package.json @@ -0,0 +1,21 @@ +{ + "name": "elprice", + "version": "1.0.0", + "description": "Start a [REPL](#repls) in your editor or terminal of choice.", + "main": "index.js", + "directories": { + "test": "test" + }, + "scripts": { + "css:build": "npx @tailwindcss/cli -i resources/css/input.css -o resources/public/css/output.css --minify", + "css:watch": "npx @tailwindcss/cli -i resources/css/input.css -o resources/public/css/output.css --watch" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs", + "devDependencies": { + "@tailwindcss/cli": "^4.1.18", + "tailwindcss": "^4.1.18" + } +} diff --git a/resources/css/input.css b/resources/css/input.css new file mode 100644 index 0000000..f1d8c73 --- /dev/null +++ b/resources/css/input.css @@ -0,0 +1 @@ +@import "tailwindcss"; diff --git a/resources/migrations/placeholder.txt b/resources/migrations/placeholder.txt new file mode 100644 index 0000000..0b1cbc7 --- /dev/null +++ b/resources/migrations/placeholder.txt @@ -0,0 +1 @@ +See https://github.com/yogthos/migratus diff --git a/resources/queries.sql b/resources/queries.sql new file mode 100644 index 0000000..8f64c5f --- /dev/null +++ b/resources/queries.sql @@ -0,0 +1,2 @@ +-- place your sql queries here +-- see https://www.hugsql.org/ for documentation diff --git a/resources/system.edn b/resources/system.edn new file mode 100644 index 0000000..47b8ec2 --- /dev/null +++ b/resources/system.edn @@ -0,0 +1,69 @@ +{:system/env + #profile {:dev :dev + :test :test + :prod :prod} + + :server/http + {:port #long #or [#env PORT 3000] + :host #or [#env HTTP_HOST "0.0.0.0"] + :handler #ig/ref :handler/ring} + + :handler/ring + {:router #ig/ref :router/core + :api-path "/api" + :cookie-secret #or [#env COOKIE_SECRET "JJCILTOQLMYRBFRU"] + ;; from ring.middleware.defaults. anti-forgery `false` by default because services may not require it + :site-defaults-config {:params {:urlencoded true + :multipart true + :nested true + :keywordize true} + :cookies true + :session {:flash true + :cookie-name "pmagnus.elprice" + :cookie-attrs {:max-age 86400 + :http-only true + :same-site :strict}} + :security {:anti-forgery false + :xss-protection {:enable? true, + :mode :block} + :frame-options :sameorigin + :content-type-options :nosniff} + :static {:resources "public"} + :responses {:not-modified-responses true + :absolute-redirects true + :content-types true + :default-charset "utf-8"}}} + + :reitit.routes/api + {:base-path "/api" + :env #ig/ref :system/env + :query-fn #ig/ref :db.sql/query-fn} + + :router/routes + {:routes #ig/refset :reitit/routes} + + :router/core + {:routes #ig/ref :router/routes + :env #ig/ref :system/env} + + :db.sql/connection + #profile {:dev {:jdbc-url "jdbc:postgresql://localhost:5432/elprice?user=elprice&password=elprice"} + :test {:jdbc-url "jdbc:postgresql://localhost:5432/elprice?user=elprice&password=elprice"} + :prod {:jdbc-url #env JDBC_URL + :init-size 1 + :min-idle 1 + :max-idle 8 + :max-active 32}} + + :db.sql/query-fn + {:conn #ig/ref :db.sql/connection + :options {} + :filename "queries.sql" + :env #ig/ref :system/env} + + :db.sql/migrations + {:store :database + :db {:datasource #ig/ref :db.sql/connection} + :migrate-on-init? true} + :reitit.routes/ui {:base-path "", + :env #ig/ref :system/env}} diff --git a/src/clj/pmagnus/elprice/config.clj b/src/clj/pmagnus/elprice/config.clj new file mode 100644 index 0000000..778b4ec --- /dev/null +++ b/src/clj/pmagnus/elprice/config.clj @@ -0,0 +1,9 @@ +(ns pmagnus.elprice.config + (:require + [kit.config :as config])) + +(def ^:const system-filename "system.edn") + +(defn system-config + [options] + (config/read-config system-filename options)) diff --git a/src/clj/pmagnus/elprice/core.clj b/src/clj/pmagnus/elprice/core.clj new file mode 100644 index 0000000..607c6bc --- /dev/null +++ b/src/clj/pmagnus/elprice/core.clj @@ -0,0 +1,43 @@ +(ns pmagnus.elprice.core + (:require + [clojure.tools.logging :as log] + [integrant.core :as ig] + [pmagnus.elprice.config :as config] + [pmagnus.elprice.env :refer [defaults]] + + ;; Edges + [kit.edge.db.sql.conman] + [kit.edge.db.sql.migratus] + [kit.edge.db.postgres] + [kit.edge.db.mysql] + [kit.edge.server.undertow] + [pmagnus.elprice.web.handler] + + ;; Routes + [pmagnus.elprice.web.routes.api] + [pmagnus.elprice.web.routes.ui]) + (:gen-class)) + +;; log uncaught exceptions in threads +(Thread/setDefaultUncaughtExceptionHandler + (fn [thread ex] + (log/error {:what :uncaught-exception + :exception ex + :where (str "Uncaught exception on" (.getName thread))}))) + +(defonce system (atom nil)) + +(defn stop-app [] + ((or (:stop defaults) (fn []))) + (some-> (deref system) (ig/halt!))) + +(defn start-app [& [params]] + ((or (:start params) (:start defaults) (fn []))) + (->> (config/system-config (or (:opts params) (:opts defaults) {})) + (ig/expand) + (ig/init) + (reset! system))) + +(defn -main [& _] + (start-app) + (.addShutdownHook (Runtime/getRuntime) (Thread. (fn [] (stop-app) (shutdown-agents))))) diff --git a/src/clj/pmagnus/elprice/web/controllers/health.clj b/src/clj/pmagnus/elprice/web/controllers/health.clj new file mode 100644 index 0000000..0c1eb1b --- /dev/null +++ b/src/clj/pmagnus/elprice/web/controllers/health.clj @@ -0,0 +1,13 @@ +(ns pmagnus.elprice.web.controllers.health + (:require + [ring.util.http-response :as http-response]) + (:import + [java.util Date])) + +(defn healthcheck! + [req] + (http-response/ok + {:time (str (Date. (System/currentTimeMillis))) + :up-since (str (Date. (.getStartTime (java.lang.management.ManagementFactory/getRuntimeMXBean)))) + :app {:status "up" + :message ""}})) diff --git a/src/clj/pmagnus/elprice/web/handler.clj b/src/clj/pmagnus/elprice/web/handler.clj new file mode 100644 index 0000000..294c255 --- /dev/null +++ b/src/clj/pmagnus/elprice/web/handler.clj @@ -0,0 +1,45 @@ +(ns pmagnus.elprice.web.handler + (:require + [pmagnus.elprice.web.middleware.core :as middleware] + [integrant.core :as ig] + [ring.util.http-response :as http-response] + [reitit.ring :as ring] + [reitit.swagger-ui :as swagger-ui])) + +(defmethod ig/init-key :handler/ring + [_ {:keys [router api-path] :as opts}] + (ring/ring-handler + (router) + (ring/routes + ;; Handle trailing slash in routes - add it + redirect to it + ;; https://github.com/metosin/reitit/blob/master/doc/ring/slash_handler.md + (ring/redirect-trailing-slash-handler) + (ring/create-resource-handler {:path "/"}) + (when (some? api-path) + (swagger-ui/create-swagger-ui-handler {:path api-path + :url (str api-path "/swagger.json")})) + (ring/create-default-handler + {:not-found + (constantly (-> {:status 404, :body "Page not found"} + (http-response/content-type "text/plain"))) + :method-not-allowed + (constantly (-> {:status 405, :body "Not allowed"} + (http-response/content-type "text/plain"))) + :not-acceptable + (constantly (-> {:status 406, :body "Not acceptable"} + (http-response/content-type "text/plain")))})) + {:middleware [(middleware/wrap-base opts)]})) + +(defmethod ig/init-key :router/routes + [_ {:keys [routes]}] + (mapv (fn [route] + (if (fn? route) + (route) + route)) + routes)) + +(defmethod ig/init-key :router/core + [_ {:keys [routes env] :as opts}] + (if (= env :dev) + #(ring/router ["" opts routes]) + (constantly (ring/router ["" opts routes])))) diff --git a/src/clj/pmagnus/elprice/web/htmx.clj b/src/clj/pmagnus/elprice/web/htmx.clj new file mode 100644 index 0000000..34ac3ac --- /dev/null +++ b/src/clj/pmagnus/elprice/web/htmx.clj @@ -0,0 +1,25 @@ +(ns pmagnus.elprice.web.htmx + (:require + [ring.util.http-response :as http-response] + [hiccup.core :as h] + [hiccup.page :as p])) + +(defmacro page [opts & content] + `(-> (p/html5 ~opts + [:head + [:meta {:charset "UTF-8"}] + [:meta {:name "viewport" + :content "width=device-width, initial-scale=1, viewport-fit=cover"}] + [:link {:rel "stylesheet" :href "/css/output.css"}] + [:script {:src "https://unpkg.com/htmx.org@2.0.8/dist/htmx.min.js" + :defer true}]] + [:body {:class "bg-gray-50 min-h-screen"} + [:div {:class "mx-auto max-w-[390px] px-4 py-6"} + ~@content]]) + http-response/ok + (http-response/content-type "text/html"))) + +(defmacro fragment [opts & content] + `(-> (str (h/html ~opts ~@content)) + http-response/ok + (http-response/content-type "text/html"))) diff --git a/src/clj/pmagnus/elprice/web/middleware/core.clj b/src/clj/pmagnus/elprice/web/middleware/core.clj new file mode 100644 index 0000000..ee994cb --- /dev/null +++ b/src/clj/pmagnus/elprice/web/middleware/core.clj @@ -0,0 +1,14 @@ +(ns pmagnus.elprice.web.middleware.core + (:require + [pmagnus.elprice.env :as env] + [ring.middleware.defaults :as defaults] + [ring.middleware.session.cookie :as cookie])) + +(defn wrap-base + [{:keys [metrics site-defaults-config cookie-secret] :as opts}] + (let [cookie-store (cookie/cookie-store {:key (.getBytes ^String cookie-secret)})] + (fn [handler] + (cond-> ((:middleware env/defaults) handler opts) + true (defaults/wrap-defaults + (assoc-in site-defaults-config [:session :store] cookie-store)) + )))) diff --git a/src/clj/pmagnus/elprice/web/middleware/exception.clj b/src/clj/pmagnus/elprice/web/middleware/exception.clj new file mode 100644 index 0000000..4839105 --- /dev/null +++ b/src/clj/pmagnus/elprice/web/middleware/exception.clj @@ -0,0 +1,31 @@ +(ns pmagnus.elprice.web.middleware.exception + (:require + [clojure.tools.logging :as log] + [reitit.ring.middleware.exception :as exception])) + +(defn handler [message status exception request] + (when (>= status 500) + ;; You can optionally use this to report error to an external service + (log/error exception)) + {:status status + :body {:message message + :exception (.getClass exception) + :data (ex-data exception) + :uri (:uri request)}}) + +(def wrap-exception + (exception/create-exception-middleware + (merge + exception/default-handlers + {:system.exception/internal (partial handler "internal exception" 500) + :system.exception/business (partial handler "bad request" 400) + :system.exception/not-found (partial handler "not found" 404) + :system.exception/unauthorized (partial handler "unauthorized" 401) + :system.exception/forbidden (partial handler "forbidden" 403) + + ;; override the default handler + ::exception/default (partial handler "default" 500) + + ;; print stack-traces for all exceptions + ::exception/wrap (fn [handler e request] + (handler e request))}))) diff --git a/src/clj/pmagnus/elprice/web/middleware/formats.clj b/src/clj/pmagnus/elprice/web/middleware/formats.clj new file mode 100644 index 0000000..5d3644d --- /dev/null +++ b/src/clj/pmagnus/elprice/web/middleware/formats.clj @@ -0,0 +1,14 @@ +(ns pmagnus.elprice.web.middleware.formats + (:require + [luminus-transit.time :as time] + [muuntaja.core :as m])) + +(def instance + (m/create + (-> m/default-options + (update-in + [:formats "application/transit+json" :decoder-opts] + (partial merge time/time-deserialization-handlers)) + (update-in + [:formats "application/transit+json" :encoder-opts] + (partial merge time/time-serialization-handlers))))) diff --git a/src/clj/pmagnus/elprice/web/routes/api.clj b/src/clj/pmagnus/elprice/web/routes/api.clj new file mode 100644 index 0000000..cccfb24 --- /dev/null +++ b/src/clj/pmagnus/elprice/web/routes/api.clj @@ -0,0 +1,52 @@ +(ns pmagnus.elprice.web.routes.api + (:require + [pmagnus.elprice.web.controllers.health :as health] + [pmagnus.elprice.web.middleware.exception :as exception] + [pmagnus.elprice.web.middleware.formats :as formats] + [integrant.core :as ig] + [reitit.coercion.malli :as malli] + [reitit.ring.coercion :as coercion] + [reitit.ring.middleware.muuntaja :as muuntaja] + [reitit.ring.middleware.parameters :as parameters] + [reitit.swagger :as swagger])) + +(def route-data + {:coercion malli/coercion + :muuntaja formats/instance + :swagger {:id ::api} + :middleware [;; query-params & form-params + parameters/parameters-middleware + ;; content-negotiation + muuntaja/format-negotiate-middleware + ;; encoding response body + muuntaja/format-response-middleware + ;; exception handling + coercion/coerce-exceptions-middleware + ;; decoding request body + muuntaja/format-request-middleware + ;; coercing response bodys + coercion/coerce-response-middleware + ;; coercing request parameters + coercion/coerce-request-middleware + ;; exception handling + exception/wrap-exception]}) + +;; Routes +(defn api-routes [_opts] + [["/swagger.json" + {:get {:no-doc true + :swagger {:info {:title "pmagnus.elprice API"}} + :handler (swagger/create-swagger-handler)}}] + ["/health" + ;; note that use of the var is necessary + ;; for reitit to reload routes without + ;; restarting the system + {:get #'health/healthcheck!}]]) + +(derive :reitit.routes/api :reitit/routes) + +(defmethod ig/init-key :reitit.routes/api + [_ {:keys [base-path] + :or {base-path ""} + :as opts}] + (fn [] [base-path route-data (api-routes opts)])) diff --git a/src/clj/pmagnus/elprice/web/routes/ui.clj b/src/clj/pmagnus/elprice/web/routes/ui.clj new file mode 100644 index 0000000..7eaa3d1 --- /dev/null +++ b/src/clj/pmagnus/elprice/web/routes/ui.clj @@ -0,0 +1,50 @@ +(ns pmagnus.elprice.web.routes.ui + (:require + [pmagnus.elprice.web.middleware.exception :as exception] + [pmagnus.elprice.web.middleware.formats :as formats] + [pmagnus.elprice.web.routes.utils :as utils] + [pmagnus.elprice.web.htmx :refer [page fragment] :as htmx] + [integrant.core :as ig] + [reitit.ring.middleware.muuntaja :as muuntaja] + [reitit.ring.middleware.parameters :as parameters])) + +(defn home [request] + (page {:lang "en"} + [:h1 {:class "text-2xl font-bold text-gray-900"} + "Welcome to elprice"] + [:p {:class "mt-2 text-gray-600"} + "Built with Kit, HTMX, Hiccup & Tailwind"] + [:button {:class "mt-4 rounded-lg bg-blue-600 px-4 py-2 text-white font-medium + active:bg-blue-700 transition-colors" + :hx-post "/clicked" + :hx-swap "outerHTML"} + "Click me!"])) + +(defn clicked [request] + (fragment + [:div {:class "mt-4 rounded-lg bg-green-100 p-4 text-green-800"} + "Congratulations! You just clicked the button!"])) + +;; Routes +(defn ui-routes [_opts] + [["/" {:get home}] + ["/clicked" {:post clicked}]]) + +(def route-data + {:muuntaja formats/instance + :middleware + [;; Default middleware for ui + ;; query-params & form-params + parameters/parameters-middleware + ;; encoding response body + muuntaja/format-response-middleware + ;; exception handling + exception/wrap-exception]}) + +(derive :reitit.routes/ui :reitit/routes) + +(defmethod ig/init-key :reitit.routes/ui + [_ {:keys [base-path] + :or {base-path ""} + :as opts}] + (fn [] [base-path route-data (ui-routes opts)])) diff --git a/src/clj/pmagnus/elprice/web/routes/utils.clj b/src/clj/pmagnus/elprice/web/routes/utils.clj new file mode 100644 index 0000000..9869eab --- /dev/null +++ b/src/clj/pmagnus/elprice/web/routes/utils.clj @@ -0,0 +1,11 @@ +(ns pmagnus.elprice.web.routes.utils) + +(def route-data-path [:reitit.core/match :data]) + +(defn route-data + [req] + (get-in req route-data-path)) + +(defn route-data-key + [req k] + (get-in req (conj route-data-path k))) diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..ceef7fe --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,5 @@ +module.exports = { + content: ["./src/**/*.clj", "./resources/**/*.html"], + theme: { extend: {} }, + plugins: [] +} diff --git a/test/clj/pmagnus/elprice/core_test.clj b/test/clj/pmagnus/elprice/core_test.clj new file mode 100644 index 0000000..3bb8c4d --- /dev/null +++ b/test/clj/pmagnus/elprice/core_test.clj @@ -0,0 +1,8 @@ +(ns pmagnus.elprice.core-test + (:require + [pmagnus.elprice.test-utils :as utils] + [clojure.test :refer :all])) + +(deftest example-test + (is (= 1 2))) + diff --git a/test/clj/pmagnus/elprice/test_utils.clj b/test/clj/pmagnus/elprice/test_utils.clj new file mode 100644 index 0000000..ac96fbe --- /dev/null +++ b/test/clj/pmagnus/elprice/test_utils.clj @@ -0,0 +1,32 @@ +(ns pmagnus.elprice.test-utils + (:require + [pmagnus.elprice.core :as core] + [peridot.core :as p] + [byte-streams :as bs] + [integrant.repl.state :as state])) + +(defn system-state + [] + (or @core/system state/system)) + +(defn system-fixture + [] + (fn [f] + (when (nil? (system-state)) + (core/start-app {:opts {:profile :test}})) + (f) + (core/stop-app))) + +(defn get-response [ctx] + (-> ctx + :response + (update :body (fnil bs/to-string "")))) + +(defn GET [app path params headers] + (-> (p/session app) + (p/request path + :request-method :get + :content-type "application/edn" + :headers headers + :params params) + (get-response))) diff --git a/test/clj/pmagnus/elprice/web/request_test.clj b/test/clj/pmagnus/elprice/web/request_test.clj new file mode 100644 index 0000000..1b5d7ea --- /dev/null +++ b/test/clj/pmagnus/elprice/web/request_test.clj @@ -0,0 +1,13 @@ +(ns pmagnus.elprice.web.request-test + (:require [clojure.test :refer [deftest testing is use-fixtures]] + [pmagnus.elprice.test-utils :refer [system-state system-fixture GET]])) + +(use-fixtures :once (system-fixture)) + +(deftest health-request-test [] + (testing "happy path" + (let [handler (:handler/ring (system-state)) + params {} + headers {} + response (GET handler "/api/health" params headers)] + (is (= 200 (:status response))))))