🌐️ pmme-ui: New replicant design with sensors and admin panel tabs

This commit is contained in:
Joseph Ferano 2025-06-22 02:31:52 +07:00
parent 2c741adac7
commit 3143f3b37f
12 changed files with 1221 additions and 181 deletions

1
pmme-ui/.dir-locals.el Normal file
View File

@ -0,0 +1 @@
((nil . ((joe/shell-command-silent-default . "./pmme-ui/browser-reload.sh"))))

6
pmme-ui/.gitignore vendored
View File

@ -18,3 +18,9 @@ pom.xml.asc
.hgignore
.hg/
node_modules/
.tab-id
\*Claude*
public/styles.css

1076
pmme-ui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,11 @@
"version": "0.0.1",
"private": true,
"devDependencies": {
"daisyui": "^5.0.43",
"shadow-cljs": "3.1.5"
},
"dependencies": {
"@tailwindcss/cli": "^4.1.10",
"tailwindcss": "^4.1.10"
}
}

View File

@ -1,32 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="stylesheet" href="/styles.css" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tauri App</title>
</head>
<body>
<main class="container">
<h1>Welcome to Tauri</h1>
<div class="row">
<a href="https://tauri.app" target="_blank">
<img src="/assets/tauri.svg" class="logo tauri" alt="Tauri logo" />
</a>
<a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript" target="_blank">
<img src="/assets/javascript.svg" class="logo vanilla" alt="JavaScript logo" />
</a>
</div>
<p>Click on the Tauri logo to learn more about the framework.</p>
<form class="row" id="greet-form">
<input id="greet-input" placeholder="Enter a name..." />
<button type="submit">Greet</button>
</form>
<p id="greet-msg"></p>
</main>
<script src="./js/main.js"></script>
</body>
<!DOCTYPE html>
<html>
<head>
<title>PMME</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" href="/styles.css"
</head>
<body>
<script src="/js/main.js"></script>
</body>
</html>

View File

@ -1,112 +0,0 @@
.logo.vanilla:hover {
filter: drop-shadow(0 0 2em #ffe21c);
}
:root {
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
font-size: 16px;
line-height: 24px;
font-weight: 400;
color: #0f0f0f;
background-color: #f6f6f6;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
.container {
margin: 0;
padding-top: 10vh;
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: 0.75s;
}
.logo.tauri:hover {
filter: drop-shadow(0 0 2em #24c8db);
}
.row {
display: flex;
justify-content: center;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
h1 {
text-align: center;
}
input,
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
color: #0f0f0f;
background-color: #ffffff;
transition: border-color 0.25s;
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
}
button {
cursor: pointer;
}
button:hover {
border-color: #396cd8;
}
button:active {
border-color: #396cd8;
background-color: #e8e8e8;
}
input,
button {
outline: none;
}
#greet-input {
margin-right: 5px;
}
@media (prefers-color-scheme: dark) {
:root {
color: #f6f6f6;
background-color: #2f2f2f;
}
a:hover {
color: #24c8db;
}
input,
button {
color: #ffffff;
background-color: #0f0f0f98;
}
button:active {
background-color: #0f0f0f69;
}
}

View File

@ -1,24 +1,28 @@
;; shadow-cljs configuration
{:source-paths
["src/main"
"src/test"]
:dependencies
[]
[[no.cjohansen/replicant "2025.03.27"]]
:dev-http {3000 "public"}
:dev-http {3001 "public"}
:builds
{:app {:target :browser
{:dev {:target :browser
:output-dir "public/js"
:asset-path "/js"
:modules {:main {:init-fn pmme-ui.core/init}}
:devtools {:devtools-url "http://192.168.110.133:9630"}}
:modules {:main {:init-fn pmme-ui.shell/main}}}
:mobile {:target :browser
:output-dir "public/js"
:asset-path "/js"
:modules {:main {:init-fn pmme-ui.shell/main}}
:devtools {:devtools-url "http://192.168.110.133:9630"}}
:release {:target :browser
:output-dir "../pmme-mobile/src/js"
:asset-path "/js"
:modules {:main {:init-fn pmme-ui.core/init}}
:modules {:main {:init-fn pmme-ui.shell/main}}
:compiler-options {:optimizations :advanced}
:build-hooks [(shadow.resource/copy-resources
{:from "public"

3
pmme-ui/src/input.css Normal file
View File

@ -0,0 +1,3 @@
@import "tailwindcss";
@source "./**/*.{clj,cljs,cljc}";
@plugin "daisyui";

View File

@ -0,0 +1,25 @@
(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,36 +1,30 @@
(ns pmme-ui.core
(:require [clojure.string :as str]))
(:require [replicant.dom :as r]
[pmme-ui.sensors :as sensors]
[pmme-ui.admin :as admin]))
(defn handle-greet [event]
(.preventDefault event)
(let [input (.getElementById js/document "greet-input")
msg-element (.getElementById js/document "greet-msg")
name (.-value input)]
(if (empty? (str/trim name))
(set! (.-innerHTML msg-element) "Please enter a name!")
(do
;; You could call a Tauri command here instead
(js/console.log "Testing the chrome thingie")
(set! (.-innerHTML msg-element)
(str "Hello, " name "! 👋 (from ClojureScript)!"))
(set! (.-value input) "")))))
(defn render-tabs [state]
(when (:show-tabs state)
[:div.fixed.top-4.w-full.flex.justify-center
[:div.tabs.tabs-boxed.bg-white-90.backdrop-blur-sm.shadow-lg.border.border-slate-200
[:button.tab
{:class (when (= (:current-tab state) :readings) "tab-active")
:on {:click [::switch-tab :readings]}}
"Current Readings"]
[:button.tab
{:class (when (= (:current-tab state) :admin) "tab-active")
:on {:click [::switch-tab :admin]}}
"Admin Panel"]]]))
(defn greet-handler [event]
(handle-greet event))
(defn render-current-content [state]
(case (:current-tab state)
:admin (admin/render-admin-panel state)
:readings (sensors/render-pm25-display state)))
(defn setup-greet-form []
(when-let [form (.getElementById js/document "greet-form")]
(.addEventListener form "submit" greet-handler)))
(defn render-ui [state]
(r/render
js/document.body
[:div
(render-tabs state)
(render-current-content state)]))
(defn add-dynamic-styling []
(let [style (.createElement js/document "style")]
(set! (.-textContent style)
"#greet-msg { color: #646cff; font-weight: bold; margin-top: 1rem; }")
(.appendChild (.-head js/document) style)))
(defn init []
(js/console.log "PMME UI initialized!")
(setup-greet-form)
(add-dynamic-styling)
;; Remove the app element modification since it doesn't exist in this HTML
(js/console.log "Greet form is now powered by ClojureScript!"))

View File

@ -0,0 +1,27 @@
(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

@ -0,0 +1,31 @@
(ns pmme-ui.shell
(:require [replicant.dom :as r]
[pmme-ui.core :as core]))
(defonce store (atom {:number 0
:current-tab :readings}))
(defn should-show-tabs? []
true)
(defn init [store]
(add-watch store ::render (fn [_ _ _ new-state] (core/render-ui new-state)))
(r/set-dispatch!
(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?))
(swap! store assoc ::loaded-at (.getTime (js/Date.))))
(defn main []
(init store)
(println "Loaded!"))
(defn ^:dev/after-load reload []
(init store)
(println "Reloaded!"))