Replace color wheel in Lighting component due to hydration issues, adjust update debounce to account for increased serverside rate limits (10 requests/2 seconds -> 25 requests/2 seconds)

This commit is contained in:
2025-10-08 10:56:42 -04:00
parent 2b7a3da085
commit ef4c80450a
2 changed files with 121 additions and 78 deletions

View File

@@ -40,7 +40,7 @@ export default function Root({ child, user = undefined }) {
{child == "Memes" && <Memes client:only="react" />} {child == "Memes" && <Memes client:only="react" />}
{child == "qs2.MediaRequestForm" && <MediaRequestForm client:only="react" />} {child == "qs2.MediaRequestForm" && <MediaRequestForm client:only="react" />}
{child == "qs2.RequestManagement" && <RequestManagement client:only="react" />} {child == "qs2.RequestManagement" && <RequestManagement client:only="react" />}
{child == "Lighting" && <Lighting client:only="react" />} {child == "Lighting" && <Lighting key={window.location.pathname + Math.random()} client:only="react" />}
</JoyUIRootIsland> </JoyUIRootIsland>
</PrimeReactProvider> </PrimeReactProvider>
); );

View File

@@ -1,8 +1,7 @@
import React, { useEffect, useState, useRef, Suspense, lazy } from 'react';
import React, { useEffect, useState, useRef } from 'react';
import { API_URL } from '../config.js'; import { API_URL } from '../config.js';
import { authFetch } from '../utils/authFetch.js'; import { authFetch } from '../utils/authFetch.js';
import { HexColorPicker } from "react-colorful"; import Wheel from '@uiw/react-color-wheel';
export default function Lighting() { export default function Lighting() {
const [state, setState] = useState({ power: '', red: 0, blue: 0, green: 0, brightness: 100 }); const [state, setState] = useState({ power: '', red: 0, blue: 0, green: 0, brightness: 100 });
@@ -23,86 +22,59 @@ export default function Lighting() {
}); });
}, []); }, []);
// Live update handler useEffect(() => {
const debounceRef = useRef(); setState({ power: '', red: 0, blue: 0, green: 0, brightness: 100 });
const updateLighting = newState => { }, []);
// If brightness is 0, force power off
let { power, red, green, blue, brightness } = newState;
if (brightness === 0) power = 'off';
const prev = state;
if (
power === prev.power &&
red === prev.red &&
green === prev.green &&
blue === prev.blue &&
brightness === prev.brightness
) return;
setState({ ...newState, power }); const handleColorChange = (color) => {
setLoading(true); console.log('Handle color change:', color);
setSuccess(false); const { r, g, b } = color.rgb;
if (debounceRef.current) clearTimeout(debounceRef.current);
debounceRef.current = setTimeout(() => {
const payload = { power, red, green, blue, brightness };
authFetch(`${API_URL}/lighting/state`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
.then(res => res.json())
.then(data => {
// Only update state if all fields are present, otherwise preserve previous
setState(prev => ({
power: typeof data.power === 'string' ? data.power : prev.power,
red: typeof data.red === 'number' ? data.red : prev.red,
green: typeof data.green === 'number' ? data.green : prev.green,
blue: typeof data.blue === 'number' ? data.blue : prev.blue,
brightness: typeof data.brightness === 'number' ? data.brightness : prev.brightness,
}));
setLoading(false);
setSuccess(true);
})
.catch(() => {
setError('Failed to update lighting state');
setLoading(false);
});
}, 400); // 400ms debounce
};
const handlePowerToggle = () => {
const newPower = state.power === 'on' ? 'off' : 'on';
updateLighting({ ...state, power: newPower });
};
const handleColorChange = hex => {
const rgb = hexToRgb(hex);
updateLighting({ updateLighting({
power: state.power, // always preserve current power state ...state,
red: rgb.red, red: r,
green: rgb.green, green: g,
blue: rgb.blue blue: b,
}); });
}; };
const handleSubmit = e => { const debounceRef = useRef();
const updateLighting = (newState) => {
setState(newState);
// Clear any pending timeout
if (debounceRef.current) {
clearTimeout(debounceRef.current);
}
// Set new timeout for API call
debounceRef.current = setTimeout(() => {
authFetch(`${API_URL}/lighting/state`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newState),
})
.then(() => setSuccess(true))
.catch(() => setError('Failed to update lighting state'));
}, 100); // 100ms debounce for 25 req/2s rate limit
};
const handleSubmit = (e) => {
e.preventDefault(); e.preventDefault();
setLoading(true); setLoading(true);
setSuccess(false);
authFetch(`${API_URL}/lighting/state`, { authFetch(`${API_URL}/lighting/state`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(state), body: JSON.stringify(state),
}) })
.then(res => res.json()) .then(() => setSuccess(true))
.then(data => { .catch(() => setError('Failed to update lighting state'))
setState(data); .finally(() => setLoading(false));
setLoading(false); };
setSuccess(true);
}) const handlePowerToggle = () => {
.catch(() => { const newPower = state.power === 'on' ? 'off' : 'on';
setError('Failed to update lighting state'); updateLighting({ ...state, power: newPower });
setLoading(false);
});
}; };
// Guard against undefined values // Guard against undefined values
@@ -137,11 +109,37 @@ export default function Lighting() {
<div className="mb-8 flex flex-col items-center"> <div className="mb-8 flex flex-col items-center">
<label className="font-semibold mb-2">Color</label> <label className="font-semibold mb-2">Color</label>
<HexColorPicker <div style={{
color={rgbToHex(state.red ?? 0, state.green ?? 0, state.blue ?? 0)} width: '180px',
onChange={handleColorChange} height: '180px',
style={{ width: '180px', height: '180px', borderRadius: '1rem', boxShadow: '0 2px 16px 0 #0002', marginBottom: '1rem' }} borderRadius: '1rem',
/> boxShadow: '0 2px 16px 0 #0002',
marginBottom: '1rem',
padding: '0',
overflow: 'hidden'
}}>
<Wheel
hsva={{ h: 0, s: 0, v: 100, a: 1 }}
onChange={(color) => {
const { h, s, v } = color.hsva;
// Convert percentages to decimals and handle undefined v
const rgb = hsvToRgb(
h / 360, // hue: 0-360 -> 0-1
s / 100, // saturation: 0-100 -> 0-1
(v ?? 100) / 100 // value: 0-100 -> 0-1, default to 1 if undefined
);
console.log('Converting color:', color.hsva, 'to RGB:', rgb);
updateLighting({
...state,
red: rgb.red,
green: rgb.green,
blue: rgb.blue
});
}}
width={180}
height={180}
/>
</div>
</div> </div>
<div className="mb-8 flex flex-col items-center w-full"> <div className="mb-8 flex flex-col items-center w-full">
<label className="font-semibold mb-2" htmlFor="brightness">Brightness</label> <label className="font-semibold mb-2" htmlFor="brightness">Brightness</label>
@@ -151,7 +149,23 @@ export default function Lighting() {
min={0} min={0}
max={100} max={100}
value={state.brightness} value={state.brightness}
onChange={e => updateLighting({ ...state, brightness: Number(e.target.value) })} onChange={e => {
const newValue = Number(e.target.value);
const newState = {
...state,
brightness: newValue,
power: newValue === 0 ? 'off' : state.power
};
setState(newState);
if (debounceRef.current) {
clearTimeout(debounceRef.current);
}
debounceRef.current = setTimeout(() => {
updateLighting(newState);
}, 100); // 100ms debounce for 25 req/2s rate limit
}}
className="w-full max-w-xs accent-yellow-500" className="w-full max-w-xs accent-yellow-500"
/> />
<span className="mt-2 text-sm font-bold text-yellow-700 dark:text-yellow-400">{state.brightness}</span> <span className="mt-2 text-sm font-bold text-yellow-700 dark:text-yellow-400">{state.brightness}</span>
@@ -196,6 +210,35 @@ function rgbToHex(r, g, b) {
); );
} }
function hsvToRgb(h, s, v) {
h = Math.max(0, Math.min(1, h));
s = Math.max(0, Math.min(1, s));
v = Math.max(0, Math.min(1, v));
let r, g, b;
const i = Math.floor(h * 6);
const f = h * 6 - i;
const p = v * (1 - s);
const q = v * (1 - f * s);
const t = v * (1 - (1 - f) * s);
switch (i % 6) {
case 0: r = v; g = t; b = p; break;
case 1: r = q; g = v; b = p; break;
case 2: r = p; g = v; b = t; break;
case 3: r = p; g = q; b = v; break;
case 4: r = t; g = p; b = v; break;
case 5: r = v; g = p; b = q; break;
default: r = 0; g = 0; b = 0;
}
return {
red: Math.max(0, Math.min(255, Math.round(r * 255))),
green: Math.max(0, Math.min(255, Math.round(g * 255))),
blue: Math.max(0, Math.min(255, Math.round(b * 255)))
};
}
function hexToRgb(hex) { function hexToRgb(hex) {
hex = hex.replace('#', ''); hex = hex.replace('#', '');
if (hex.length === 3) { if (hex.length === 3) {