Create home page

This commit is contained in:
Pierre HUBERT 2024-06-29 16:45:28 +02:00
parent e1739d9818
commit 1d32ca1559
11 changed files with 376 additions and 9 deletions

View File

@ -11,10 +11,13 @@
"@emotion/react": "^11.11.4", "@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5", "@emotion/styled": "^11.11.5",
"@fontsource/roboto": "^5.0.13", "@fontsource/roboto": "^5.0.13",
"@mdi/js": "^7.4.47",
"@mdi/react": "^1.6.1",
"@mui/icons-material": "^5.15.21", "@mui/icons-material": "^5.15.21",
"@mui/material": "^5.15.21", "@mui/material": "^5.15.21",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1" "react-dom": "^18.3.1",
"react-router-dom": "^6.24.0"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^18.3.3", "@types/react": "^18.3.3",
@ -1138,6 +1141,19 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@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": { "node_modules/@mui/base": {
"version": "5.0.0-beta.40", "version": "5.0.0-beta.40",
"resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.40.tgz", "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.40.tgz",
@ -1427,6 +1443,14 @@
"url": "https://opencollective.com/popperjs" "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": { "node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.18.0", "version": "4.18.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz", "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": ">=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": { "node_modules/react-transition-group": {
"version": "4.4.5", "version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",

View File

@ -13,10 +13,13 @@
"@emotion/react": "^11.11.4", "@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5", "@emotion/styled": "^11.11.5",
"@fontsource/roboto": "^5.0.13", "@fontsource/roboto": "^5.0.13",
"@mdi/js": "^7.4.47",
"@mdi/react": "^1.6.1",
"@mui/icons-material": "^5.15.21", "@mui/icons-material": "^5.15.21",
"@mui/material": "^5.15.21", "@mui/material": "^5.15.21",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1" "react-dom": "^18.3.1",
"react-router-dom": "^6.24.0"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^18.3.3", "@types/react": "^18.3.3",

View File

@ -1,10 +1,29 @@
import {
Route,
RouterProvider,
createBrowserRouter,
createRoutesFromElements,
} from "react-router-dom";
import { AuthApi } from "./api/AuthApi"; import { AuthApi } from "./api/AuthApi";
import { ServerApi } from "./api/ServerApi"; import { ServerApi } from "./api/ServerApi";
import { LoginRoute } from "./routes/LoginRoute"; import { LoginRoute } from "./routes/LoginRoute";
import { NotFoundRoute } from "./routes/NotFoundRoute";
import { HomeRoute } from "./routes/HomeRoute";
import { BaseAuthenticatedPage } from "./widgets/BaseAuthenticatedPage";
export function App() { export function App() {
if (!AuthApi.SignedIn && !ServerApi.Config.auth_disabled) if (!AuthApi.SignedIn && !ServerApi.Config.auth_disabled)
return <LoginRoute />; 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} />;
} }

View File

@ -1,7 +1,7 @@
import { APIClient } from "./ApiClient"; import { APIClient } from "./ApiClient";
export interface AuthInfo { export interface AuthInfo {
name: string; id: string;
} }
const TokenStateKey = "auth-state"; const TokenStateKey = "auth-state";
@ -60,11 +60,15 @@ export class AuthApi {
* Sign out * Sign out
*/ */
static async SignOut(): Promise<void> { static async SignOut(): Promise<void> {
this.UnsetAuthenticated();
try {
await APIClient.exec({ await APIClient.exec({
uri: "/auth/sign_out", uri: "/auth/sign_out",
method: "GET", method: "GET",
}); });
} finally {
this.UnsetAuthenticated(); window.location.href = "/";
}
} }
} }

View File

@ -0,0 +1,3 @@
export function HomeRoute(): React.ReactElement {
return <>home authenticated todo</>;
}

View 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>
);
}

View 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)!;
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}