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:
parent
601ad9ecf7
commit
718fb49bf2
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/*Claude-PMME*
|
@ -5,7 +5,7 @@
|
|||||||
:dependencies
|
:dependencies
|
||||||
[[no.cjohansen/replicant "2025.03.27"]]
|
[[no.cjohansen/replicant "2025.03.27"]]
|
||||||
|
|
||||||
:dev-http {3001 "public"}
|
:dev-http {3000 "public"}
|
||||||
|
|
||||||
:builds
|
:builds
|
||||||
{:dev {:target :browser
|
{:dev {:target :browser
|
||||||
@ -23,8 +23,4 @@
|
|||||||
:output-dir "../pmme-mobile/src/js"
|
:output-dir "../pmme-mobile/src/js"
|
||||||
:asset-path "/js"
|
:asset-path "/js"
|
||||||
:modules {:main {:init-fn pmme-ui.shell/main}}
|
:modules {:main {:init-fn pmme-ui.shell/main}}
|
||||||
:compiler-options {:optimizations :advanced}
|
:compiler-options {:optimizations :advanced}}}}
|
||||||
:build-hooks [(shadow.resource/copy-resources
|
|
||||||
{:from "public"
|
|
||||||
:to "../pmme-mobile/src"
|
|
||||||
:exclude ["**/*.js"]})]}}}
|
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
@source "./**/*.{clj,cljs,cljc}";
|
@source "./**/*.{clj,cljs,cljc}";
|
||||||
@plugin "daisyui";
|
@plugin "daisyui";
|
||||||
|
|
||||||
|
@layer utilities {
|
||||||
|
.badge-error { /* This forces Tailwind to include it */ }
|
||||||
|
}
|
||||||
|
@ -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"]]]]]])
|
|
@ -1,7 +1,84 @@
|
|||||||
(ns pmme-ui.core
|
(ns pmme-ui.core
|
||||||
(:require [replicant.dom :as r]
|
(:require [replicant.dom :as r]))
|
||||||
[pmme-ui.sensors :as sensors]
|
|
||||||
[pmme-ui.admin :as admin]))
|
(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]
|
(defn render-tabs [state]
|
||||||
(when (:show-tabs state)
|
(when (:show-tabs state)
|
||||||
@ -14,12 +91,16 @@
|
|||||||
[:button.tab
|
[:button.tab
|
||||||
{:class (when (= (:current-tab state) :admin) "tab-active")
|
{:class (when (= (:current-tab state) :admin) "tab-active")
|
||||||
:on {:click [::switch-tab :admin]}}
|
:on {:click [::switch-tab :admin]}}
|
||||||
"Admin Panel"]]]))
|
"Admin Panel"
|
||||||
|
(case (:admin-status state)
|
||||||
(defn render-current-content [state]
|
::available
|
||||||
(case (:current-tab state)
|
[:div.ml-2 {:class ["inline-grid" "*:[grid-area:1/1]"]}
|
||||||
:admin (admin/render-admin-panel state)
|
[:div.status.status-success.animate-ping]
|
||||||
:readings (sensors/render-pm25-display state)))
|
[:div.status.status-success]]
|
||||||
|
::unavailable
|
||||||
|
[:div.status.status-error.ml-2]
|
||||||
|
::unknown
|
||||||
|
[:div.status.status-neutral.ml-2])]]]))
|
||||||
|
|
||||||
(defn render-ui [state]
|
(defn render-ui [state]
|
||||||
(r/render
|
(r/render
|
||||||
@ -27,4 +108,3 @@
|
|||||||
[:div
|
[:div
|
||||||
(render-tabs state)
|
(render-tabs state)
|
||||||
(render-current-content state)]))
|
(render-current-content state)]))
|
||||||
|
|
||||||
|
@ -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)])]
|
|
||||||
]]])
|
|
@ -1,12 +1,21 @@
|
|||||||
(ns pmme-ui.shell
|
(ns pmme-ui.shell
|
||||||
(:require [replicant.dom :as r]
|
(: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
|
(defonce store (atom {:number 0
|
||||||
:current-tab :readings}))
|
:current-tab :readings
|
||||||
|
:show-tabs true
|
||||||
|
:admin-status ::core/unknown}))
|
||||||
|
|
||||||
(defn should-show-tabs? []
|
(defn ping-admin-endpoint []
|
||||||
true)
|
(go
|
||||||
|
(try
|
||||||
|
(<p! (js/fetch "http://localhost:8080"))
|
||||||
|
true
|
||||||
|
(catch js/Error e
|
||||||
|
false))))
|
||||||
|
|
||||||
(defn init [store]
|
(defn init [store]
|
||||||
(add-watch store ::render (fn [_ _ _ new-state] (core/render-ui new-state)))
|
(add-watch store ::render (fn [_ _ _ new-state] (core/render-ui new-state)))
|
||||||
@ -14,12 +23,18 @@
|
|||||||
(fn [_ event-data]
|
(fn [_ event-data]
|
||||||
(let [[action & args] event-data]
|
(let [[action & args] event-data]
|
||||||
(case action
|
(case action
|
||||||
::core/test-me
|
|
||||||
(swap! store update :number inc)
|
|
||||||
|
|
||||||
::core/switch-tab
|
::core/switch-tab
|
||||||
(swap! store assoc :current-tab (first args))))))
|
(let [target-tab (first args)]
|
||||||
(swap! store assoc :show-tabs (should-show-tabs?))
|
(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.))))
|
(swap! store assoc ::loaded-at (.getTime (js/Date.))))
|
||||||
|
|
||||||
(defn main []
|
(defn main []
|
||||||
@ -28,4 +43,4 @@
|
|||||||
|
|
||||||
(defn ^:dev/after-load reload []
|
(defn ^:dev/after-load reload []
|
||||||
(init store)
|
(init store)
|
||||||
(println "Reloaded!"))
|
(println "Reloaded"))
|
||||||
|
Loading…
x
Reference in New Issue
Block a user