aboutsummaryrefslogtreecommitdiff
path: root/controller_webpage
diff options
context:
space:
mode:
authorJosh Rahm <joshuarahm@gmail.com>2025-12-07 13:27:46 -0700
committerJosh Rahm <joshuarahm@gmail.com>2025-12-07 13:27:46 -0700
commitb2b344ebec8f7b55506deb0c2138a1e2b9ffeb46 (patch)
tree92163dfc785675294ba71fff2eb0c27c1a16a8c0 /controller_webpage
parent194763e82347be1112d1572f86aa8c8fcf7c54c1 (diff)
downloadesp32-ws2812b-b2b344ebec8f7b55506deb0c2138a1e2b9ffeb46.tar.gz
esp32-ws2812b-b2b344ebec8f7b55506deb0c2138a1e2b9ffeb46.tar.bz2
esp32-ws2812b-b2b344ebec8f7b55506deb0c2138a1e2b9ffeb46.zip
Much nicer version of the controller page.
Diffstat (limited to 'controller_webpage')
-rw-r--r--controller_webpage/index.html458
1 files changed, 317 insertions, 141 deletions
diff --git a/controller_webpage/index.html b/controller_webpage/index.html
index 1400956..bfa6461 100644
--- a/controller_webpage/index.html
+++ b/controller_webpage/index.html
@@ -1,171 +1,347 @@
- <head>
- <style>
- .controls div {
- display: block;
+<!doctype html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>Tree Lights Controller</title>
+ <style>
+ :root {
+ --bg: #0b1221;
+ --panel: #111a2f;
+ --panel-border: #1f2c45;
+ --accent: #35d1ff;
+ --accent-2: #ff6f61;
+ --text: #e8edf4;
+ --muted: #8ba0c2;
+ --radius: 12px;
+ --shadow: 0 12px 40px rgba(0, 0, 0, 0.35);
}
- .right {
- float: right;
+ * { box-sizing: border-box; }
+
+ body {
+ margin: 0;
+ padding: 24px;
+ font-family: "Space Grotesk", "Segoe UI", "Helvetica Neue", sans-serif;
+ background: radial-gradient(circle at 20% 20%, #12213d, #0b1221 40%),
+ radial-gradient(circle at 80% 0%, #1a2c4b, transparent 30%),
+ var(--bg);
+ color: var(--text);
+ min-height: 100vh;
}
- .left {
- float: left;
+
+ header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 16px;
}
- .control-panel div {
- display: inline-block;
+ h1 {
+ margin: 0;
+ font-size: 24px;
+ letter-spacing: 0.3px;
}
- .label .pad {
- min-width: 10em;
+ .toolbar {
+ display: flex;
+ gap: 10px;
+ align-items: center;
}
- </style>
- <script>
- var getJSON = function(url, callback) {
- var xhr = new XMLHttpRequest();
- xhr.open('GET', url, true);
- xhr.responseType = 'json';
- xhr.onload = function() {
- var status = xhr.status;
- if (status === 200) {
- callback(null, xhr.response);
- } else {
- callback(status, xhr.response);
- }
- };
- xhr.send();
- };
-
- var create_int_controls = function(attr, value) {
- var templ = `
- <div id="${attr}-ctrl" class="control-panel">
- <div class="label">
- <div class="pad">
- <a>${value.disp}</a>
- </div>
- </div>
- <div class="controls">
- <input type="text" id="${attr}" value="${value.value}"></input>
- <button onclick="send_request_from_element('${attr}', 'set', '${attr}')">Set</button>
- <button onclick="send_request('${attr}', 'add', '1')">+</button>
- <button onclick="send_request('${attr}', 'sub', '1')">-</button>
- </div>
- </div>
- `
- return templ;
- }
+ button {
+ border: none;
+ border-radius: 999px;
+ padding: 10px 16px;
+ background: var(--accent);
+ color: #041022;
+ font-weight: 600;
+ cursor: pointer;
+ transition: transform 120ms ease, box-shadow 120ms ease, opacity 120ms ease;
+ box-shadow: 0 10px 20px rgba(53, 209, 255, 0.3);
+ }
- var create_color_controls = function(attr, value) {
- var templ = `
- <div id="${attr}-ctrl" class="control-panel">
- <div class="label">
- <div class="pad">
- <a>${value.disp}</a>
- </div>
- </div>
- <div class="controls">
- <input type="text" id="${attr}" value="${value.value}"></input>
- <button onclick="send_request_from_element('${attr}', 'set', '${attr}')">Set</button>
- </div>
- </div>
- `
- return templ;
- }
+ button.secondary {
+ background: transparent;
+ color: var(--text);
+ border: 1px solid var(--panel-border);
+ box-shadow: none;
+ }
- var create_bool_controls = function(attr, value) {
- var templ = `
- <div id="${attr}-ctrl" class="control-panel">
- <div class="label">
- <div class="pad">
- <a>${value.disp}</a>
- </div>
- </div>
- <div class="controls">
- <input type="checkbox" id="${attr}" ${value.value === 'on' ? 'checked' : ''} onclick="send_request('${attr}', 'set', this.checked ? 'on' : 'off')"></input>
- </div>
- </div>
- `
-
- return templ;
- }
+ button:hover { transform: translateY(-1px); }
+ button:active { transform: translateY(0); opacity: 0.9; }
- var parse_data = function(data) {
- var inner_html = '';
- for (const attr in data) {
- var value = data[attr];
- console.log("data: " + data);
- console.log("data['" + attr + "']: " + value.value);
+ #controls {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
+ gap: 14px;
+ }
- if (value.type === 'uint8_t') {
- inner_html += create_int_controls(attr, value);
- }
+ .card {
+ background: var(--panel);
+ border: 1px solid var(--panel-border);
+ border-radius: var(--radius);
+ padding: 14px 16px;
+ box-shadow: var(--shadow);
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ }
- if (value.type === 'uint32_t') {
- inner_html += create_int_controls(attr, value);
- }
+ .card header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 10px;
+ margin: 0;
+ }
- if (value.type === 'color') {
- inner_html += create_color_controls(attr, value);
- }
+ .label {
+ font-weight: 700;
+ font-size: 15px;
+ }
- if (value.type === 'int') {
- inner_html += create_int_controls(attr, value);
- }
+ .type {
+ font-size: 12px;
+ color: var(--muted);
+ background: rgba(255, 255, 255, 0.05);
+ border-radius: 999px;
+ padding: 4px 8px;
+ }
- if (value.type === 'bool') {
- inner_html += create_bool_controls(attr, value);
- }
- }
+ .field {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ width: 100%;
+ }
- document.getElementById('controls').innerHTML = inner_html;
- }
+ input[type="number"],
+ input[type="text"] {
+ flex: 1;
+ border-radius: 10px;
+ border: 1px solid var(--panel-border);
+ background: #0f1629;
+ padding: 10px 12px;
+ color: var(--text);
+ font-size: 14px;
+ outline: none;
+ }
- var refresh_values = function() {
- getJSON("http://192.168.86.249/params",
- function (err, data) {
- parse_data(data);
- });
- }
+ input[type="color"] {
+ width: 48px;
+ height: 36px;
+ border-radius: 10px;
+ border: 1px solid var(--panel-border);
+ background: #0f1629;
+ padding: 0;
+ cursor: pointer;
+ }
- var update_values = function() {
- getJSON("http://192.168.86.249/params",
- function (err, data) {
- for (const attr in data) {
- var value = data[attr];
- if (value.type === 'bool') {
- document.getElementById(attr).checked = value.value === 'on';
- } else {
- document.getElementById(attr).value = value.value;
- }
- }
- });
- }
+ .actions {
+ display: flex;
+ gap: 8px;
+ flex-wrap: wrap;
+ }
- function send_request_from_element(attr, param, id) {
- var value = document.getElementById(id).value;
- send_request(attr, param, value);
- }
+ .checkbox {
+ display: inline-flex;
+ align-items: center;
+ gap: 10px;
+ }
+
+ .status {
+ color: var(--muted);
+ font-size: 13px;
+ margin-top: 12px;
+ }
+
+ @media (max-width: 640px) {
+ body { padding: 18px; }
+ h1 { font-size: 20px; }
+ }
+ </style>
+</head>
+<body>
+ <header>
+ <h1>Tree Lights Controller</h1>
+ <div class="toolbar">
+ <button class="secondary" onclick="refreshValues()">Reload</button>
+ </div>
+ </header>
+
+ <div id="controls"></div>
+ <div class="status" id="status"></div>
+
+ <script>
+ const BASE_URL = "http://192.168.86.249";
+
+ const state = {
+ params: {},
+ };
+
+ function setStatus(text) {
+ document.getElementById("status").textContent = text || "";
+ }
+
+ async function fetchJSON(url) {
+ const res = await fetch(url);
+ if (!res.ok) throw new Error(res.status);
+ return res.json();
+ }
+
+ function createCard(attr, value) {
+ const container = document.createElement("div");
+ container.className = "card";
+ container.id = `${attr}-card`;
- function send_request(attr, param, value) {
- var xhr = new XMLHttpRequest();
- xhr.open("POST", "http://192.168.86.249/" + attr + "?" + param + "=" +
- value);
- xhr.onload = function() {
- update_values();
+ const header = document.createElement("header");
+ const label = document.createElement("div");
+ label.className = "label";
+ label.textContent = value.disp || attr;
+ const type = document.createElement("div");
+ type.className = "type";
+ type.textContent = value.type;
+ header.appendChild(label);
+ header.appendChild(type);
+
+ const field = document.createElement("div");
+ field.className = "field";
+
+ if (value.type === "bool") {
+ const checkboxWrap = document.createElement("label");
+ checkboxWrap.className = "checkbox";
+ const input = document.createElement("input");
+ input.type = "checkbox";
+ input.id = attr;
+ input.checked = value.value === "on";
+ input.onchange = () =>
+ sendRequest(attr, "set", input.checked ? "on" : "off");
+ const text = document.createElement("span");
+ text.textContent = input.checked ? "On" : "Off";
+ input.addEventListener("change", () => {
+ text.textContent = input.checked ? "On" : "Off";
+ });
+ checkboxWrap.appendChild(input);
+ checkboxWrap.appendChild(text);
+ field.appendChild(checkboxWrap);
+ } else if (value.type === "color") {
+ const colorInput = document.createElement("input");
+ colorInput.type = "color";
+ colorInput.id = attr;
+ const textInput = document.createElement("input");
+ textInput.type = "text";
+ colorInput.value = value.value.startsWith("#")
+ ? value.value
+ : `#${value.value}`;
+ textInput.value = colorInput.value;
+
+ colorInput.onchange = () => {
+ textInput.value = colorInput.value;
+ sendRequest(attr, "set", colorInput.value.replace(/^#/, ""));
+ };
+
+ textInput.onchange = () => {
+ let val = textInput.value.trim();
+ if (!val.startsWith("#")) val = `#${val.replace(/^0x/i, "")}`;
+ colorInput.value = val;
+ sendRequest(attr, "set", val.replace(/^#/, ""));
};
- xhr.send();
+
+ field.appendChild(colorInput);
+ field.appendChild(textInput);
+ } else {
+ const input = document.createElement("input");
+ input.type = "number";
+ input.id = attr;
+ input.value = value.value;
+ field.appendChild(input);
+
+ const actions = document.createElement("div");
+ actions.className = "actions";
+
+ const setBtn = document.createElement("button");
+ setBtn.textContent = "Set";
+ setBtn.onclick = () => sendRequest(attr, "set", input.value);
+ actions.appendChild(setBtn);
+
+ const incBtn = document.createElement("button");
+ incBtn.textContent = "+";
+ incBtn.onclick = () => sendRequest(attr, "add", "1");
+ actions.appendChild(incBtn);
+
+ const decBtn = document.createElement("button");
+ decBtn.textContent = "−";
+ decBtn.onclick = () => sendRequest(attr, "sub", "1");
+ actions.appendChild(decBtn);
+
+ field.appendChild(actions);
}
- </script>
- </head>
- <body onload="refresh_values()">
+ container.appendChild(header);
+ container.appendChild(field);
+ return container;
+ }
- <h1>Control Josh's Christmas Tree!</h1>
+ function renderControls(data) {
+ state.params = data;
+ const controls = document.getElementById("controls");
+ controls.innerHTML = "";
+ Object.keys(data).forEach((attr) => {
+ const card = createCard(attr, data[attr]);
+ controls.appendChild(card);
+ });
+ }
- <div id="controls">
- </div>
+ async function refreshValues() {
+ setStatus("Loading…");
+ try {
+ const data = await fetchJSON(`${BASE_URL}/params`);
+ renderControls(data);
+ setStatus("");
+ } catch (err) {
+ setStatus("Failed to load params");
+ console.error(err);
+ }
+ }
- </body>
+ async function updateValues() {
+ try {
+ const data = await fetchJSON(`${BASE_URL}/params`);
+ Object.keys(data).forEach((attr) => {
+ const value = data[attr];
+ const el = document.getElementById(attr);
+ if (!el) return;
+ if (value.type === "bool") {
+ el.checked = value.value === "on";
+ } else {
+ const newVal = value.type === "color"
+ ? (value.value.startsWith("#") ? value.value : `#${value.value}`)
+ : value.value;
+ el.value = newVal;
+ if (value.type === "color" && el.nextElementSibling) {
+ el.nextElementSibling.value = newVal;
+ }
+ }
+ });
+ } catch (err) {
+ console.error(err);
+ }
+ }
-</html>
+ async function sendRequest(attr, param, value) {
+ try {
+ await fetch(`${BASE_URL}/${attr}?${param}=${encodeURIComponent(value)}`, {
+ method: "POST",
+ });
+ updateValues();
+ } catch (err) {
+ setStatus("Request failed");
+ console.error(err);
+ }
+ }
+ refreshValues();
+ </script>
+</body>
+</html>