1
submitted 4 months ago by [email protected] to c/[email protected]

I had to make a little UI to display some stats about an app at work, and decided to try using Babashka with HTMX for it. Turned out to be a really good experience.

Babashka has pretty much everything you need for a basic web app baked in, and HTMX lets you do dynamic loading on the page without having to bother with a Js frontend.

Best part is that bb can start nREPL with bb --nrepl-server and then you can connect an editor like Calva to it and develop the script interactively.

Definitely recommend checking it out if you need to make a simple web UI.

#!/usr/bin/env bb
(require
 '[clojure.string :as str]
 '[org.httpkit.server :as srv]
 '[hiccup2.core :as hp]
 '[cheshire.core :as json]
 '[babashka.pods :as pods]
 '[clojure.java.io :as io]
 '[clojure.edn :as edn])
(import '[java.net URLDecoder])

(pods/load-pod 'org.babashka/postgresql "0.1.0")
(require '[pod.babashka.postgresql :as pg])

(defonce server (atom nil))
(defonce conn (atom nil))

(def favicon "data:image/x-icon;base64,AAABAAEAEBAAAAAAAABoBQAAFgAAACgAAAAQAAAAIAAAAAEACAAAAAAAAAEAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP//AAD8fwAA/H8AAPxjAAD/4wAA/+MAAMY/AADGPwAAxjEAAP/xAAD/8QAA4x8AAOMfAADjHwAA//8AAP//AAA=")

(defn list-accounts [{:keys [from to]}]
  (pg/execute! @conn
               ["select account_id, created_at
                 from accounts
                 where created_at between to_date(?, 'yyyy-mm-dd') and to_date(?, 'yyyy-mm-dd')"
                from to]))

(defn list-all-accounts [_req]
  (json/encode {:accounts (pg/execute! @conn ["select account_id, created_at from accounts"])}))

(defn parse-body [{:keys [body]}]
  (reduce
   (fn [params param]
     (let [[k v] (str/split param #"=")]
       (assoc params (keyword k) (URLDecoder/decode v))))
   {}
   (-> body slurp (str/split #"&"))))

(defn render [html]
  (str (hp/html html)))

(defn render-accounts [request]
  (let [params (parse-body request)
        accounts (list-accounts params)]
    [:table.table {:id "accounts"}
     [:thead
      [:tr [:th "account id"] [:th "created at"]]]
     [:tbody
      (for [{:accounts/keys [account_id created_at]} accounts]
        [:tr [:td account_id] [:td (str created_at)]])]]))

(defn date-str [date]
  (let [fmt (java.text.SimpleDateFormat. "yyyy-MM-dd")]
    (.format fmt date)))

(defn account-stats []
  [:section.hero
   [:div.hero-body
    [:div.container
     [:div.columns
      [:div.column
       [:form.box
        {:hx-post "/accounts-in-range"
         :hx-target "#accounts"
         :hx-swap "outerHTML"}
        [:h1.title "Accounts"]
        [:div.field
         [:label.label {:for "from"} [:b "from "]]
         [:input.control {:type "date" :id "from" :name "from" :value (date-str (java.util.Date.))}]]

        [:div.field
         [:label.label {:for "to"} [:b " to "]]
         [:input.control {:type "date" :id "to" :name "to" :value (date-str (java.util.Date.))}]]

        [:button.button {:type "submit"} "list accounts"]]
       [:div.box [:table.table {:id "accounts"}]]]]]]])

(defn home-page [_req]
  (render
   [:html
    [:head
     [:link {:href favicon :rel "icon" :type "image/x-icon"}]
     [:meta {:charset "UTF-8"}]
     [:title "Account Stats"]
     [:link {:href "https://cdn.jsdelivr.net/npm/[email protected]/css/bulma.min.css" :rel "stylesheet"}]
     [:link {:href "https://unpkg.com/[email protected]/index.css" :rel "stylesheet"}]
     [:script {:src "https://unpkg.com/[email protected]/dist/htmx.min.js" :defer true}]
     [:script {:src "https://unpkg.com/[email protected]/dist/_hyperscript.min.js" :defer true}]]
    [:body
     (account-stats)]]))

(defn handler [{:keys [uri request-method] :as req}]
  (condp = [request-method uri]
    [:get "/"]
    {:body (home-page req)
     :headers {"Content-Type" "text/html charset=utf-8"}
     :status 200}

    [:get "/accounts.json"]
    {:body (list-all-accounts req)
     :headers {"Content-Type" "application/json; charset=utf-8"}
     :status 200}

    [:post "/accounts-in-range"]
    {:body (render (render-accounts req))
     :status 200}

    {:body (str "page " uri " not found")
     :status 404}))

(defn read-config []
  (if (.exists (io/file "config.edn"))
    (edn/read-string (slurp "config.edn"))
    {:port 3001
     :db {:dbtype   "postgresql"
          :host     "localhost"
          :dbname   "postgres"
          :user     "postgres"
          :password "postgres"
          :port     5432}}))

(defn run []
  (let [{:keys [port db]} (read-config)]
    (reset! conn db)
    (when-let [server @server]
      (server))
    (reset! server
            (srv/run-server #(handler %) {:port port}))
    (println "started on port:" port)))

;; ensures process doesn't exit when running from command line
(when (= "start" (first *command-line-args*))
  (run)
  @(promise))

(comment
  ;; restart server 
  (do
    (when-let [instance @server] (instance))
    (reset! server nil)
    (run)))
no comments (yet)
sorted by: hot top controversial new old
there doesn't seem to be anything here
this post was submitted on 08 May 2024
1 points (100.0% liked)

Clojure programming language discussion

438 readers
1 users here now

Clojure is a Lisp that targets JVM and JS runtimes

Finding information about Clojure

API Reference

Clojure Guides

Practice Problems

Interactive Problems

Clojure Videos

The Clojure Community

Clojure Books

Tools & Libraries

Clojure Editors

Web Platforms

founded 4 years ago
MODERATORS