pmme-ui: Refactor admin/sensors split into single core module

- Merged admin.cljs and sensors.cljs into core.cljs to consolidate UI logic
- Added WiFi network management interface with active/inactive status indicators
- Improved admin panel with async status checking and visual feedback
- Updated shadow-cljs config to use port 3000 and simplified mobile build
- Enhanced CSS with utility layer for badge-error class
This commit is contained in:
Joseph Ferano 2025-07-12 11:13:13 +07:00
parent 601ad9ecf7
commit 718fb49bf2
7 changed files with 125 additions and 81 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/*Claude-PMME*

View File

@ -5,7 +5,7 @@
:dependencies
[[no.cjohansen/replicant "2025.03.27"]]
:dev-http {3001 "public"}
:dev-http {3000 "public"}
:builds
{:dev {:target :browser
@ -23,8 +23,4 @@
:output-dir "../pmme-mobile/src/js"
:asset-path "/js"
:modules {:main {:init-fn pmme-ui.shell/main}}
:compiler-options {:optimizations :advanced}
:build-hooks [(shadow.resource/copy-resources
{:from "public"
:to "../pmme-mobile/src"
:exclude ["**/*.js"]})]}}}
:compiler-options {:optimizations :advanced}}}}

View File

@ -1,3 +1,7 @@
@import "tailwindcss";
@source "./**/*.{clj,cljs,cljc}";
@plugin "daisyui";
@layer utilities {
.badge-error { /* This forces Tailwind to include it */ }
}

View File

@ -1,25 +0,0 @@
(ns pmme-ui.admin
(:require [replicant.dom :as r]))
(defn render-admin-panel [state]
[:div.min-h-screen.bg-gradient-to-br.from-slate-50.to-slate-100.flex.items-center.justify-center.p-8
[:div.card.bg-white.shadow-2xl.border-0.max-w-2xl.w-full
[:div.card-body.p-8
[:h2.text-2xl.font-bold.text-slate-800.mb-6.text-center "ESP32 Admin Panel"]
[:div.space-y-4
[:div.form-control
[:label.label.w-32.items-center [:span.label-text "WiFi SSID"]]
[:input.input.input-bordered {:type "text" :placeholder "Enter WiFi network name"}]]
[:div.form-control
[:label.label.w-32 [:span.label-text "WiFi Password"]]
[:input.input.input-bordered {:type "password" :placeholder "Enter WiFi password"}]]
[:div.form-control
[:label.label.w-32 [:span.label-text "Device Name"]]
[:input.input.input-bordered {:type "text" :placeholder "Enter device name"}]]
[:div.flex.gap-2.mt-6
[:button.btn.btn-primary.flex-1 "Provision Device"]
[:button.btn.btn-outline.flex-1 "Reset Device"]]]]]])

View File

@ -1,7 +1,84 @@
(ns pmme-ui.core
(:require [replicant.dom :as r]
[pmme-ui.sensors :as sensors]
[pmme-ui.admin :as admin]))
(:require [replicant.dom :as r]))
(defn render-pm25-display [state]
[:div.min-h-screen.bg-gradient-to-br.from-slate-50.to-slate-100.flex.items-center.justify-center.p-8
[:div.card.bg-white.shadow-2xl.border-0.max-w-2xl.w-full
[:div.card-body.text-center.p-12
[:div.mb-6
[:h1.text-2xl.font-light.text-slate-600.mb-2 "PME"]
[:div.text-sm.text-slate-400.uppercase.tracking-wide.font-medium "PM 2.5 Level"]]
[:div.text-center.mb-8
[:div.relative.inline-block
[:span.text-8xl.md:text-9xl.font-black.text-slate-800.leading-none.tracking-tight
(:pm25 state "250")]
[:span.absolute.top-0.-right-20.text-2xl.text-slate-400.font-light "μg/m³"]]]
[:div.mt-4
(let [pm25-val (js/parseInt (:pm25 state "0"))
status (cond
(<= pm25-val 12) {:text "Good" :color "text-success" :bg "bg-success/10"}
(<= pm25-val 35) {:text "Moderate" :color "text-warning" :bg "bg-warning/10"}
:else {:text "Unhealthy" :color "text-error" :bg "bg-error/10"})]
[:div.badge.badge-lg.px-6.py-3.text-base.font-semibold
{:class [(:bg status) (:color status)]}
(:text status)])]
]]])
(defn render-wifi-table [wifi-networks]
[:div.min-h-screen.bg-gradient-to-br.from-slate-50.to-slate-100.flex.items-center.justify-center.p-8
[:div.card.bg-white.shadow-2xl.border-0.max-w-2xl.w-full
[:div.card-body.p-8
[:h2.text-2xl.font-bold.text-slate-800.mb-6.text-center "Saved WiFi Networks"]
[:div.overflow-x-auto
[:table.table.w-full
[:thead
[:tr
[:th.w-full "SSID"]
[:th "Actions"]]]
[:tbody
(for [wifi wifi-networks]
[:tr {:key (:ssid wifi)}
[:td.font-bold (:ssid wifi)]
[:td
[:div.flex.gap-2
(if (:active wifi)
[:button.btn.btn-success.btn-sm.w-32.text-black.font-normal "Active"]
[:button.btn.btn-info.btn-sm.w-32.text-black.font-normal "Make Active"])
[:button.btn.btn-error.btn-sm.flex-1.font-normal "Delete"]]]])]]]
[:btn.btn.btn-accent.mt-2 {:on {:click ::add-wifi}} "+ Add Wifi"]]]])
(defn render-admin-panel [state]
(render-wifi-table [{:ssid "The Brick" :active true} {:ssid "Home" :active false}])
)
;; [:div.min-h-screen.bg-gradient-to-br.from-slate-50.to-slate-100.flex.items-center.justify-center.p-8
;; [:div.card.bg-white.shadow-2xl.border-0.max-w-2xl.w-full
;; [:div.card-body.p-8
;; [:h2.text-2xl.font-bold.text-slate-800.mb-6.text-center "ESP32 Admin Panel"]
;; [:div.space-y-4
;; [:div.form-control
;; [:label.label.w-32.items-center [:span.label-text "WiFi SSID"]]
;; [:input.input.input-bordered {:type "text" :placeholder "Enter WiFi network name"}]]
;; [:div.form-control
;; [:label.label.w-32 [:span.label-text "WiFi Password"]]
;; [:input.input.input-bordered {:type "password" :placeholder "Enter WiFi password"}]]
;; [:div.form-control
;; [:label.label.w-32 [:span.label-text "Device Name"]]
;; [:input.input.input-bordered {:type "text" :placeholder "Enter device name"}]]
;; [:div.flex.gap-2.mt-6
;; [:button.btn.btn-primary.flex-1 "Provision Device"]
;; [:button.btn.btn-outline.flex-1 "Reset Device"]]]]]]
(defn render-current-content [state]
(case (:current-tab state)
:admin (render-admin-panel state)
:readings (render-pm25-display state)))
(defn render-tabs [state]
(when (:show-tabs state)
@ -14,17 +91,20 @@
[:button.tab
{:class (when (= (:current-tab state) :admin) "tab-active")
:on {:click [::switch-tab :admin]}}
"Admin Panel"]]]))
(defn render-current-content [state]
(case (:current-tab state)
:admin (admin/render-admin-panel state)
:readings (sensors/render-pm25-display state)))
"Admin Panel"
(case (:admin-status state)
::available
[:div.ml-2 {:class ["inline-grid" "*:[grid-area:1/1]"]}
[:div.status.status-success.animate-ping]
[:div.status.status-success]]
::unavailable
[:div.status.status-error.ml-2]
::unknown
[:div.status.status-neutral.ml-2])]]]))
(defn render-ui [state]
(r/render
js/document.body
[:div
(render-tabs state)
(render-current-content state)]))
[:div
(render-tabs state)
(render-current-content state)]))

