From eba0dbe7c2b2857535211160773aa950880ff412 Mon Sep 17 00:00:00 2001 From: ajet Date: Wed, 17 Sep 2025 11:46:54 -0900 Subject: [PATCH] rewrite in d* --- deps.edn | 4 +- src/ajet/todo/core.clj | 236 +++++++++++++++++++++------------------ src/ajet/todo/server.clj | 5 +- src/data_readers.clj | 1 + 4 files changed, 134 insertions(+), 112 deletions(-) create mode 100644 src/data_readers.clj diff --git a/deps.edn b/deps.edn index 5109ecc..976ebe7 100644 --- a/deps.edn +++ b/deps.edn @@ -11,7 +11,9 @@ ring/ring-json {:mvn/version "0.5.1"} ring/ring-jetty-adapter {:mvn/version "1.14.2"} dev.data-star.clojure/sdk {:mvn/version "1.0.0-RC3"} - dev.data-star.clojure/ring {:mvn/version "1.0.0-RC3"}} + dev.data-star.clojure/ring {:mvn/version "1.0.0-RC3"} + datastar/expressions {:git/url "https://github.com/outskirtslabs/datastar-expressions/" + :git/sha "53efc7093be1ba33b331b4a27884be8925d8bdce"}} :paths [:clj-paths :resource-paths] :aliases {:repl {:extra-paths ["src/dev"] diff --git a/src/ajet/todo/core.clj b/src/ajet/todo/core.clj index 24d101a..b38ad20 100644 --- a/src/ajet/todo/core.clj +++ b/src/ajet/todo/core.clj @@ -6,12 +6,11 @@ [compojure.core :refer [context defroutes DELETE GET let-routes PATCH POST]] [compojure.route :as route] [hiccup2.core :as h] - [ring.middleware.content-type :refer [wrap-content-type]] - [ring.middleware.json :as rmjson] - [ring.middleware.not-modified :refer [wrap-not-modified]] - [ring.middleware.params :refer [wrap-params]] - [ring.middleware.resource :refer [wrap-resource]] - [ring.util.response :refer [response]])) + [ring.middleware.json :refer [wrap-json-body]] + [starfederation.datastar.clojure.adapter.common :refer [on-open]] + [starfederation.datastar.clojure.adapter.ring :refer [->sse-response]] + [starfederation.datastar.clojure.api :as d*] + [starfederation.datastar.clojure.expressions :refer [->expr]])) (comment (do @@ -20,120 +19,137 @@ (remove-tap printer)) -(defn get-todos [_] - (let [res (db/get-all-todos)] - (response res))) - -(defn get-todo [id _] - (let [res (db/get-todo id)] - (response res))) - -(defn add-todo [req] - (db/add-todo! (-> req :body :title)) - {:status 201}) - -(defn update-todo [id {{:keys [done]} :body}] - (db/set-todo-done! id done)) - -(defn delete-todo [id] - (db/delete-todo! id) - {:status 204}) - (defn page-template [view] - (-> [:html - [:head - [:script {:src "https://cdn.jsdelivr.net/npm/htmx.org@2.0.7/dist/htmx.min.js"}]] - [:body view]] - h/html - str)) + (str + (h/html + (h/raw "") + [:html + [:head + [:script {:src "https://cdn.jsdelivr.net/gh/starfederation/datastar@1.0.0-RC.5/bundles/datastar.js" + :type "module" + :defer true}] + [:link {:rel "icon" + :type "imaage/x-icon" + :href "https://data-star.dev/cdn-cgi/image/format=auto,width=24/static/images/rocket-48x48-4c739bfaffe86a6ffcc3a6d77e3c5547730f03d74c11aa460209596d1811f7a3.png"}]] + [:body view]]))) -(def hx-app {:hx-swap "outerHTML" - :hx-target "#app"}) +(defn todos-fragment [] + (let [todos (db/get-all-todos)] + (h/html + [:div {:id "todos"} + [:div {:data-computed-show-toggle-display "($showComplete ? 'hide' : 'show') + ' done'"}] + [:ul + (for [{id :id + done :todo/done? + title :todo/title} todos] + [:li {:data-show (->expr (or (not ~done) + $showComplete))} + [:span {:style (str "text-decoration: " (if done "line-through" "none"))} + title] + [:input {:type "checkbox" + :checked done + :data-on-click (str "@patch('/sse/todo/" id "/" (when done "un") "set')") + :style "margin-right: 0.25rem;"}] + (when done + [:button {:data-on-click (->expr (@delete ~(str "/sse/todo/" id)))} + "X"])])] + [:div + [:p "settings"] + [:button {:data-on-click "$showComplete = !$showComplete" + :data-text "$showToggleDisplay" + :style "margin-right: 0.25rem;"}] + [:button {:data-on-click "$debugMode = !$debugMode"} + "toggle debug"]]]))) -(defn render-app [{:keys [show-complete] - :or {show-complete true} - :as opts}] - (-> (let [todos (db/get-all-todos) - complete-toggle (if show-complete "hide" "show")] - [:div {:id "app"} - [:ul - (for [{id :id - done :todo/done? - title :todo/title} todos - :when (or show-complete (not done))] - [:li - [:span {:style (str "text-decoration: " (if done "line-through" "none"))} - title] - [:input (merge hx-app - {:type "checkbox" - :checked done - :hx-patch (format "/htmx/todo/%s/%s?opts=%s" - id - (if done "unset" "set") - (json/write-str opts))})]])] - [:div - [:p "settings"] - [:button (merge hx-app - {:hx-get (format "/htmx/todo/?opts=%s" - (-> opts - (update :show-complete not) - json/write-str))}) - complete-toggle " done"]]]) - h/html)) +(defn create-todo-fragment [] + (h/html + [:div {:id "create-box" + :style "margin-top: 2rem;"} + [:label "Make a todo: "] + [:input {:name "title" + :data-bind "title" + :style "margin-right: 0.25rem;"}] + [:button {:data-on-click (->expr (@post "/sse/todo/create-todo"))} + "submit"]])) -;; im gonna regert this -(defn DRY [req] - (-> req - :params - (get "opts" "{}") - json/read-json - render-app - str)) +(defn app-fragment [] + (h/html + [:div {:data-signals #json{:showComplete true + :debugMode false + :title ""}}] + [:pre {:data-json-signals "true" + :data-show "$debugMode"}] + [:div {:id "app"} + (todos-fragment) + (create-todo-fragment)])) -(defroutes api-routes +(defn refresh-todos! [sse] + (d*/patch-elements! sse + (str (todos-fragment)) + #:d*.elements{:selector "#todos" + :patch-mode "replace"})) + +(defroutes approutes (GET "/" [] - (wrap-params - #(-> % :params - (get "opts" "{}") - json/read-json - render-app - page-template))) - (-> (context "/api/v1" [] - (context "/todo" [] - (GET "/" [] get-todos) - (POST "/" [] add-todo) - (context ["/:id", :id #"[0-9]+"] [id] - (let-routes [id (read-string id)] - (GET "/" [] (partial get-todo id)) - (PATCH "/" [] (partial update-todo id)) - (DELETE "/" [] (delete-todo id))))) + (fn [_ respond _] + (-> (app-fragment) + page-template + respond))) - (GET "/example/math/:x/:y" [x y] - (->> (+ (parse-long x) - (parse-long y)) - (hash-map :result) - response))) - (rmjson/wrap-json-body {:keywords? true}) - (rmjson/wrap-json-response)) + (wrap-json-body + (context "/sse/todo" [] + (GET "/" [] + (fn [req respond _] + (respond + (->sse-response req + {on-open (fn [sse] + (d*/with-open-sse sse + (refresh-todos! sse)))})))) + (POST "/create-todo" [] + (fn [req respond _] + (let [title (-> req :body :title)] + (tap> title) + (when title + (db/add-todo! title)) + (respond + (->sse-response req + {on-open (fn [sse] + (d*/with-open-sse sse + (refresh-todos! sse) + (d*/patch-signals! sse + #json{:title ""})))}))))) - (wrap-params - (context "/htmx/todo" [] - (GET "/" [] DRY) - (POST "/" [] add-todo) (context ["/:id", :id #"[0-9]+"] [id] (let-routes [id (read-string id)] - (PATCH "/set" [] (do (db/set-todo-done! id true) - DRY)) - (PATCH "/unset" [] (do (db/set-todo-done! id false) - DRY)) - (DELETE "/" [] (do (delete-todo id) - DRY)))))) - + (PATCH "/set" [] + (fn [req respond _] + (db/set-todo-done! id true) + (respond + (->sse-response req + {on-open (fn [sse] + (d*/with-open-sse sse + (refresh-todos! sse)))})))) + (PATCH "/unset" [] + (fn [req respond _] + (db/set-todo-done! id false) + (respond + (->sse-response + req + {on-open (fn [sse] + (d*/with-open-sse sse + (refresh-todos! sse)))})))) + (DELETE "/" [] + (db/delete-todo! id) + (fn [req respond _] + (respond + (->sse-response + req + {on-open + (fn [sse] + (d*/with-open-sse sse + (refresh-todos! sse)))}))))))) + {:keywords? true}) (route/not-found (str (h/html [:h2 "Not Found"])))) -(def app - (-> #'api-routes - (wrap-resource "public") - wrap-content-type - wrap-not-modified)) ;; files from resources/public are served +(def app #'approutes) diff --git a/src/ajet/todo/server.clj b/src/ajet/todo/server.clj index 475a800..17e3115 100644 --- a/src/ajet/todo/server.clj +++ b/src/ajet/todo/server.clj @@ -6,7 +6,10 @@ (def port 1738) (defn make-server [opts] - (run-jetty #'core/app (merge {:join? false, :port port} opts))) + (run-jetty #'core/app (merge {:join? false, + :port port, + :async? true} + opts))) (defn -main [& _args] (make-server {})) diff --git a/src/data_readers.clj b/src/data_readers.clj new file mode 100644 index 0000000..7a7489f --- /dev/null +++ b/src/data_readers.clj @@ -0,0 +1 @@ +{json clojure.data.json/write-str}