From 9f8f0bb990725bdeb2a0cacf2f09e759503b05a7 Mon Sep 17 00:00:00 2001 From: codey Date: Thu, 2 Oct 2025 13:14:13 -0400 Subject: [PATCH] Add Lighting component and integrate with navigation and routing - Introduced Lighting component - Updated AppLayout to include Lighting in the child components. - Added Lighting route to navigation with authentication requirement. --- src/components/AppLayout.jsx | 2 + src/components/AudioPlayer.jsx | 23 +++- src/components/Lighting.jsx | 210 +++++++++++++++++++++++++++++++++ src/config.js | 2 + src/layouts/Nav.astro | 1 + src/pages/lighting.astro | 20 ++++ 6 files changed, 254 insertions(+), 4 deletions(-) create mode 100644 src/components/Lighting.jsx create mode 100644 src/pages/lighting.astro diff --git a/src/components/AppLayout.jsx b/src/components/AppLayout.jsx index 6f93176..f9e818c 100644 --- a/src/components/AppLayout.jsx +++ b/src/components/AppLayout.jsx @@ -1,5 +1,6 @@ import React, { Suspense, lazy } from 'react'; import Memes from './Memes.jsx'; +import Lighting from './Lighting.jsx'; import { toast } from 'react-toastify'; import { JoyUIRootIsland } from './Components.jsx'; import { PrimeReactProvider } from "primereact/api"; @@ -39,6 +40,7 @@ export default function Root({ child, user = undefined }) { {child == "Memes" && } {child == "qs2.MediaRequestForm" && } {child == "qs2.RequestManagement" && } + {child == "Lighting" && } ); diff --git a/src/components/AudioPlayer.jsx b/src/components/AudioPlayer.jsx index 1a62da2..e9744e1 100644 --- a/src/components/AudioPlayer.jsx +++ b/src/components/AudioPlayer.jsx @@ -613,7 +613,7 @@ export default function Player({ user }) { const handleRemoveFromQueue = async (uuid) => { try { - const response = await authFetch(`${API_URL}/radio/remove_from_queue`, { + const response = await authFetch(`${API_URL}/radio/queue_remove`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ @@ -637,12 +637,13 @@ export default function Player({ user }) { const [queueSearch, setQueueSearch] = useState(""); const fetchQueue = async (page = queuePage, rows = queueRows, search = queueSearch) => { const start = page * rows; + console.log("Fetching queue for station (ref):", activeStationRef.current); try { const response = await authFetch(`${API_URL}/radio/get_queue`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ - station: activeStation, + station: activeStationRef.current, // Use ref to ensure latest value start, length: rows, search, @@ -650,7 +651,6 @@ export default function Player({ user }) { }); const data = await response.json(); setQueueData(data.items || []); - // Use recordsFiltered for search, otherwise recordsTotal setQueueTotalRecords( typeof search === 'string' && search.length > 0 ? data.recordsFiltered ?? data.recordsTotal ?? 0 @@ -663,9 +663,24 @@ export default function Player({ user }) { useEffect(() => { if (isQueueVisible) { + console.log("Fetching queue for station:", activeStation); fetchQueue(queuePage, queueRows, queueSearch); } - }, [isQueueVisible, queuePage, queueRows, queueSearch]); + }, [isQueueVisible, queuePage, queueRows, queueSearch, activeStation]); + + useEffect(() => { + console.log("Active station changed to:", activeStation); + if (isQueueVisible) { + fetchQueue(queuePage, queueRows, queueSearch); + } + }, [activeStation]); + + useEffect(() => { + if (isQueueVisible) { + console.log("Track changed, refreshing queue for station:", activeStation); + fetchQueue(queuePage, queueRows, queueSearch); + } + }, [currentTrackUuid]); // Always define queueFooter, fallback to Close button if user is not available const isDJ = (user && user.roles.includes('dj')) || ENVIRONMENT === "Dev"; diff --git a/src/components/Lighting.jsx b/src/components/Lighting.jsx new file mode 100644 index 0000000..b662e21 --- /dev/null +++ b/src/components/Lighting.jsx @@ -0,0 +1,210 @@ + +import React, { useEffect, useState, useRef } from 'react'; +import { API_URL } from '../config.js'; +import { authFetch } from '../utils/authFetch.js'; +import { HexColorPicker } from "react-colorful"; + +export default function Lighting() { + const [state, setState] = useState({ power: '', red: 0, blue: 0, green: 0, brightness: 100 }); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + const [success, setSuccess] = useState(false); + + useEffect(() => { + authFetch(`${API_URL}/lighting/state`) + .then(res => res.json()) + .then(data => { + setState(data); + setLoading(false); + }) + .catch(() => { + setError('Failed to fetch lighting state'); + setLoading(false); + }); + }, []); + + // 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; + + 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); + updateLighting({ + power: state.power, // always preserve current power state + red: rgb.red, + green: rgb.green, + blue: rgb.blue + }); + }; + + 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); + }); + }; + + // Guard against undefined values + const colorPreview = `rgb(${state.red ?? 0},${state.green ?? 0},${state.blue ?? 0})`; + + + return ( +
+
+

Lighting Controls

+ +
+ Power +