removed idea folder
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
import {NextResponse} from "next/server";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
export async function GET() {
|
||||
const configPath = process.env.CONFIG_PATH || "./config.json";
|
||||
const absolutePath = path.resolve(configPath);
|
||||
|
||||
if (!fs.existsSync(absolutePath)) {
|
||||
return NextResponse.json({error: "Config file not found"}, {status: 404});
|
||||
}
|
||||
|
||||
const fileContent = fs.readFileSync(absolutePath, "utf-8");
|
||||
const config = JSON.parse(fileContent);
|
||||
|
||||
return NextResponse.json(config);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
"use client";
|
||||
|
||||
import {useEffect, useState} from "react";
|
||||
import {fetchConfig} from "@/app/utils/fetchConfig";
|
||||
import InputControl from "@/app/components/InputControl";
|
||||
|
||||
const BusControl = ({busControlConfig, mainConfig}) => {
|
||||
|
||||
const [appConfig, setAppConfig] = useState(null);
|
||||
const busId = busControlConfig.id;
|
||||
const busChannel = busControlConfig.bus
|
||||
|
||||
useEffect(() => {
|
||||
if (mainConfig) {
|
||||
setAppConfig(mainConfig);
|
||||
} else {
|
||||
fetchConfig().then(config => {
|
||||
setAppConfig(config);
|
||||
}
|
||||
);
|
||||
}
|
||||
}, [mainConfig]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{!appConfig ? <p>Loading...</p> : (
|
||||
<div className={"p-2 rounded-md border border-gray-250"}>
|
||||
<div className={"mb-2 pb-3 border-b border-b-gray-250"}>
|
||||
<InputControl
|
||||
id={`bus-${busId}-main`}
|
||||
name={`Main Level`}
|
||||
basePath={`/bus/${busChannel.toString().padStart(2, "0")}/mix`}
|
||||
appConfig={appConfig} faderControlSuffix={undefined} muteControlSuffix={undefined} />
|
||||
</div>
|
||||
<div className={"flex flex-col gap-3"}>
|
||||
<h3>Inputs</h3>
|
||||
{appConfig.inputs.map((input) => (
|
||||
<InputControl
|
||||
key={input.id}
|
||||
id={`bus-${busId}-input-${input.id}`}
|
||||
name={input.name}
|
||||
faderControlSuffix={"level"}
|
||||
basePath={`/ch/${input.channels[0].toString().padStart(2, "0")}/mix/${busChannel.toString().padStart(2, "0")}`}
|
||||
appConfig={appConfig} muteControlSuffix={undefined} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BusControl;
|
||||
@@ -0,0 +1,85 @@
|
||||
import React, {useEffect, useMemo, useState} from "react";
|
||||
import {fetchConfig} from "@/app/utils/fetchConfig";
|
||||
import BusControl from "@/app/components/BusControl";
|
||||
import {Config} from "@/app/types/config";
|
||||
|
||||
const BusControlTabs = () => {
|
||||
const [busControlConfigs, setBusControlConfigs] = useState([]);
|
||||
const [activeTab, setActiveTab] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchConfig().then((config: Config) => {
|
||||
if (config) {
|
||||
setBusControlConfigs(config.bus_controls);
|
||||
// create tab components from bus control configs
|
||||
const components = config.bus_controls.map((busControl) => {
|
||||
return {
|
||||
id: busControl.id,
|
||||
label: busControl.name,
|
||||
content: <BusControl busControlConfig={busControl} mainConfig={config}/>
|
||||
};
|
||||
});
|
||||
// setTabComponents(components);
|
||||
|
||||
// set the active tab to the first tab
|
||||
if (components.length > 0) {
|
||||
setActiveTab(components[0].id);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}, []);
|
||||
|
||||
const tabComponents = useMemo(() => {
|
||||
return busControlConfigs.map((busControl) => ({
|
||||
id: busControl.id,
|
||||
label: busControl.name,
|
||||
content: (
|
||||
<BusControl
|
||||
key={busControl.id}
|
||||
busControlConfig={busControl} mainConfig={undefined}
|
||||
/>
|
||||
),
|
||||
}));
|
||||
}, [busControlConfigs]);
|
||||
|
||||
|
||||
if (!tabComponents.length) {
|
||||
return <p>Loading...</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Tabs Header */}
|
||||
<div className="flex border-b mb-4">
|
||||
{tabComponents.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
className={`px-4 py-2 ${
|
||||
activeTab === tab.id
|
||||
? "border-b-2 border-blue-500 text-blue-500"
|
||||
: "text-gray-500"
|
||||
}`}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Tabs Content */}
|
||||
<div>
|
||||
{tabComponents.map((tab) => (
|
||||
<div
|
||||
key={tab.id}
|
||||
style={{display: activeTab === tab.id ? "block" : "none"}}
|
||||
>
|
||||
{tab.content}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BusControlTabs;
|
||||
@@ -0,0 +1,64 @@
|
||||
"use client";
|
||||
|
||||
import {useState, useEffect} from "react";
|
||||
import {Slider} from "@/components/ui/slider"
|
||||
import {fetchValue, sendValue} from "@/app/lib/x32CommunicationManager";
|
||||
|
||||
|
||||
interface FaderProps {
|
||||
id: string;
|
||||
path: string;
|
||||
pollingInterval: number;
|
||||
}
|
||||
|
||||
const Fader: React.FC<FaderProps> = ({id, path, pollingInterval}) => {
|
||||
|
||||
const [faderValue, setFaderValue] = useState(0); // Values for each path
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const updateCurrentFaderValue = async () => {
|
||||
fetchValue(path).then((value) => {
|
||||
if (value !== faderValue) {
|
||||
setFaderValue(value);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
// Initial fetch and polling
|
||||
updateCurrentFaderValue();
|
||||
|
||||
// add random interval to avoid all clients polling at the same time
|
||||
const randomInterval = Math.random() * pollingInterval;
|
||||
setTimeout(() => null, randomInterval);
|
||||
|
||||
const interval = setInterval(() => {
|
||||
updateCurrentFaderValue();
|
||||
}, pollingInterval);
|
||||
|
||||
return () => clearInterval(interval); // Cleanup interval
|
||||
}, [path, pollingInterval]);
|
||||
|
||||
|
||||
const handleFaderChange = async (path: string, newValue: number) => {
|
||||
setFaderValue(newValue)
|
||||
sendValue(path, newValue); // Send new value
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<Slider
|
||||
id={`${id}-${path}-fader`}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
value={faderValue ? [faderValue,] : [0,]}
|
||||
onValueChange={(newValue) => handleFaderChange(path, newValue[0])}
|
||||
style={{
|
||||
background: `linear-gradient(to right, #4ade80 75%, #ef4444 75%)`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Fader;
|
||||
@@ -0,0 +1,45 @@
|
||||
import Fader from "@/app/components/Fader";
|
||||
import MuteButton from "@/app/components/MuteButton";
|
||||
|
||||
|
||||
const InputControl = ({
|
||||
id,
|
||||
name,
|
||||
basePath,
|
||||
faderControlSuffix,
|
||||
muteControlSuffix,
|
||||
appConfig
|
||||
}) => {
|
||||
|
||||
if (faderControlSuffix === undefined) {
|
||||
faderControlSuffix = "fader";
|
||||
}
|
||||
if (muteControlSuffix === undefined) {
|
||||
muteControlSuffix = "on";
|
||||
}
|
||||
|
||||
const faderControlPath = `${basePath}/${faderControlSuffix}`;
|
||||
const muteControlPath = `${basePath}/${muteControlSuffix}`;
|
||||
|
||||
return (
|
||||
<div className={"flex gap-5 w-auto"}>
|
||||
<div className={"min-w-24"}>
|
||||
<MuteButton
|
||||
key={`inputControl-${id}-mute`}
|
||||
id={`inputControl-${id}-mute`}
|
||||
label={name}
|
||||
path={muteControlPath}
|
||||
pollingInterval={appConfig.polling_interval}
|
||||
/>
|
||||
</div>
|
||||
<Fader
|
||||
key={`inputControl-${id}`}
|
||||
id={`inputControl-${id}`}
|
||||
path={faderControlPath}
|
||||
pollingInterval={appConfig.polling_interval}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default InputControl;
|
||||
@@ -0,0 +1,42 @@
|
||||
import {useEffect, useState} from "react";
|
||||
import {fetchConfig} from "@/app/utils/fetchConfig";
|
||||
import InputControl from "@/app/components/InputControl";
|
||||
|
||||
const MainMix = () => {
|
||||
|
||||
const [appConfig, setAppConfig] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchConfig().then(config => {
|
||||
setAppConfig(config);
|
||||
});
|
||||
}, []);
|
||||
|
||||
|
||||
if (!appConfig) {
|
||||
return <p> Loading...</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={"p-2 rounded-md border border-gray-250"}>
|
||||
<div className={"mb-3 pb-3 border-b border-b-gray-250"}>
|
||||
<InputControl id={"mainMix"} name={"Main Level"} basePath={"/main/st/mix"} appConfig={appConfig}
|
||||
faderControlSuffix={undefined} muteControlSuffix={undefined}/>
|
||||
</div>
|
||||
<div className={"flex flex-col gap-5"}>
|
||||
{appConfig.inputs.map((input) => (
|
||||
<InputControl
|
||||
key={input.id}
|
||||
id={input.id}
|
||||
name={input.name}
|
||||
basePath={`/ch/${input.channels[0].toString().padStart(2, "0")}/mix`}
|
||||
appConfig={appConfig} faderControlSuffix={undefined} muteControlSuffix={undefined} />
|
||||
))
|
||||
}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MainMix;
|
||||
@@ -0,0 +1,57 @@
|
||||
import {useEffect, useState} from "react";
|
||||
import {Button} from "@/components/ui/button";
|
||||
|
||||
import {fetchValue, sendValue} from "@/app/lib/x32CommunicationManager";
|
||||
|
||||
const MuteButton = ({id, label, path, pollingInterval}) => {
|
||||
|
||||
|
||||
const [isMuted, setIsMuted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
const updateCurrentMuteValue = async () => {
|
||||
fetchValue(path).then((value) => {
|
||||
if (value !== isMuted) {
|
||||
setIsMuted(value);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
// Initial fetch and polling
|
||||
updateCurrentMuteValue();
|
||||
|
||||
// add random interval to avoid all clients polling at the same time
|
||||
const randomInterval = Math.random() * pollingInterval;
|
||||
setTimeout(() => null, randomInterval);
|
||||
|
||||
const interval = setInterval(() => {
|
||||
updateCurrentMuteValue();
|
||||
}, pollingInterval);
|
||||
|
||||
return () => clearInterval(interval); // Cleanup interval
|
||||
}, [path, pollingInterval]);
|
||||
|
||||
|
||||
const toggleMute = () => {
|
||||
const newIsMuted = !isMuted;
|
||||
|
||||
// disable or enable the input for each channel
|
||||
sendValue(
|
||||
path,
|
||||
newIsMuted ? 1 : 0,
|
||||
);
|
||||
|
||||
setIsMuted(newIsMuted);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button key={`${id}-${path}-mute`}
|
||||
className={`w-24 ${isMuted ? 'bg-green-500' : 'bg-red-500'}`}
|
||||
onClick={toggleMute}>
|
||||
{label}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export default MuteButton;
|
||||
+61
-10
@@ -2,20 +2,71 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
body {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
@layer base {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 240 10% 3.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 240 10% 3.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 240 10% 3.9%;
|
||||
--primary: 240 5.9% 10%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--secondary: 240 4.8% 95.9%;
|
||||
--secondary-foreground: 240 5.9% 10%;
|
||||
--muted: 240 4.8% 95.9%;
|
||||
--muted-foreground: 240 3.8% 46.1%;
|
||||
--accent: 240 4.8% 95.9%;
|
||||
--accent-foreground: 240 5.9% 10%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 240 5.9% 90%;
|
||||
--input: 240 5.9% 90%;
|
||||
--ring: 240 10% 3.9%;
|
||||
--chart-1: 12 76% 61%;
|
||||
--chart-2: 173 58% 39%;
|
||||
--chart-3: 197 37% 24%;
|
||||
--chart-4: 43 74% 66%;
|
||||
--chart-5: 27 87% 67%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
.dark {
|
||||
--background: 240 10% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
--card: 240 10% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--popover: 240 10% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
--primary: 0 0% 98%;
|
||||
--primary-foreground: 240 5.9% 10%;
|
||||
--secondary: 240 3.7% 15.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--muted: 240 3.7% 15.9%;
|
||||
--muted-foreground: 240 5% 64.9%;
|
||||
--accent: 240 3.7% 15.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 240 3.7% 15.9%;
|
||||
--input: 240 3.7% 15.9%;
|
||||
--ring: 240 4.9% 83.9%;
|
||||
--chart-1: 220 70% 50%;
|
||||
--chart-2: 160 60% 45%;
|
||||
--chart-3: 30 80% 55%;
|
||||
--chart-4: 280 65% 60%;
|
||||
--chart-5: 340 75% 55%;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
color: var(--foreground);
|
||||
background: var(--background);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import dotenv from "dotenv";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
// A global manager (no React hooks)
|
||||
let ws: WebSocket | null = null;
|
||||
let isConnected = false;
|
||||
|
||||
// Your choice: store responses in memory, or just call callbacks
|
||||
let messageCallbacks: Array<(msg: string) => void> = [];
|
||||
|
||||
|
||||
export function initConnection(wsUrl: string) {
|
||||
if (ws) return; // Already connected or connecting
|
||||
|
||||
console.log("Connecting to WebSocket:", wsUrl);
|
||||
|
||||
ws = new WebSocket(wsUrl);
|
||||
ws.onopen = () => {
|
||||
console.log("Global WebSocket connected!");
|
||||
isConnected = true;
|
||||
};
|
||||
ws.onclose = () => {
|
||||
console.log("Global WebSocket disconnected.");
|
||||
isConnected = false;
|
||||
ws = null; // So we can reconnect if needed
|
||||
};
|
||||
ws.onerror = (err) => {
|
||||
console.error("Global WebSocket error:", err);
|
||||
};
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const message = JSON.parse(event.data);
|
||||
// Notify any registered callbacks
|
||||
messageCallbacks.forEach((cb) => cb(message));
|
||||
} catch (err) {
|
||||
console.error("Error parsing WebSocket message:", err);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function registerMessageCallback(cb: (msg: any) => void) {
|
||||
messageCallbacks.push(cb);
|
||||
}
|
||||
|
||||
export function removeMessageCallback(cb: (response: any) => void) {
|
||||
messageCallbacks = messageCallbacks.filter((f) => f !== cb);
|
||||
}
|
||||
|
||||
// Send a message to the X32
|
||||
export function sendMessage(msg: any) {
|
||||
if (ws && isConnected) {
|
||||
ws.send(JSON.stringify(msg));
|
||||
} else {
|
||||
console.error("WebSocket not connected. Message not sent:", msg);
|
||||
}
|
||||
}
|
||||
|
||||
export function fetchValue(path: string): Promise<any> {
|
||||
return new Promise((resolve) => {
|
||||
sendMessage({action: "getValue", path});
|
||||
|
||||
// We'll listen for the matching response just once
|
||||
const callback = (response: any) => {
|
||||
if (response.action === "response" && response.path === path) {
|
||||
removeMessageCallback(callback);
|
||||
resolve(response.value);
|
||||
}
|
||||
};
|
||||
|
||||
registerMessageCallback(callback);
|
||||
});
|
||||
}
|
||||
|
||||
export function sendValue(path: string, value: any) {
|
||||
sendMessage({action: "sendValue", path, value});
|
||||
}
|
||||
+79
-98
@@ -1,101 +1,82 @@
|
||||
import Image from "next/image";
|
||||
"use client";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
|
||||
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/next.svg"
|
||||
alt="Next.js logo"
|
||||
width={180}
|
||||
height={38}
|
||||
priority
|
||||
/>
|
||||
<ol className="list-inside list-decimal text-sm text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
|
||||
<li className="mb-2">
|
||||
Get started by editing{" "}
|
||||
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-semibold">
|
||||
src/app/page.tsx
|
||||
</code>
|
||||
.
|
||||
</li>
|
||||
<li>Save and see your changes instantly.</li>
|
||||
</ol>
|
||||
import {useEffect, useMemo, useState} from "react";
|
||||
import {fetchConfig} from "@/app/utils/fetchConfig";
|
||||
import BusControlTabs from "@/app/components/BusControlTabs";
|
||||
import MainMix from "@/app/components/MainMix";
|
||||
|
||||
<div className="flex gap-4 items-center flex-col sm:flex-row">
|
||||
<a
|
||||
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5"
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/vercel.svg"
|
||||
alt="Vercel logomark"
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
Deploy now
|
||||
</a>
|
||||
<a
|
||||
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:min-w-44"
|
||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Read our docs
|
||||
</a>
|
||||
// initialize global websocket connection
|
||||
import {initConnection} from "@/app/lib/x32CommunicationManager";
|
||||
|
||||
const Page = () => {
|
||||
const [appConfig, setAppConfig] = useState(null);
|
||||
const [activeTab, setActiveTab] = useState("mainMix");
|
||||
|
||||
useEffect(() => {
|
||||
initConnection(`ws://${window.location.hostname}:8080`);
|
||||
fetchConfig().then(config => {
|
||||
setAppConfig(config);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const tabComponents = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
id: "mainMix",
|
||||
label: "Main Mix",
|
||||
content: (
|
||||
<MainMix/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "busControl",
|
||||
label: "Bus Control",
|
||||
content: (
|
||||
<BusControlTabs/>
|
||||
),
|
||||
}
|
||||
]
|
||||
}
|
||||
, []);
|
||||
|
||||
if (!appConfig) {
|
||||
return <p>Error fetching config</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="p-4">
|
||||
{/* Tabs Header */}
|
||||
<div className="flex border-b mb-4">
|
||||
{tabComponents.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
className={`px-4 py-2 ${
|
||||
activeTab === tab.id
|
||||
? "border-b-2 border-blue-500 text-blue-500"
|
||||
: "text-gray-500"
|
||||
}`}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Tabs Content */}
|
||||
<div>
|
||||
{tabComponents.map((tab) => (
|
||||
<div
|
||||
key={tab.id}
|
||||
style={{display: activeTab === tab.id ? "block" : "none"}}
|
||||
>
|
||||
{tab.content}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<footer className="row-start-3 flex gap-6 flex-wrap items-center justify-center">
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/file.svg"
|
||||
alt="File icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Learn
|
||||
</a>
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/window.svg"
|
||||
alt="Window icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Examples
|
||||
</a>
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/globe.svg"
|
||||
alt="Globe icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Go to nextjs.org →
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
export interface Config {
|
||||
polling_interval: number
|
||||
inputs: any
|
||||
bus_controls: any
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
export const fetchConfig = async <T>(): Promise<T | null> => {
|
||||
try {
|
||||
const response = await fetch("/api/config");
|
||||
if (!response.ok) {
|
||||
console.error(`Failed to fetch config:`, response.statusText);
|
||||
return null;
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error(`Error fetching config from:`, error);
|
||||
return null;
|
||||
}
|
||||
|
||||
};
|
||||
Reference in New Issue
Block a user