diff options
| author | Josh Rahm <joshuarahm@gmail.com> | 2025-12-07 13:27:46 -0700 |
|---|---|---|
| committer | Josh Rahm <joshuarahm@gmail.com> | 2025-12-07 13:27:46 -0700 |
| commit | b2b344ebec8f7b55506deb0c2138a1e2b9ffeb46 (patch) | |
| tree | 92163dfc785675294ba71fff2eb0c27c1a16a8c0 /controller_webpage | |
| parent | 194763e82347be1112d1572f86aa8c8fcf7c54c1 (diff) | |
| download | esp32-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.html | 458 |
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> |