workable POC
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
:root { color-scheme: dark; background: #080a0f; font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", sans-serif; }
|
||||
* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
|
||||
body { margin: 0; background: #080a0f; color: #f7f8fc; }
|
||||
.dashboard { min-height: 100vh; min-height: 100dvh; padding: max(12px, env(safe-area-inset-top)) max(12px, env(safe-area-inset-right)) max(12px, env(safe-area-inset-bottom)) max(12px, env(safe-area-inset-left)); }
|
||||
.button-grid { height: calc(100dvh - max(24px, env(safe-area-inset-top)) - max(24px, env(safe-area-inset-bottom))); display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-rows: repeat(4, minmax(0, 1fr)); gap: 12px; }
|
||||
.group-button { appearance: none; border: 1px solid #252a36; border-radius: 20px; background: linear-gradient(145deg, #1a1e28, #10131b); color: inherit; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; font: inherit; font-size: clamp(1.05rem, 5vw, 1.5rem); font-weight: 650; letter-spacing: -.02em; touch-action: manipulation; transition: transform .12s ease, background .2s ease, border-color .2s ease, box-shadow .2s ease; }
|
||||
.group-button small { color: #8c94a6; font-size: .76rem; font-weight: 600; line-height: 1.2; text-align: center; }
|
||||
.group-button.is-on { border-color: #3196ff; box-shadow: 0 0 0 2px #1677d8, 0 0 20px #1264ba55; }
|
||||
.group-button:active { transform: scale(.975); background: #222938; }
|
||||
.group-button:focus-visible { outline: 3px solid #f3f7ff; outline-offset: 3px; }
|
||||
.group-button[disabled] { opacity: .5; }
|
||||
.status { position: fixed; z-index: 1; bottom: calc(env(safe-area-inset-bottom) + 10px); left: 50%; margin: 0; padding: 6px 10px; border-radius: 999px; background: #252a36dd; color: #cbd2df; font-size: .75rem; transform: translateX(-50%); opacity: 0; transition: opacity .2s; pointer-events: none; white-space: nowrap; }
|
||||
.status.visible { opacity: 1; }
|
||||
@media (orientation: landscape) { .button-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); grid-template-rows: repeat(2, minmax(0, 1fr)); } }
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
const status = document.querySelector('#status');
|
||||
const buttons = [...document.querySelectorAll('.group-button')];
|
||||
const apiPrefix = document.body.dataset.apiPrefix;
|
||||
const url = new URL(window.location.href);
|
||||
const suppliedKey = url.searchParams.get('key');
|
||||
if (suppliedKey) {
|
||||
sessionStorage.setItem('fasthue-api-key', suppliedKey);
|
||||
url.searchParams.delete('key');
|
||||
history.replaceState({}, '', url);
|
||||
}
|
||||
const apiKey = sessionStorage.getItem('fasthue-api-key');
|
||||
|
||||
function apiFetch(path, options = {}) {
|
||||
return fetch(path, {
|
||||
...options,
|
||||
headers: { ...options.headers, Authorization: `Bearer ${apiKey || ''}` },
|
||||
});
|
||||
}
|
||||
|
||||
function showStatus(message, persistent = false) {
|
||||
status.textContent = message;
|
||||
status.classList.add('visible');
|
||||
if (!persistent) window.setTimeout(() => status.classList.remove('visible'), 1800);
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
try {
|
||||
const response = await apiFetch(`${apiPrefix}/groups`, { cache: 'no-store' });
|
||||
const body = await response.json();
|
||||
if (!response.ok) throw new Error(body.error || 'Could not check the bridge');
|
||||
const states = new Map(body.groups.map(group => [group.id, group]));
|
||||
buttons.forEach(button => {
|
||||
const group = states.get(button.dataset.id);
|
||||
button.classList.toggle('is-on', group?.on === true);
|
||||
button.querySelector('small').textContent = group?.scene || '';
|
||||
});
|
||||
status.classList.remove('visible');
|
||||
} catch (error) {
|
||||
showStatus(error.message, true);
|
||||
}
|
||||
}
|
||||
|
||||
async function activateNextScene(button) {
|
||||
button.disabled = true;
|
||||
try {
|
||||
const response = await apiFetch(`${apiPrefix}/groups/${button.dataset.id}/next-scene`, { method: 'POST' });
|
||||
const body = await response.json();
|
||||
if (!response.ok) throw new Error(body.error || 'Could not control the bridge');
|
||||
showStatus(`${button.querySelector('span').textContent}: ${body.scene}`);
|
||||
button.querySelector('small').textContent = body.scene;
|
||||
window.setTimeout(refresh, 350);
|
||||
} catch (error) {
|
||||
showStatus(error.message, true);
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function turnOff(button) {
|
||||
button.disabled = true;
|
||||
try {
|
||||
const response = await apiFetch(`${apiPrefix}/groups/${button.dataset.id}/off`, { method: 'POST' });
|
||||
const body = await response.json();
|
||||
if (!response.ok) throw new Error(body.error || 'Could not turn off the lights');
|
||||
button.classList.remove('is-on');
|
||||
button.querySelector('small').textContent = 'Lights off';
|
||||
showStatus(`${button.querySelector('span').textContent}: off`);
|
||||
window.setTimeout(refresh, 350);
|
||||
} catch (error) {
|
||||
showStatus(error.message, true);
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
buttons.forEach(button => {
|
||||
let pressTimer;
|
||||
let longPressHandled = false;
|
||||
|
||||
const clearPressTimer = () => window.clearTimeout(pressTimer);
|
||||
button.addEventListener('pointerdown', () => {
|
||||
longPressHandled = false;
|
||||
pressTimer = window.setTimeout(() => {
|
||||
longPressHandled = true;
|
||||
turnOff(button);
|
||||
}, 650);
|
||||
});
|
||||
button.addEventListener('pointerup', clearPressTimer);
|
||||
button.addEventListener('pointercancel', clearPressTimer);
|
||||
button.addEventListener('pointerleave', clearPressTimer);
|
||||
button.addEventListener('click', event => {
|
||||
if (longPressHandled) {
|
||||
event.preventDefault();
|
||||
longPressHandled = false;
|
||||
return;
|
||||
}
|
||||
activateNextScene(button);
|
||||
});
|
||||
});
|
||||
|
||||
refresh();
|
||||
window.setInterval(refresh, 15000);
|
||||
Reference in New Issue
Block a user