diff --git a/frontend/apps/web/src/app/authenticated-layout/AuthenticatedLayout.tsx b/frontend/apps/web/src/app/authenticated-layout/AuthenticatedLayout.tsx index 56ee4b7..95c8e30 100644 --- a/frontend/apps/web/src/app/authenticated-layout/AuthenticatedLayout.tsx +++ b/frontend/apps/web/src/app/authenticated-layout/AuthenticatedLayout.tsx @@ -2,7 +2,6 @@ import * as React from "react"; import { selectIsAuthenticated } from "@abrechnung/redux"; import { Link as RouterLink, Navigate, Outlet, useLocation, useParams } from "react-router-dom"; import { selectAuthSlice, useAppSelector } from "@/store"; -import { useRecoilValue } from "recoil"; import { ListItemLink } from "@/components/style/ListItemLink"; import { SidebarGroupList } from "@/app/authenticated-layout/SidebarGroupList"; import { @@ -40,13 +39,13 @@ import { Paid, People, } from "@mui/icons-material"; -import { config } from "@/state/config"; import { useTheme } from "@mui/material/styles"; import { Banner } from "@/components/style/Banner"; import { Loading } from "@/components/style/Loading"; import styles from "./AuthenticatedLayout.module.css"; import { LanguageSelect } from "@/components/LanguageSelect"; import { useTranslation } from "react-i18next"; +import { useConfig } from "@/core/config"; const drawerWidth = 240; const AUTH_FALLBACK = "/login"; @@ -60,7 +59,7 @@ export const AuthenticatedLayout: React.FC = () => { const [anchorEl, setAnchorEl] = React.useState(null); const theme: Theme = useTheme(); const dotsMenuOpen = Boolean(anchorEl); - const cfg = useRecoilValue(config); + const cfg = useConfig(); const isLargeScreen = useMediaQuery(theme.breakpoints.up("sm")); const [mobileOpen, setMobileOpen] = React.useState(true); diff --git a/frontend/apps/web/src/components/style/Banner.tsx b/frontend/apps/web/src/components/style/Banner.tsx index 46e7fea..f2748ff 100644 --- a/frontend/apps/web/src/components/style/Banner.tsx +++ b/frontend/apps/web/src/components/style/Banner.tsx @@ -1,10 +1,9 @@ import React from "react"; import { Alert, AlertTitle } from "@mui/material"; -import { useRecoilValue } from "recoil"; -import { config } from "@/state/config"; +import { useConfig } from "@/core/config"; export const Banner: React.FC = () => { - const cfg = useRecoilValue(config); + const cfg = useConfig(); if (cfg.error) { return ( @@ -23,5 +22,3 @@ export const Banner: React.FC = () => { ); }; - -export default Banner; diff --git a/frontend/apps/web/src/core/config.tsx b/frontend/apps/web/src/core/config.tsx new file mode 100644 index 0000000..234d2a4 --- /dev/null +++ b/frontend/apps/web/src/core/config.tsx @@ -0,0 +1,92 @@ +import * as React from "react"; +import { z } from "zod"; +import { environment } from "@/environments/environment"; +import { AlertColor } from "@mui/material/Alert/Alert"; +import Loading from "@/components/style/Loading"; +import { Alert, AlertTitle } from "@mui/material"; + +const configSchema = z.object({ + imprintURL: z.string().optional().nullable(), + sourceCodeURL: z.string().optional(), + issueTrackerURL: z.string().optional().nullable(), + messages: z + .array( + z.object({ + type: z.union([z.literal("info"), z.literal("error"), z.literal("warning"), z.literal("success")]), + title: z.string().default(null).nullable(), + body: z.string(), + }) + ) + .optional(), + error: z.string().optional(), +}); + +export interface StatusMessage { + type: AlertColor; + title: string | null; + body: string; +} + +export type Config = z.infer; + +const ConfigContext = React.createContext(null as Config); + +export type ConfigProviderProps = { + children: React.ReactNode; +}; + +type ConfigState = + | { + state: "loaded"; + config: Config; + } + | { + state: "loading"; + } + | { + state: "error"; + error: string; + }; + +const invalidConfigurationMessage = "This frontend is configured incorrectly, please contact your server administrator"; + +export const ConfigProvider: React.FC = ({ children }) => { + const [state, setState] = React.useState({ state: "loading" }); + + React.useEffect(() => { + const fetcher = async () => { + try { + const path = environment.production ? "/config.json" : "/assets/config.json"; + const resp = await fetch(path, { headers: { "Content-Type": "application/json" } }); + const data = await resp.json(); + const parsed = configSchema.safeParse(data); + if (parsed.success) { + setState({ state: "loaded", config: parsed.data }); + } else { + setState({ state: "error", error: invalidConfigurationMessage }); + } + } catch (e) { + setState({ state: "error", error: invalidConfigurationMessage }); + } + }; + fetcher(); + }, []); + + if (state.state === "loading") { + return ; + } + + if (state.state === "error") { + return ( + + Error loading config: {state.error} + + ); + } + + return {children}; +}; + +export const useConfig = () => { + return React.useContext(ConfigContext); +}; diff --git a/frontend/apps/web/src/main.tsx b/frontend/apps/web/src/main.tsx index 36df8b2..8f27db1 100644 --- a/frontend/apps/web/src/main.tsx +++ b/frontend/apps/web/src/main.tsx @@ -1,24 +1,24 @@ import React from "react"; import * as ReactDOM from "react-dom/client"; import { Provider } from "react-redux"; -import { RecoilRoot } from "recoil"; import { PersistGate } from "redux-persist/integration/react"; import { App } from "./app/app"; import { Loading } from "./components/style/Loading"; import "./i18n"; import { persistor, store } from "./store"; +import { ConfigProvider } from "./core/config"; const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement); root.render( - - - } persistor={persistor}> - }> + + } persistor={persistor}> + }> + - - - - + + + + ); diff --git a/frontend/apps/web/src/state/config.ts b/frontend/apps/web/src/state/config.ts deleted file mode 100644 index 7cf714d..0000000 --- a/frontend/apps/web/src/state/config.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { selector } from "recoil"; -import axios from "axios"; -import { z } from "zod"; -import DevConfig from "../assets/config.json"; -import { environment } from "@/environments/environment"; -import { AlertColor } from "@mui/material/Alert/Alert"; - -const configSchema = z.object({ - imprintURL: z.string().optional().nullable(), - sourceCodeURL: z.string().optional(), - issueTrackerURL: z.string().optional().nullable(), - messages: z - .array( - z.object({ - type: z.union([z.literal("info"), z.literal("error"), z.literal("warning"), z.literal("success")]), - title: z.string().default(null).nullable(), - body: z.string(), - }) - ) - .optional(), - error: z.string().optional(), -}); - -export interface StatusMessage { - type: AlertColor; - title: string | null; - body: string; -} - -export type Config = z.infer; - -export const config = selector({ - key: "config", - get: async ({ get }) => { - if (!environment.production) { - return DevConfig as Config; - } - - try { - const resp = await axios.get("/config.json", { - headers: { "Content-Type": "application/json" }, - }); - try { - return await configSchema.parseAsync(await resp.data); - } catch (e) { - console.log(e); - return { - error: "This frontend is configured incorrectly, please contact your server administrator", - }; - } - } catch { - return { - error: "This frontend is configured incorrectly, please contact your server administrator", - }; - } - }, -}); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 28fb1aa..f5c31df 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -62,7 +62,6 @@ "react-router-dom": "6.21.1", "react-toastify": "^9.1.3", "recharts": "^2.10.3", - "recoil": "^0.7.7", "redux-persist": "^6.0.0", "redux-persist-filesystem-storage": "^4.2.0", "regenerator-runtime": "0.14.1", @@ -19346,11 +19345,6 @@ "graphql": "^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" } }, - "node_modules/hamt_plus": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/hamt_plus/-/hamt_plus-1.0.2.tgz", - "integrity": "sha512-t2JXKaehnMb9paaYA7J0BX8QQAY8lwfQ9Gjf4pg/mk4krt+cmwmU652HOoWonf+7+EQV97ARPMhhVgU1ra2GhA==" - }, "node_modules/handle-thing": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", @@ -29217,25 +29211,6 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, - "node_modules/recoil": { - "version": "0.7.7", - "resolved": "https://registry.npmjs.org/recoil/-/recoil-0.7.7.tgz", - "integrity": "sha512-8Og5KPQW9LwC577Vc7Ug2P0vQshkv1y3zG3tSSkWMqkWSwHmE+by06L8JtnGocjW6gcCvfwB3YtrJG6/tWivNQ==", - "dependencies": { - "hamt_plus": "1.0.2" - }, - "peerDependencies": { - "react": ">=16.13.1" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - }, - "react-native": { - "optional": true - } - } - }, "node_modules/redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 75271a9..61b4771 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -62,7 +62,6 @@ "react-router-dom": "6.21.1", "react-toastify": "^9.1.3", "recharts": "^2.10.3", - "recoil": "^0.7.7", "redux-persist": "^6.0.0", "redux-persist-filesystem-storage": "^4.2.0", "regenerator-runtime": "0.14.1",