Create home page
This commit is contained in:
parent
e1739d9818
commit
1d32ca1559
56
central_frontend/package-lock.json
generated
56
central_frontend/package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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 <LoginRoute />;
|
||||
|
||||
return <>logged in todo</>;
|
||||
const router = createBrowserRouter(
|
||||
createRoutesFromElements(
|
||||
<Route path="*" element={<BaseAuthenticatedPage />}>
|
||||
<Route path="" element={<HomeRoute />} />
|
||||
|
||||
<Route path="*" element={<NotFoundRoute />} />
|
||||
</Route>
|
||||
)
|
||||
);
|
||||
|
||||
return <RouterProvider router={router} />;
|
||||
}
|
||||
|
@ -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<void> {
|
||||
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 = "/";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
3
central_frontend/src/routes/HomeRoute.tsx
Normal file
3
central_frontend/src/routes/HomeRoute.tsx
Normal file
@ -0,0 +1,3 @@
|
||||
export function HomeRoute(): React.ReactElement {
|
||||
return <>home authenticated todo</>;
|
||||
}
|
23
central_frontend/src/routes/NotFoundRoute.tsx
Normal file
23
central_frontend/src/routes/NotFoundRoute.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { Button } from "@mui/material";
|
||||
import { RouterLink } from "../widgets/RouterLink";
|
||||
|
||||
export function NotFoundRoute(): React.ReactElement {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
textAlign: "center",
|
||||
flex: "1",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<h1>404 Not found</h1>
|
||||
<p>The page you requested was not found!</p>
|
||||
<RouterLink to="/">
|
||||
<Button>Go back home</Button>
|
||||
</RouterLink>
|
||||
</div>
|
||||
);
|
||||
}
|
82
central_frontend/src/widgets/BaseAuthenticatedPage.tsx
Normal file
82
central_frontend/src/widgets/BaseAuthenticatedPage.tsx
Normal file
@ -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<AuthInfoContext | null>(null);
|
||||
|
||||
export function BaseAuthenticatedPage(): React.ReactElement {
|
||||
const [authInfo, setAuthInfo] = React.useState<null | AuthInfo>(null);
|
||||
|
||||
const signOut = () => {
|
||||
AuthApi.SignOut();
|
||||
};
|
||||
|
||||
const load = async () => {
|
||||
setAuthInfo(await AuthApi.GetAuthInfo());
|
||||
};
|
||||
|
||||
return (
|
||||
<AsyncWidget
|
||||
loadKey="1"
|
||||
load={load}
|
||||
errMsg="Failed to load user information!"
|
||||
errAdditionalElement={() => (
|
||||
<>
|
||||
<Button onClick={signOut}>Sign out</Button>
|
||||
</>
|
||||
)}
|
||||
build={() => (
|
||||
<AuthInfoContextK.Provider
|
||||
value={{
|
||||
info: authInfo!,
|
||||
reloadAuthInfo: load,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component="div"
|
||||
sx={{
|
||||
minHeight: "100vh",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
backgroundColor: (theme) =>
|
||||
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],
|
||||
}}
|
||||
>
|
||||
<SolarEnergyAppBar onSignOut={signOut} />
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flex: "2",
|
||||
}}
|
||||
>
|
||||
<SolarEnergyNavList />
|
||||
<div style={{ flex: 1, display: "flex" }}>
|
||||
<Outlet />
|
||||
</div>
|
||||
</Box>
|
||||
</Box>
|
||||
</AuthInfoContextK.Provider>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function useAuthInfo(): AuthInfoContext {
|
||||
return React.useContext(AuthInfoContextK)!;
|
||||
}
|
19
central_frontend/src/widgets/DarkThemeButton.tsx
Normal file
19
central_frontend/src/widgets/DarkThemeButton.tsx
Normal file
@ -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 (
|
||||
<Tooltip title="Activer / désactiver le mode sombre">
|
||||
<IconButton
|
||||
onClick={() => darkTheme.setEnabled(!darkTheme.enabled)}
|
||||
style={{ color: "inherit" }}
|
||||
>
|
||||
{!darkTheme.enabled ? <DarkModeIcon /> : <Brightness7Icon />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
16
central_frontend/src/widgets/RouterLink.tsx
Normal file
16
central_frontend/src/widgets/RouterLink.tsx
Normal file
@ -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 (
|
||||
<Link
|
||||
to={p.to}
|
||||
target={p.target}
|
||||
style={{ color: "inherit", textDecoration: "inherit" }}
|
||||
>
|
||||
{p.children}
|
||||
</Link>
|
||||
);
|
||||
}
|
85
central_frontend/src/widgets/SolarEnergyAppBar.tsx
Normal file
85
central_frontend/src/widgets/SolarEnergyAppBar.tsx
Normal file
@ -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 | HTMLElement>(null);
|
||||
const handleMenu = (event: React.MouseEvent<HTMLElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleCloseMenu = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const signOut = () => {
|
||||
handleCloseMenu();
|
||||
p.onSignOut();
|
||||
};
|
||||
|
||||
return (
|
||||
<AppBar position="sticky">
|
||||
<Toolbar>
|
||||
<Icon
|
||||
path={mdiWhiteBalanceSunny}
|
||||
size={1}
|
||||
style={{ marginRight: "1rem" }}
|
||||
/>
|
||||
|
||||
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
|
||||
<RouterLink to="/">Solar Energy</RouterLink>
|
||||
</Typography>
|
||||
|
||||
<div>
|
||||
<DarkThemeButton />
|
||||
|
||||
<Button size="large" color="inherit">
|
||||
{authInfo!.info.id}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size="large"
|
||||
aria-label="account of current user"
|
||||
aria-controls="menu-appbar"
|
||||
aria-haspopup="true"
|
||||
onClick={handleMenu}
|
||||
color="inherit"
|
||||
>
|
||||
<SettingsIcon />
|
||||
</Button>
|
||||
<Menu
|
||||
id="menu-appbar"
|
||||
anchorEl={anchorEl}
|
||||
anchorOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "right",
|
||||
}}
|
||||
keepMounted
|
||||
transformOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "right",
|
||||
}}
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={handleCloseMenu}
|
||||
>
|
||||
<MenuItem onClick={signOut}>Déconnexion</MenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
);
|
||||
}
|
59
central_frontend/src/widgets/SolarEnergyNavList.tsx
Normal file
59
central_frontend/src/widgets/SolarEnergyNavList.tsx
Normal file
@ -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 (
|
||||
<List
|
||||
dense
|
||||
component="nav"
|
||||
sx={{
|
||||
minWidth: "200px",
|
||||
backgroundColor: "background.paper",
|
||||
}}
|
||||
>
|
||||
<NavLink label="Home" uri="/" icon={<Icon path={mdiHome} size={1} />} />
|
||||
</List>
|
||||
);
|
||||
}
|
||||
|
||||
function NavLink(p: {
|
||||
icon: React.ReactElement;
|
||||
uri: string;
|
||||
label: string;
|
||||
secondaryAction?: React.ReactElement;
|
||||
}): React.ReactElement {
|
||||
const location = useLocation();
|
||||
return (
|
||||
<RouterLink to={p.uri}>
|
||||
<ListItemButton selected={p.uri === location.pathname}>
|
||||
<ListItemIcon>{p.icon}</ListItemIcon>
|
||||
<ListItemText primary={p.label} />
|
||||
{p.secondaryAction && (
|
||||
<ListItemSecondaryAction>{p.secondaryAction}</ListItemSecondaryAction>
|
||||
)}
|
||||
</ListItemButton>
|
||||
</RouterLink>
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue
Block a user