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 == "qs2.MediaRequestForm" && <MediaRequestForm 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>
</PrimeReactProvider>
);

View File

@@ -1,8 +1,7 @@
import React, { useEffect, useState, useRef } from 'react';
import React, { useEffect, useState, useRef, Suspense, lazy } from 'react';
import { API_URL } from '../config.js';
import { authFetch } from '../utils/authFetch.js';
import { HexColorPicker } from "react-colorful";
import Wheel from '@uiw/react-color-wheel';
export default function Lighting() {
const [state, setState] = useState({ power: '', red: 0, blue: 0, green: 0, brightness: 100 });
@@ -23,86 +22,59 @@ export default function Lighting() {
});
}, []);
// Live update handler
const debounceRef = useRef();
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;
useEffect(() => {
setState({ power: '', red: 0, blue: 0, green: 0, brightness: 100 });
}, []);
setState({ ...newState, power });
setLoading(true);
setSuccess(false);
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);
const handleColorChange = (color) => {
console.log('Handle color change:', color);
const { r, g, b } = color.rgb;
updateLighting({
power: state.power, // always preserve current power state
red: rgb.red,
green: rgb.green,
blue: rgb.blue
...state,
red: r,
green: g,
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();
setLoading(true);
setSuccess(false);
authFetch(`${API_URL}/lighting/state`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(state),
})
.then(res => res.json())
.then(data => {
setState(data);
setLoading(false);
setSuccess(true);
})
.catch(() => {
setError('Failed to update lighting state');
setLoading(false);
});
.then(() => setSuccess(true))
.catch(() => setError('Failed to update lighting state'))
.finally(() => setLoading(false));
};
const handlePowerToggle = () => {
const newPower = state.power === 'on' ? 'off' : 'on';
updateLighting({ ...state, power: newPower });
};
// Guard against undefined values
@@ -137,11 +109,37 @@ export default function Lighting() {
<div className="mb-8 flex flex-col items-center">
<label className="font-semibold mb-2">Color</label>
<HexColorPicker
color={rgbToHex(state.red ?? 0, state.green ?? 0, state.blue ?? 0)}
onChange={handleColorChange}
style={{ width: '180px', height: '180px', borderRadius: '1rem', boxShadow: '0 2px 16px 0 #0002', marginBottom: '1rem' }}
/>
<div style={{
width: '180px',
height: '180px',
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 className="mb-8 flex flex-col items-center w-full">
<label className="font-semibold mb-2" htmlFor="brightness">Brightness</label>
@@ -151,7 +149,23 @@ export default function Lighting() {
min={0}
max={100}
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"
/>
<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) {
hex = hex.replace('#', '');
if (hex.length === 3) {