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:
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user