Generated
+238
@@ -0,0 +1,238 @@
|
||||
{
|
||||
"name": "tbd-ui",
|
||||
"version": "0.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "tbd-ui",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"react": "^19.3.0",
|
||||
"react-dom": "^19.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"shadow-cljs": "^3.5.3"
|
||||
}
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/buffer": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-from": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz",
|
||||
"integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/process": {
|
||||
"version": "0.11.10",
|
||||
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
||||
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.3.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz",
|
||||
"integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "19.3.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz",
|
||||
"integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"scheduler": "^0.28.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^19.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/readline-sync": {
|
||||
"version": "1.4.10",
|
||||
"resolved": "https://registry.npmjs.org/readline-sync/-/readline-sync-1.4.10.tgz",
|
||||
"integrity": "sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz",
|
||||
"integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/shadow-cljs": {
|
||||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmjs.org/shadow-cljs/-/shadow-cljs-3.5.3.tgz",
|
||||
"integrity": "sha512-tDb/lTtHYTCP6VflT575u5oFqx242DgBu/pMqUF7ctA4VbA01X8pJE+9TAPY+D9pfQqWpqGrZLgmDWsmrabYVQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"buffer": "^6.0.3",
|
||||
"process": "^0.11.10",
|
||||
"readline-sync": "^1.4.10",
|
||||
"shadow-cljs-jar": "1.3.4",
|
||||
"source-map-support": "^0.5.21",
|
||||
"which": "^5.0.0",
|
||||
"ws": "^8.18.1"
|
||||
},
|
||||
"bin": {
|
||||
"shadow-cljs": "cli/runner.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/shadow-cljs-jar": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/shadow-cljs-jar/-/shadow-cljs-jar-1.3.4.tgz",
|
||||
"integrity": "sha512-cZB2pzVXBnhpJ6PQdsjO+j/MksR28mv4QD/hP/2y1fsIa9Z9RutYgh3N34FZ8Ktl4puAXaIGlct+gMCJ5BmwmA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-support": {
|
||||
"version": "0.5.21",
|
||||
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
|
||||
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-from": "^1.0.0",
|
||||
"source-map": "^0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz",
|
||||
"integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"isexe": "^3.1.1"
|
||||
},
|
||||
"bin": {
|
||||
"node-which": "bin/which.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.17.0 || >=20.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.21.3",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz",
|
||||
"integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "tbd-ui",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"description": "ClojureScript (re-frame) front end for the tbd server",
|
||||
"scripts": {
|
||||
"watch": "shadow-cljs watch app",
|
||||
"release": "shadow-cljs release app"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^19.3.0",
|
||||
"react-dom": "^19.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"shadow-cljs": "^3.5.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
;; The UI compiles straight into the server's resources dir, so the server picks it up from
|
||||
;; the classpath (see `tbd.core`, which serves `resources/public` at `/js`).
|
||||
{:source-paths ["src"]
|
||||
|
||||
:dependencies
|
||||
[[reagent "2.0.1"]
|
||||
[re-frame "1.4.7"]]
|
||||
|
||||
:builds
|
||||
{:app {:target :browser
|
||||
:output-dir "../resources/public/js"
|
||||
:asset-path "/js"
|
||||
:modules {:main {:init-fn tbd.ui.core/init}}
|
||||
:devtools {:after-load tbd.ui.core/reload}}}}
|
||||
@@ -0,0 +1,21 @@
|
||||
(ns tbd.ui.core
|
||||
(:require [re-frame.core :as rf]
|
||||
[reagent.dom.client :as rdc]
|
||||
[tbd.ui.events :as events]
|
||||
[tbd.ui.views :as views]))
|
||||
|
||||
(defonce root (atom nil))
|
||||
|
||||
(defn- render []
|
||||
(rdc/render @root [views/app]))
|
||||
|
||||
(defn reload
|
||||
"Called by shadow-cljs after each hot reload."
|
||||
[]
|
||||
(rf/clear-subscription-cache!)
|
||||
(render))
|
||||
|
||||
(defn init []
|
||||
(rf/dispatch-sync [::events/initialize])
|
||||
(reset! root (rdc/create-root (js/document.getElementById "app")))
|
||||
(render))
|
||||
@@ -0,0 +1,9 @@
|
||||
(ns tbd.ui.db)
|
||||
|
||||
(def default-db
|
||||
{:calc {:x "" :y ""
|
||||
;; {:status :idle}
|
||||
;; {:status :loading}
|
||||
;; {:status :ok :x "2" :y "3" :sum 5}
|
||||
;; {:status :error :message "..."}
|
||||
:result {:status :idle}}})
|
||||
@@ -0,0 +1,53 @@
|
||||
(ns tbd.ui.events
|
||||
(:require [cljs.reader :as reader]
|
||||
[clojure.string :as str]
|
||||
[re-frame.core :as rf]
|
||||
[tbd.ui.db :as db]))
|
||||
|
||||
;; Effect: GET `uri`, then dispatch `(conj on-response {:ok? bool :status int-or-nil :body string})`.
|
||||
;; Network failures come back as `{:ok? false :status nil :body <message>}`.
|
||||
(rf/reg-fx
|
||||
::fetch
|
||||
(fn [{:keys [uri on-response]}]
|
||||
(-> (js/fetch uri)
|
||||
(.then (fn [resp]
|
||||
(.then (.text resp)
|
||||
#(rf/dispatch (conj on-response {:ok? (.-ok resp)
|
||||
:status (.-status resp)
|
||||
:body %})))))
|
||||
(.catch #(rf/dispatch (conj on-response {:ok? false :status nil :body (.-message %)}))))))
|
||||
|
||||
(rf/reg-event-db
|
||||
::initialize
|
||||
(fn [_ _] db/default-db))
|
||||
|
||||
(rf/reg-event-db
|
||||
::set-operand
|
||||
(fn [db [_ k value]]
|
||||
(assoc-in db [:calc k] value)))
|
||||
|
||||
(rf/reg-event-fx
|
||||
::add
|
||||
(fn [{:keys [db]} _]
|
||||
(let [{:keys [x y]} (:calc db)]
|
||||
{:db (assoc-in db [:calc :result] {:status :loading})
|
||||
::fetch {:uri (str "/api/add?x=" (js/encodeURIComponent x)
|
||||
"&y=" (js/encodeURIComponent y))
|
||||
:on-response [::add-response x y]}})))
|
||||
|
||||
(defn- error-message
|
||||
"The API answers bad input with EDN like {:error \"...\" :details {:x [...]}}."
|
||||
[{:keys [status body]}]
|
||||
(let [{:keys [error details]} (try (reader/read-string body) (catch :default _ nil))]
|
||||
(cond
|
||||
error (str error (when (seq details) (str " (" (str/join ", " (map name (keys details))) ")")))
|
||||
status (str "HTTP " status)
|
||||
:else body)))
|
||||
|
||||
(rf/reg-event-db
|
||||
::add-response
|
||||
(fn [db [_ x y {:keys [ok? body] :as resp}]]
|
||||
(assoc-in db [:calc :result]
|
||||
(if ok?
|
||||
{:status :ok :x x :y y :sum (:result (reader/read-string body))}
|
||||
{:status :error :message (error-message resp)}))))
|
||||
@@ -0,0 +1,5 @@
|
||||
(ns tbd.ui.subs
|
||||
(:require [re-frame.core :as rf]))
|
||||
|
||||
(rf/reg-sub ::operand (fn [db [_ k]] (get-in db [:calc k])))
|
||||
(rf/reg-sub ::result (fn [db _] (get-in db [:calc :result])))
|
||||
@@ -0,0 +1,38 @@
|
||||
(ns tbd.ui.views
|
||||
(:require [re-frame.core :as rf]
|
||||
[tbd.ui.events :as events]
|
||||
[tbd.ui.subs :as subs]))
|
||||
|
||||
(defn- operand-input [k label]
|
||||
[:label label " "
|
||||
[:input {:type "number" :step "any" :required true :name (name k)
|
||||
:style {:width "6rem"}
|
||||
:value @(rf/subscribe [::subs/operand k])
|
||||
:on-change #(rf/dispatch [::events/set-operand k (.. % -target -value)])}]])
|
||||
|
||||
(defn- result-line [{:keys [status x y sum message]}]
|
||||
(case status
|
||||
:idle nil
|
||||
:loading [:p "Adding…"]
|
||||
:ok [:p {:aria-live "polite"} x " + " y " = " [:strong sum]]
|
||||
:error [:p {:role "alert" :style {:color "crimson"}} message]))
|
||||
|
||||
(defn calculator
|
||||
"Sends GET /api/add?x=..&y=.. on submit and shows the sum the server returns."
|
||||
[]
|
||||
(let [result @(rf/subscribe [::subs/result])]
|
||||
[:form {:on-submit (fn [e]
|
||||
(.preventDefault e)
|
||||
(rf/dispatch [::events/add]))}
|
||||
[:div {:style {:display "flex" :gap "1rem" :align-items "center"}}
|
||||
[operand-input :x "x"]
|
||||
"+"
|
||||
[operand-input :y "y"]
|
||||
[:button {:type "submit" :disabled (= :loading (:status result))} "Add"]]
|
||||
[result-line result]]))
|
||||
|
||||
(defn app []
|
||||
[:main {:style {:font-family "system-ui, sans-serif" :max-width "40rem" :margin "2rem auto"}}
|
||||
[:h1 "tbd"]
|
||||
[calculator]
|
||||
[:p [:a {:href "/api-docs"} "API docs"] " · " [:a {:href "/swagger.json"} "swagger.json"]]])
|
||||
Reference in New Issue
Block a user