This commit is contained in:
Adam Jeniski 2025-10-16 10:46:45 -09:00
parent 905640b0e1
commit c190f20b14
7 changed files with 149 additions and 134 deletions

View File

@ -1,151 +1,41 @@
(ns ajet.todo.core (ns ajet.todo.core
(:require (:require
[ajet.todo.db :as db] [ajet.todo.db :as db]
[clojure.data.json :as json] [ajet.todo.handler :as handler]
[clojure.pprint :as pprint] [ajet.todo.util :refer [fn-sse]]
[ajet.todo.view :as view]
[compojure.core :refer [context defroutes DELETE GET let-routes PATCH POST]] [compojure.core :refer [context defroutes DELETE GET let-routes PATCH POST]]
[compojure.route :as route] [compojure.route :as route]
[hiccup2.core :as h] [hiccup2.core :as h]
[ring.middleware.json :refer [wrap-json-body]] [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]]))
;; utils
(defmacro fn-sse [[req sse :as _bindings]
& body]
`(fn [~req respond# _#]
(respond#
(->sse-response
~req
{on-open (fn [~sse]
(d*/with-open-sse ~sse
~@body))}))))
(defmacro defn-sse [var-name
[req sse :as _bindings]
& body]
`(defn ~var-name [~req respond# _#]
(respond#
(->sse-response
~req
{on-open (fn [~sse]
(d*/with-open-sse ~sse
~@body))}))))
(comment
(do
(defonce printer (bound-fn* pprint/pprint))
(add-tap printer))
(remove-tap printer))
;; app
(defn render-page [content]
(str
(h/html
(h/raw "<!DOCTYPE html>")
[: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 content]])))
(defn todos-fragment []
(let [todos (db/get-all-todos)] ;; todo: make functional. sloppy sloppy
(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 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"]]))
(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)]))
(defn refresh-todos! [sse]
(d*/patch-elements! sse
(str (todos-fragment))
#:d*.elements{:selector "#todos"
:patch-mode "replace"}))
(defn-sse create-todo-handler [req sse]
(let [title (-> req :body :title)]
(when title
(db/add-todo! title))
(d*/with-open-sse sse
(refresh-todos! sse)
(d*/patch-signals! sse #json{:title ""}))))
(defroutes approutes (defroutes approutes
(GET "/" [] (GET "/" []
(fn [_ respond _] (fn [_ respond _]
(-> (app-fragment) (-> (view/app-fragment)
render-page view/render-page
respond))) respond)))
(-> (context "/sse/todo" [] (-> (context "/sse/todo" []
(GET "/" [] (GET "/" []
(fn-sse [_req sse] (fn-sse [_req sse]
(refresh-todos! sse))) (handler/refresh-todos! sse)))
(POST "/create-todo" [] create-todo-handler) (POST "/create-todo" [] handler/create-todo-handler)
(context ["/:id", :id #"[0-9]+"] [id] (context ["/:id", :id #"[0-9]+"] [id]
(let-routes [id (read-string id)] (let-routes [id (read-string id)]
(PATCH "/set" [] (PATCH "/set" []
(fn-sse [_req sse] (fn-sse [_req sse]
(db/set-todo-done! id true) (db/set-todo-done! id true)
(refresh-todos! sse))) (handler/refresh-todos! sse)))
(PATCH "/unset" [] (PATCH "/unset" []
(fn-sse [_req sse] (fn-sse [_req sse]
(db/set-todo-done! id false) (db/set-todo-done! id false)
(refresh-todos! sse))) (handler/refresh-todos! sse)))
(DELETE "/" [] (DELETE "/" []
(fn-sse [_req sse] (fn-sse [_req sse]
(db/delete-todo! id) (db/delete-todo! id)
(refresh-todos! sse)))))) (handler/refresh-todos! sse))))))
(wrap-json-body {:keywords? true})) (wrap-json-body {:keywords? true}))
(route/not-found (str (h/html [:h2 "Not Found"])))) (route/not-found (str (h/html [:h2 "Not Found"]))))

View File

@ -58,16 +58,8 @@
(sort-by first) (sort-by first)
(map normalize-todo)))) (map normalize-todo))))
(defn get-todo [id] (defn get-todo [db id]
(let [db (d/db conn)] (d/pull db '[*] id))
(some->> (d/q '[:find ?e ?title ?done
:in $ ?e
:where [?e :todo/title ?title]
[?e :todo/done? ?done]]
db
id)
first
normalize-todo)))
(defn get-todo-by-title [title] (defn get-todo-by-title [title]
(let [db (d/db conn)] (let [db (d/db conn)]

20
src/ajet/todo/handler.clj Normal file
View File

@ -0,0 +1,20 @@
(ns ajet.todo.handler
(:require
[ajet.todo.db :as db]
[ajet.todo.util :refer [defn-sse]]
[ajet.todo.view :as view]
[starfederation.datastar.clojure.api :as d*]))
(defn refresh-todos! [sse]
(d*/patch-elements! sse
(str (view/todos-fragment))
#:d*.elements{:selector "#todos"
:patch-mode "replace"}))
(defn-sse create-todo-handler [req sse]
(let [title (-> req :body :title)]
(when title
(db/add-todo! title))
(d*/with-open-sse sse
(refresh-todos! sse)
(d*/patch-signals! sse #json{:title ""}))))

View File

@ -3,7 +3,7 @@
[ajet.todo.core :as core] [ajet.todo.core :as core]
[ring.adapter.jetty :refer [run-jetty]])) [ring.adapter.jetty :refer [run-jetty]]))
(def port 1738) (def port 80)
(defn make-server [opts] (defn make-server [opts]
(run-jetty #'core/app (merge {:join? false, (run-jetty #'core/app (merge {:join? false,

27
src/ajet/todo/util.clj Normal file
View File

@ -0,0 +1,27 @@
(ns ajet.todo.util
(:require
[starfederation.datastar.clojure.adapter.common :refer [on-open]]
[starfederation.datastar.clojure.adapter.ring :refer [->sse-response]]
[starfederation.datastar.clojure.api :as d*]))
;; utils
(defmacro fn-sse [[req sse :as _bindings]
& body]
`(fn [~req respond# _#]
(respond#
(->sse-response
~req
{on-open (fn [~sse]
(d*/with-open-sse ~sse
~@body))}))))
(defmacro defn-sse [var-name
[req sse :as _bindings]
& body]
`(defn ~var-name [~req respond# _#]
(respond#
(->sse-response
~req
{on-open (fn [~sse]
(d*/with-open-sse ~sse
~@body))}))))

76
src/ajet/todo/view.clj Normal file
View File

@ -0,0 +1,76 @@
(ns ajet.todo.view
(:require
[ajet.todo.db :as db]
[clojure.data.json]
[hiccup2.core :as h]
[hiccup.util :as hu]
[starfederation.datastar.clojure.expressions :refer [->expr]]))
(defn render-page [content]
(str
(h/html
(h/raw "<!DOCTYPE html>")
[:html
[:head
[:script {:src "https://cdn.jsdelivr.net/gh/starfederation/datastar@1.0.0-RC.5/bundles/datastar.js"
:type "module"
:defer true}]
[:script {:src "https://cdn.jsdelivr.net/npm/eruda"
:id "erudaLoaderScript"
:type "module"
:defer true}]
(hu/raw-string "<script>document.getElementById('erudaLoaderScript').addEventListener('load', function () { eruda.init () });</script/>")
[: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 content]])))
(defn todos-fragment []
(let [todos (db/get-all-todos)] ;; todo: make functional. sloppy sloppy
(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 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"]]))
(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)]))

View File

@ -1,11 +1,21 @@
(ns user (ns user
(:require [ajet.todo.server :as server])) (:require
[ajet.todo.server :as server]
[clojure.pprint :as pprint]))
(defonce server (server/make-server {})) (defonce server (server/make-server {}))
(comment (comment
;; server controls
server server
(. server stop) (. server stop)
(. server start)) (. server start)
;; tap debugging
(do
(defonce printer (bound-fn* pprint/pprint))
(add-tap printer))
(remove-tap printer))