import { useCallback, useEffect, useReducer, useState } from "react"; import { Navbar, Form, Offcanvas, Container } from "react-bootstrap"; import { authority, socket, forwardedProto } from "./socket"; import "./App.css"; import { Notifications, Notification, LevelName, } from "./components/NotificationsComponent"; import { ConnectionToast } from "./components/ConnectionToast"; import { setNestedValueByPath, State } from "./utils/stateUtils"; import { WebSettingsContext, WebSetting } from "./WebSettings"; import { GenericComponent } from "./components/GenericComponent"; import { SerializedObject } from "./types/SerializedObject"; import useLocalStorage from "./hooks/useLocalStorage"; type Action = | { type: "SET_DATA"; data: State } | { type: "UPDATE_ATTRIBUTE"; fullAccessPath: string; newValue: SerializedObject; }; interface UpdateMessage { data: { full_access_path: string; value: SerializedObject }; } interface LogMessage { levelname: LevelName; message: string; } const reducer = (state: State | null, action: Action): State | null => { switch (action.type) { case "SET_DATA": return action.data; case "UPDATE_ATTRIBUTE": { if (state === null) { return null; } return { ...state, value: setNestedValueByPath( state.value as Record, action.fullAccessPath, action.newValue, ), }; } default: throw new Error(); } }; const App = () => { const [state, dispatch] = useReducer(reducer, null); const [serviceName, setServiceName] = useState(null); const [webSettings, setWebSettings] = useState>({}); const [isInstantUpdate, setIsInstantUpdate] = useLocalStorage( "isInstantUpdate", false, ); const [showSettings, setShowSettings] = useState(false); const [showNotification, setShowNotification] = useLocalStorage( "showNotification", false, ); const [notifications, setNotifications] = useState([]); const [connectionStatus, setConnectionStatus] = useState("connecting"); useEffect(() => { // Allow the user to add a custom css file fetch(`${forwardedProto}://${authority}/custom.css`, { credentials: "include" }) .then((response) => { if (response.ok) { // If the file exists, create a link element for the custom CSS const link = document.createElement("link"); link.href = `${forwardedProto}://${authority}/custom.css`; link.type = "text/css"; link.rel = "stylesheet"; document.head.appendChild(link); } }) .catch(console.error); // Handle the error appropriately socket.on("connect", () => { // Fetch data from the API when the client connects fetch(`${forwardedProto}://${authority}/service-properties`, { credentials: "include", }) .then((response) => response.json()) .then((data: State) => { dispatch({ type: "SET_DATA", data }); setServiceName(data.name); document.title = data.name; // Setting browser tab title }); fetch(`${forwardedProto}://${authority}/web-settings`, { credentials: "include" }) .then((response) => response.json()) .then((data: Record) => setWebSettings(data)); setConnectionStatus("connected"); }); socket.on("disconnect", () => { setConnectionStatus("disconnected"); setTimeout(() => { // Only set "reconnecting" is the state is still "disconnected" // E.g. when the client has already reconnected setConnectionStatus((currentState) => currentState === "disconnected" ? "reconnecting" : currentState, ); }, 2000); }); socket.on("notify", onNotify); socket.on("log", onLogMessage); return () => { socket.off("notify", onNotify); socket.off("log", onLogMessage); }; }, []); // Adding useCallback to prevent notify to change causing a re-render of all // components const addNotification = useCallback( (message: string, levelname: LevelName = "DEBUG") => { // Getting the current time in the required format const timeStamp = new Date().toISOString().substring(11, 19); // Adding an id to the notification to provide a way of removing it const id = Math.random(); // Custom logic for notifications setNotifications((prevNotifications) => [ { levelname, id, message, timeStamp }, ...prevNotifications, ]); }, [], ); const removeNotificationById = (id: number) => { setNotifications((prevNotifications) => prevNotifications.filter((n) => n.id !== id), ); }; const handleCloseSettings = () => setShowSettings(false); const handleShowSettings = () => setShowSettings(true); function onNotify(value: UpdateMessage) { // Extracting data from the notification const { full_access_path: fullAccessPath, value: newValue } = value.data; // Dispatching the update to the reducer dispatch({ type: "UPDATE_ATTRIBUTE", fullAccessPath, newValue, }); } function onLogMessage(value: LogMessage) { addNotification(value.message, value.levelname); } // While the data is loading if (!state) { return ; } return ( <> {serviceName} Settings setIsInstantUpdate(e.target.checked)} type="switch" label="Enable Instant Update" /> setShowNotification(e.target.checked)} type="switch" label="Show Notifications" /> {/* Add any additional controls you want here */}
); }; export default App;