diff --git a/central_frontend/package-lock.json b/central_frontend/package-lock.json index b5c7739..a973ea2 100644 --- a/central_frontend/package-lock.json +++ b/central_frontend/package-lock.json @@ -11,10 +11,13 @@ "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", "@fontsource/roboto": "^5.0.13", + "@mdi/js": "^7.4.47", + "@mdi/react": "^1.6.1", "@mui/icons-material": "^5.15.21", "@mui/material": "^5.15.21", "react": "^18.3.1", - "react-dom": "^18.3.1" + "react-dom": "^18.3.1", + "react-router-dom": "^6.24.0" }, "devDependencies": { "@types/react": "^18.3.3", @@ -1138,6 +1141,19 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@mdi/js": { + "version": "7.4.47", + "resolved": "https://registry.npmjs.org/@mdi/js/-/js-7.4.47.tgz", + "integrity": "sha512-KPnNOtm5i2pMabqZxpUz7iQf+mfrYZyKCZ8QNz85czgEt7cuHcGorWfdzUMWYA0SD+a6Hn4FmJ+YhzzzjkTZrQ==" + }, + "node_modules/@mdi/react": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@mdi/react/-/react-1.6.1.tgz", + "integrity": "sha512-4qZeDcluDFGFTWkHs86VOlHkm6gnKaMql13/gpIcUQ8kzxHgpj31NuCkD8abECVfbULJ3shc7Yt4HJ6Wu6SN4w==", + "dependencies": { + "prop-types": "^15.7.2" + } + }, "node_modules/@mui/base": { "version": "5.0.0-beta.40", "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.40.tgz", @@ -1427,6 +1443,14 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@remix-run/router": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.17.0.tgz", + "integrity": "sha512-2D6XaHEVvkCn682XBnipbJjgZUU7xjLtA4dGJRBVUKpEaDYOZMENZoZjAOSb7qirxt5RupjzZxz4fK2FO+EFPw==", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.18.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz", @@ -3449,6 +3473,36 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "6.24.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.24.0.tgz", + "integrity": "sha512-sQrgJ5bXk7vbcC4BxQxeNa5UmboFm35we1AFK0VvQaz9g0LzxEIuLOhHIoZ8rnu9BO21ishGeL9no1WB76W/eg==", + "dependencies": { + "@remix-run/router": "1.17.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.24.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.24.0.tgz", + "integrity": "sha512-960sKuau6/yEwS8e+NVEidYQb1hNjAYM327gjEyXlc6r3Skf2vtwuJ2l7lssdegD2YjoKG5l8MsVyeTDlVeY8g==", + "dependencies": { + "@remix-run/router": "1.17.0", + "react-router": "6.24.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", diff --git a/central_frontend/package.json b/central_frontend/package.json index 4dff39d..71a6436 100644 --- a/central_frontend/package.json +++ b/central_frontend/package.json @@ -13,10 +13,13 @@ "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", "@fontsource/roboto": "^5.0.13", + "@mdi/js": "^7.4.47", + "@mdi/react": "^1.6.1", "@mui/icons-material": "^5.15.21", "@mui/material": "^5.15.21", "react": "^18.3.1", - "react-dom": "^18.3.1" + "react-dom": "^18.3.1", + "react-router-dom": "^6.24.0" }, "devDependencies": { "@types/react": "^18.3.3", diff --git a/central_frontend/src/App.tsx b/central_frontend/src/App.tsx index 5950a66..bdd9c77 100644 --- a/central_frontend/src/App.tsx +++ b/central_frontend/src/App.tsx @@ -1,10 +1,29 @@ +import { + Route, + RouterProvider, + createBrowserRouter, + createRoutesFromElements, +} from "react-router-dom"; import { AuthApi } from "./api/AuthApi"; import { ServerApi } from "./api/ServerApi"; import { LoginRoute } from "./routes/LoginRoute"; +import { NotFoundRoute } from "./routes/NotFoundRoute"; +import { HomeRoute } from "./routes/HomeRoute"; +import { BaseAuthenticatedPage } from "./widgets/BaseAuthenticatedPage"; export function App() { if (!AuthApi.SignedIn && !ServerApi.Config.auth_disabled) return ; - return <>logged in todo; + const router = createBrowserRouter( + createRoutesFromElements( + }> + } /> + + } /> + + ) + ); + + return ; } diff --git a/central_frontend/src/api/AuthApi.ts b/central_frontend/src/api/AuthApi.ts index 653a18b..57af829 100644 --- a/central_frontend/src/api/AuthApi.ts +++ b/central_frontend/src/api/AuthApi.ts @@ -1,7 +1,7 @@ import { APIClient } from "./ApiClient"; export interface AuthInfo { - name: string; + id: string; } const TokenStateKey = "auth-state"; @@ -60,11 +60,15 @@ export class AuthApi { * Sign out */ static async SignOut(): Promise { - await APIClient.exec({ - uri: "/auth/sign_out", - method: "GET", - }); - this.UnsetAuthenticated(); + + try { + await APIClient.exec({ + uri: "/auth/sign_out", + method: "GET", + }); + } finally { + window.location.href = "/"; + } } } diff --git a/central_frontend/src/routes/HomeRoute.tsx b/central_frontend/src/routes/HomeRoute.tsx new file mode 100644 index 0000000..6207f28 --- /dev/null +++ b/central_frontend/src/routes/HomeRoute.tsx @@ -0,0 +1,3 @@ +export function HomeRoute(): React.ReactElement { + return <>home authenticated todo; +} diff --git a/central_frontend/src/routes/NotFoundRoute.tsx b/central_frontend/src/routes/NotFoundRoute.tsx new file mode 100644 index 0000000..72812e6 --- /dev/null +++ b/central_frontend/src/routes/NotFoundRoute.tsx @@ -0,0 +1,23 @@ +import { Button } from "@mui/material"; +import { RouterLink } from "../widgets/RouterLink"; + +export function NotFoundRoute(): React.ReactElement { + return ( +
+

404 Not found

+

The page you requested was not found!

+ + + +
+ ); +} diff --git a/central_frontend/src/widgets/BaseAuthenticatedPage.tsx b/central_frontend/src/widgets/BaseAuthenticatedPage.tsx new file mode 100644 index 0000000..74c6205 --- /dev/null +++ b/central_frontend/src/widgets/BaseAuthenticatedPage.tsx @@ -0,0 +1,82 @@ +import { Box, Button } from "@mui/material"; +import * as React from "react"; +import { Outlet } from "react-router-dom"; +import { AuthApi, AuthInfo } from "../api/AuthApi"; +import { AsyncWidget } from "./AsyncWidget"; +import { SolarEnergyAppBar } from "./SolarEnergyAppBar"; +import { SolarEnergyNavList } from "./SolarEnergyNavList"; + +interface AuthInfoContext { + info: AuthInfo; + reloadAuthInfo: () => void; +} + +const AuthInfoContextK = React.createContext(null); + +export function BaseAuthenticatedPage(): React.ReactElement { + const [authInfo, setAuthInfo] = React.useState(null); + + const signOut = () => { + AuthApi.SignOut(); + }; + + const load = async () => { + setAuthInfo(await AuthApi.GetAuthInfo()); + }; + + return ( + ( + <> + + + )} + build={() => ( + + + theme.palette.mode === "light" + ? theme.palette.grey[100] + : theme.palette.grey[900], + color: (theme) => + theme.palette.mode === "light" + ? theme.palette.grey[900] + : theme.palette.grey[100], + }} + > + + + + +
+ +
+
+
+
+ )} + /> + ); +} + +export function useAuthInfo(): AuthInfoContext { + return React.useContext(AuthInfoContextK)!; +} diff --git a/central_frontend/src/widgets/DarkThemeButton.tsx b/central_frontend/src/widgets/DarkThemeButton.tsx new file mode 100644 index 0000000..cbbcf70 --- /dev/null +++ b/central_frontend/src/widgets/DarkThemeButton.tsx @@ -0,0 +1,19 @@ +import Brightness7Icon from "@mui/icons-material/Brightness7"; +import DarkModeIcon from "@mui/icons-material/DarkMode"; +import { IconButton, Tooltip } from "@mui/material"; +import { useDarkTheme } from "../hooks/context_providers/DarkThemeProvider"; + +export function DarkThemeButton(): React.ReactElement { + const darkTheme = useDarkTheme(); + + return ( + + darkTheme.setEnabled(!darkTheme.enabled)} + style={{ color: "inherit" }} + > + {!darkTheme.enabled ? : } + + + ); +} diff --git a/central_frontend/src/widgets/RouterLink.tsx b/central_frontend/src/widgets/RouterLink.tsx new file mode 100644 index 0000000..108d105 --- /dev/null +++ b/central_frontend/src/widgets/RouterLink.tsx @@ -0,0 +1,16 @@ +import { PropsWithChildren } from "react"; +import { Link } from "react-router-dom"; + +export function RouterLink( + p: PropsWithChildren<{ to: string; target?: React.HTMLAttributeAnchorTarget }> +): React.ReactElement { + return ( + + {p.children} + + ); +} diff --git a/central_frontend/src/widgets/SolarEnergyAppBar.tsx b/central_frontend/src/widgets/SolarEnergyAppBar.tsx new file mode 100644 index 0000000..92c65cf --- /dev/null +++ b/central_frontend/src/widgets/SolarEnergyAppBar.tsx @@ -0,0 +1,85 @@ +import { mdiWhiteBalanceSunny } from "@mdi/js"; +import Icon from "@mdi/react"; +import SettingsIcon from "@mui/icons-material/Settings"; +import { Button } from "@mui/material"; +import AppBar from "@mui/material/AppBar"; +import Menu from "@mui/material/Menu"; +import MenuItem from "@mui/material/MenuItem"; +import Toolbar from "@mui/material/Toolbar"; +import Typography from "@mui/material/Typography"; +import * as React from "react"; +import { useAuthInfo } from "./BaseAuthenticatedPage"; +import { DarkThemeButton } from "./DarkThemeButton"; +import { RouterLink } from "./RouterLink"; + +export function SolarEnergyAppBar(p: { + onSignOut: () => void; +}): React.ReactElement { + const authInfo = useAuthInfo(); + + const [anchorEl, setAnchorEl] = React.useState(null); + const handleMenu = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleCloseMenu = () => { + setAnchorEl(null); + }; + + const signOut = () => { + handleCloseMenu(); + p.onSignOut(); + }; + + return ( + + + + + + Solar Energy + + +
+ + + + + + + Déconnexion + +
+
+
+ ); +} diff --git a/central_frontend/src/widgets/SolarEnergyNavList.tsx b/central_frontend/src/widgets/SolarEnergyNavList.tsx new file mode 100644 index 0000000..355b07c --- /dev/null +++ b/central_frontend/src/widgets/SolarEnergyNavList.tsx @@ -0,0 +1,59 @@ +import { + mdiAccountMultiple, + mdiAccountMusic, + mdiAlbum, + mdiApi, + mdiChartLine, + mdiCog, + mdiHome, + mdiInbox, + mdiMusic, +} from "@mdi/js"; +import Icon from "@mdi/react"; +import { + List, + ListItemButton, + ListItemIcon, + ListItemSecondaryAction, + ListItemText, + ListSubheader, +} from "@mui/material"; +import { useLocation } from "react-router-dom"; +import { useAuthInfo } from "./BaseAuthenticatedPage"; +import { RouterLink } from "./RouterLink"; + +export function SolarEnergyNavList(): React.ReactElement { + const user = useAuthInfo().info; + return ( + + } /> + + ); +} + +function NavLink(p: { + icon: React.ReactElement; + uri: string; + label: string; + secondaryAction?: React.ReactElement; +}): React.ReactElement { + const location = useLocation(); + return ( + + + {p.icon} + + {p.secondaryAction && ( + {p.secondaryAction} + )} + + + ); +}