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 (
+
+
+ );
+}
+
+// Helper functions for color conversion
+function rgbToHex(r, g, b) {
+ r = typeof r === 'number' && !isNaN(r) ? r : 0;
+ g = typeof g === 'number' && !isNaN(g) ? g : 0;
+ b = typeof b === 'number' && !isNaN(b) ? b : 0;
+ return (
+ '#' +
+ [r, g, b]
+ .map(x => {
+ const hex = x.toString(16);
+ return hex.length === 1 ? '0' + hex : hex;
+ })
+ .join('')
+ );
+}
+
+function hexToRgb(hex) {
+ hex = hex.replace('#', '');
+ if (hex.length === 3) {
+ hex = hex.split('').map(x => x + x).join('');
+ }
+ const num = parseInt(hex, 16);
+ return {
+ red: (num >> 16) & 255,
+ green: (num >> 8) & 255,
+ blue: num & 255,
+ };
+}
diff --git a/src/config.js b/src/config.js
index e94fbe4..bae25c2 100644
--- a/src/config.js
+++ b/src/config.js
@@ -9,6 +9,8 @@ export const metaData = {
};
export const API_URL = "https://api.codey.lol";
+export const RADIO_API_URL = "https://radio-api.codey.lol";
+
export const socialLinks = {
};
diff --git a/src/layouts/Nav.astro b/src/layouts/Nav.astro
index 3578950..0ff2062 100644
--- a/src/layouts/Nav.astro
+++ b/src/layouts/Nav.astro
@@ -7,6 +7,7 @@ const navItems = [
{ label: "Home", href: "/" },
{ label: "Radio", href: "/radio" },
{ label: "Memes", href: "/memes" },
+ { label: "Lighting", href: "/lighting", auth: true },
{ label: "TRip", href: "/TRip", auth: true },
{ label: "Status", href: "https://status.boatson.boats", icon: ExitToApp },
// { label: "Git", href: "https://kode.boatson.boats", icon: ExitToApp },
diff --git a/src/pages/lighting.astro b/src/pages/lighting.astro
new file mode 100644
index 0000000..8205387
--- /dev/null
+++ b/src/pages/lighting.astro
@@ -0,0 +1,20 @@
+---
+import Base from "@/layouts/Base.astro";
+import Root from "@/components/AppLayout.jsx";
+import { requireAuthHook } from "@/hooks/requireAuthHook";
+
+const user = await requireAuthHook(Astro);
+
+if (!user) {
+ return Astro.redirect('/login');
+}
+
+---
+
+
+
+