From 0c61609774d1418944979a24dfb54324170c39ec Mon Sep 17 00:00:00 2001 From: ajet Date: Sun, 14 Sep 2025 17:54:58 -0900 Subject: [PATCH] init commit --- .gitignore | 5 ++ deps.edn | 22 +++++++ restart.sh | 3 + src/ajet/todo/core.clj | 139 +++++++++++++++++++++++++++++++++++++++ src/ajet/todo/db.clj | 103 +++++++++++++++++++++++++++++ src/ajet/todo/server.clj | 13 ++++ src/dev/user.clj | 11 ++++ start.sh | 3 + 8 files changed, 299 insertions(+) create mode 100644 .gitignore create mode 100644 deps.edn create mode 100755 restart.sh create mode 100644 src/ajet/todo/core.clj create mode 100644 src/ajet/todo/db.clj create mode 100644 src/ajet/todo/server.clj create mode 100644 src/dev/user.clj create mode 100755 start.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..07e77b5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.clj-kondo +.nrepl-port +.lsp +.cpcache + diff --git a/deps.edn b/deps.edn new file mode 100644 index 0000000..e78f182 --- /dev/null +++ b/deps.edn @@ -0,0 +1,22 @@ +{:deps + {compojure/compojure {:mvn/version "1.7.1"} + com.datomic/local {:mvn/version "1.0.291"} + org.clojure/clojure {:mvn/version "1.12.0"} + org.clojure/data.json {:mvn/version "2.5.1"} + org.slf4j/slf4j-api {:mvn/version "2.0.17"} + org.slf4j/slf4j-simple {:mvn/version "2.0.17"} + hiccup/hiccup {:mvn/version "2.0.0-RC1"} + ring-logger/ring-logger {:mvn/version "1.1.1"} + ring/ring-core {:mvn/version "1.14.2"} + ring/ring-json {:mvn/version "0.5.1"} + ring/ring-jetty-adapter {:mvn/version "1.14.2"}} + :paths [:clj-paths :resource-paths] + :aliases + {:repl {:extra-paths ["src/dev"] + :extra-deps {org.clojure/clojure {:mvn/version "1.12.1"} + nrepl/nrepl {:mvn/version "1.3.0"} + cider/cider-nrepl {:mvn/version "0.50.2"}} + :main-opts ["-m" "nrepl.cmdline", + "--middleware" "[cider.nrepl/cider-middleware]"]} + :clj-paths ["src"] + :resource-paths ["resources"]}} diff --git a/restart.sh b/restart.sh new file mode 100755 index 0000000..215ad2a --- /dev/null +++ b/restart.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +systemctl restart tbd diff --git a/src/ajet/todo/core.clj b/src/ajet/todo/core.clj new file mode 100644 index 0000000..24d101a --- /dev/null +++ b/src/ajet/todo/core.clj @@ -0,0 +1,139 @@ +(ns ajet.todo.core + (:require + [ajet.todo.db :as db] + [clojure.data.json :as json] + [clojure.pprint :as pprint] + [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]])) + +(comment + (do + (defonce printer (bound-fn* pprint/pprint)) + (add-tap printer)) + + (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)) + +(def hx-app {:hx-swap "outerHTML" + :hx-target "#app"}) + +(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)) + +;; im gonna regert this +(defn DRY [req] + (-> req + :params + (get "opts" "{}") + json/read-json + render-app + str)) + +(defroutes api-routes + (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))))) + + (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-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)))))) + + (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 + diff --git a/src/ajet/todo/db.clj b/src/ajet/todo/db.clj new file mode 100644 index 0000000..d9a6e33 --- /dev/null +++ b/src/ajet/todo/db.clj @@ -0,0 +1,103 @@ +(ns ajet.todo.db + (:require [datomic.client.api :as d])) + +(def client (d/client {:server-type :datomic-local + :system "dev"})) + +(d/create-database client {:db-name "todos"}) +(def conn (d/connect client {:db-name "todos"})) + +(comment + ;; to reset local dev db + (try + (d/delete-database client {:db-name "todos"}) + (catch Exception _)) + + (do + (def todo-schema [{:db/ident :todo/title + :db/valueType :db.type/string + :db/cardinality :db.cardinality/one + :db/unique :db.unique/identity + :db/doc "The title of the todo"} + + {:db/ident :todo/done? + :db/valueType :db.type/boolean + :db/cardinality :db.cardinality/one + :db/doc "The completedness of the todo"}]) + (d/transact conn {:tx-data todo-schema}) + (def first-todos [{:todo/title "set up git repo" + :todo/done? true} + {:todo/title "teach jon lisp" + :todo/done? false} + {:todo/title "finish the todo app" + :todo/done? false} + {:todo/title "profit?" + :todo/done? false}]) + (d/transact conn {:tx-data first-todos}))) + +(defn normalize-todo [[id title done]] + {:id id + :todo/title title + :todo/done? done}) + +(defn get-unfinished-todos [] + (let [db (d/db conn)] + (->> db + (d/q '[:find ?e ?title + :where [?e :todo/title ?title] + [?e :todo/done? false]]) + (map normalize-todo) + (map #(assoc % :todo?/done false))))) + +(defn get-all-todos [] + (let [db (d/db conn)] + (->> db + (d/q '[:find ?e ?title ?done + :where [?e :todo/title ?title] + [?e :todo/done? ?done]]) + (sort-by first) + (map normalize-todo)))) + +(defn get-todo [id] + (let [db (d/db conn)] + (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] + (let [db (d/db conn)] + (some->> (d/q '[:find ?e ?title ?done + :in $ ?title + :where [?e :todo/title ?title] + [?e :todo/done? ?done]] + db + title) + first + normalize-todo))) + +(defn add-todo! [title] + (d/transact conn {:tx-data [{:todo/title title + :todo/done? false}]})) + +(defn set-todo-done! [eid done] + (d/transact conn {:tx-data [{:db/id eid + :todo/done? done}]})) + +(defn delete-todo! [id] + (d/transact conn {:tx-data [[:db/retractEntity id]]})) + +(comment + (get-all-todos) + (get-unfinished-todos) + + (add-todo! "foo") + (def foo-id (-> (get-todo-by-title "foo") + :id)) + (set-todo-done! foo-id true) + (delete-todo! foo-id)) + diff --git a/src/ajet/todo/server.clj b/src/ajet/todo/server.clj new file mode 100644 index 0000000..475a800 --- /dev/null +++ b/src/ajet/todo/server.clj @@ -0,0 +1,13 @@ +(ns ajet.todo.server + (:require + [ajet.todo.core :as core] + [ring.adapter.jetty :refer [run-jetty]])) + +(def port 1738) + +(defn make-server [opts] + (run-jetty #'core/app (merge {:join? false, :port port} opts))) + +(defn -main [& _args] + (make-server {})) + diff --git a/src/dev/user.clj b/src/dev/user.clj new file mode 100644 index 0000000..c87e0d2 --- /dev/null +++ b/src/dev/user.clj @@ -0,0 +1,11 @@ +(ns user + (:require [ajet.todo.server :as server])) + +(defonce server (server/make-server {})) + +(comment + server + (. server stop) + (. server start)) + + diff --git a/start.sh b/start.sh new file mode 100755 index 0000000..ab101ad --- /dev/null +++ b/start.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env sh +cd ~/repos/tbd +clj -M -m ajet.todo.server