View File

@ -1,27 +0,0 @@
(ns pmme-ui.sensors
(:require [replicant.dom :as r]))
(defn render-pm25-display [state]
[:div.min-h-screen.bg-gradient-to-br.from-slate-50.to-slate-100.flex.items-center.justify-center.p-8
[:div.card.bg-white.shadow-2xl.border-0.max-w-2xl.w-full
[:div.card-body.text-center.p-12
[:div.mb-6
[:h1.text-2xl.font-light.text-slate-600.mb-2 "PME"]
[:div.text-sm.text-slate-400.uppercase.tracking-wide.font-medium "PM 2.5 Level"]]
[:div.text-center.mb-8
[:div.relative.inline-block
[:span.text-8xl.md:text-9xl.font-black.text-slate-800.leading-none.tracking-tight
(:pm25 state "250")]
[:span.absolute.top-0.-right-20.text-2xl.text-slate-400.font-light "μg/m³"]]]
[:div.mt-4
(let [pm25-val (js/parseInt (:pm25 state "0"))
status (cond
(<= pm25-val 12) {:text "Good" :color "text-success" :bg "bg-success/10"}
(<= pm25-val 35) {:text "Moderate" :color "text-warning" :bg "bg-warning/10"}
:else {:text "Unhealthy" :color "text-error" :bg "bg-error/10"})]
[:div.badge.badge-lg.px-6.py-3.text-base.font-semibold
{:class [(:bg status) (:color status)]}
(:text status)])]
]]])

View File

@ -1,12 +1,21 @@
(ns pmme-ui.shell
(:require [replicant.dom :as r]
[pmme-ui.core :as core]))
[pmme-ui.core :as core]
[cljs.core.async :as async :refer [go <!]]
[cljs.core.async.interop :refer [<p!]]))
(defonce store (atom {:number 0
:current-tab :readings}))
:current-tab :readings
:show-tabs true
:admin-status ::core/unknown}))
(defn should-show-tabs? []
true)
(defn ping-admin-endpoint []
(go
(try
(<p! (js/fetch "http://localhost:8080"))
true
(catch js/Error e
false))))
(defn init [store]
(add-watch store ::render (fn [_ _ _ new-state] (core/render-ui new-state)))
@ -14,12 +23,18 @@
(fn [_ event-data]
(let [[action & args] event-data]
(case action
::core/test-me
(swap! store update :number inc)
::core/switch-tab
(swap! store assoc :current-tab (first args))))))
(swap! store assoc :show-tabs (should-show-tabs?))
(let [target-tab (first args)]
(if (= target-tab :admin)
(go
;; (let [admin-available? (<! (ping-admin-endpoint))]
(let [admin-available? true]
(if admin-available?
(do
(swap! store assoc :current-tab :admin)
(swap! store assoc :admin-status ::core/available))
(swap! store assoc :admin-status ::core/unavailable))))
(swap! store assoc :current-tab target-tab)))))))
(swap! store assoc ::loaded-at (.getTime (js/Date.))))
(defn main []
@ -28,4 +43,4 @@
(defn ^:dev/after-load reload []
(init store)
(println "Reloaded!"))
(println "Reloaded"))