diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6315119 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/*Claude-PMME* diff --git a/pmme-ui/shadow-cljs.edn b/pmme-ui/shadow-cljs.edn index c47783b..3638429 100644 --- a/pmme-ui/shadow-cljs.edn +++ b/pmme-ui/shadow-cljs.edn @@ -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}}}} diff --git a/pmme-ui/src/input.css b/pmme-ui/src/input.css index c5201fe..02b8cdf 100644 --- a/pmme-ui/src/input.css +++ b/pmme-ui/src/input.css @@ -1,3 +1,7 @@ @import "tailwindcss"; @source "./**/*.{clj,cljs,cljc}"; @plugin "daisyui"; + +@layer utilities { + .badge-error { /* This forces Tailwind to include it */ } +} diff --git a/pmme-ui/src/main/pmme_ui/admin.cljs b/pmme-ui/src/main/pmme_ui/admin.cljs deleted file mode 100644 index 8f63ff6..0000000 --- a/pmme-ui/src/main/pmme_ui/admin.cljs +++ /dev/null @@ -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"]]]]]]) diff --git a/pmme-ui/src/main/pmme_ui/core.cljs b/pmme-ui/src/main/pmme_ui/core.cljs index 867e80e..a4fc8e7 100644 --- a/pmme-ui/src/main/pmme_ui/core.cljs +++ b/pmme-ui/src/main/pmme_ui/core.cljs @@ -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)])) diff --git a/pmme-ui/src/main/pmme_ui/sensors.cljs b/pmme-ui/src/main/pmme_ui/sensors.cljs deleted file mode 100644 index 47cbf17..0000000 --- a/pmme-ui/src/main/pmme_ui/sensors.cljs +++ /dev/null @@ -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)])] - ]]]) diff --git a/pmme-ui/src/main/pmme_ui/shell.cljs b/pmme-ui/src/main/pmme_ui/shell.cljs index 1d3b3e6..a59a0df 100644 --- a/pmme-ui/src/main/pmme_ui/shell.cljs +++ b/pmme-ui/src/main/pmme_ui/shell.cljs @@ -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