Add base web UI
This commit is contained in:
92
moneymgr_web/src/widgets/AsyncWidget.tsx
Normal file
92
moneymgr_web/src/widgets/AsyncWidget.tsx
Normal file
@ -0,0 +1,92 @@
|
||||
import { Alert, Box, Button, CircularProgress } from "@mui/material";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
enum State {
|
||||
Loading,
|
||||
Ready,
|
||||
Error,
|
||||
}
|
||||
|
||||
export function AsyncWidget(p: {
|
||||
loadKey: any;
|
||||
load: () => Promise<void>;
|
||||
errMsg: string;
|
||||
build: () => React.ReactElement;
|
||||
ready?: boolean;
|
||||
errAdditionalElement?: () => React.ReactElement;
|
||||
}): React.ReactElement {
|
||||
const [state, setState] = useState(State.Loading);
|
||||
|
||||
const counter = useRef<any | null>(null);
|
||||
|
||||
const load = async () => {
|
||||
try {
|
||||
setState(State.Loading);
|
||||
await p.load();
|
||||
setState(State.Ready);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setState(State.Error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (counter.current === p.loadKey) return;
|
||||
counter.current = p.loadKey;
|
||||
|
||||
load();
|
||||
});
|
||||
|
||||
if (state === State.Error)
|
||||
return (
|
||||
<Box
|
||||
component="div"
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: "100%",
|
||||
flex: "1",
|
||||
flexDirection: "column",
|
||||
backgroundColor: (theme) =>
|
||||
theme.palette.mode === "light"
|
||||
? theme.palette.grey[100]
|
||||
: theme.palette.grey[900],
|
||||
}}
|
||||
>
|
||||
<Alert
|
||||
variant="outlined"
|
||||
severity="error"
|
||||
style={{ margin: "0px 15px 15px 15px" }}
|
||||
>
|
||||
{p.errMsg}
|
||||
</Alert>
|
||||
|
||||
<Button onClick={load}>Try again</Button>
|
||||
|
||||
{p.errAdditionalElement && p.errAdditionalElement()}
|
||||
</Box>
|
||||
);
|
||||
|
||||
if (state === State.Loading || p.ready === false)
|
||||
return (
|
||||
<Box
|
||||
component="div"
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: "100%",
|
||||
flex: "1",
|
||||
backgroundColor: (theme) =>
|
||||
theme.palette.mode === "light"
|
||||
? theme.palette.grey[100]
|
||||
: theme.palette.grey[900],
|
||||
}}
|
||||
>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
);
|
||||
|
||||
return p.build();
|
||||
}
|
13
moneymgr_web/src/widgets/AuthSingleMessage.tsx
Normal file
13
moneymgr_web/src/widgets/AuthSingleMessage.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import { Button } from "@mui/material";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
export function AuthSingleMessage(p: { message: string }): React.ReactElement {
|
||||
return (
|
||||
<>
|
||||
<p style={{ textAlign: "center" }}>{p.message}</p>
|
||||
<Link to={"/"}>
|
||||
<Button>Go back home</Button>
|
||||
</Link>
|
||||
</>
|
||||
);
|
||||
}
|
88
moneymgr_web/src/widgets/BaseAuthenticatedPage.tsx
Normal file
88
moneymgr_web/src/widgets/BaseAuthenticatedPage.tsx
Normal file
@ -0,0 +1,88 @@
|
||||
import { Box, Button } from "@mui/material";
|
||||
import * as React from "react";
|
||||
import { Outlet, useNavigate } from "react-router-dom";
|
||||
import { useAuth } from "../App";
|
||||
import { AuthApi, AuthInfo } from "../api/AuthApi";
|
||||
import { AsyncWidget } from "./AsyncWidget";
|
||||
import { MoneyWebAppBar } from "./MoneyWebAppBar";
|
||||
import { MoneyNavList } from "./MoneyNavList";
|
||||
|
||||
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 auth = useAuth();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const signOut = () => {
|
||||
AuthApi.SignOut();
|
||||
navigate("/");
|
||||
auth.setSignedIn(false);
|
||||
};
|
||||
|
||||
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],
|
||||
}}
|
||||
>
|
||||
<MoneyWebAppBar onSignOut={signOut} />
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flex: "2",
|
||||
}}
|
||||
>
|
||||
<MoneyNavList />
|
||||
<div style={{ flex: 1, display: "flex" }}>
|
||||
<Outlet />
|
||||
</div>
|
||||
</Box>
|
||||
</Box>
|
||||
</AuthInfoContextK.Provider>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function useAuthInfo(): AuthInfoContext {
|
||||
return React.useContext(AuthInfoContextK)!;
|
||||
}
|
94
moneymgr_web/src/widgets/BaseLoginPage.tsx
Normal file
94
moneymgr_web/src/widgets/BaseLoginPage.tsx
Normal file
@ -0,0 +1,94 @@
|
||||
import { mdiServer } from "@mdi/js";
|
||||
import Icon from "@mdi/react";
|
||||
import Avatar from "@mui/material/Avatar";
|
||||
import Box from "@mui/material/Box";
|
||||
import CssBaseline from "@mui/material/CssBaseline";
|
||||
import Grid from "@mui/material/Grid2";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { Link, Outlet } from "react-router-dom";
|
||||
import loginSplashImage from "./mufid-majnun-LVcjYwuHQlg-unsplash.jpg";
|
||||
|
||||
function Copyright(props: any): React.ReactElement {
|
||||
return (
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
align="center"
|
||||
style={{ marginTop: "20px" }}
|
||||
{...props}
|
||||
>
|
||||
{"Copyright © "}
|
||||
<a
|
||||
color="inherit"
|
||||
href="https://0ph.fr/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
style={{ color: "inherit" }}
|
||||
>
|
||||
Pierre HUBERT
|
||||
</a>{" "}
|
||||
{new Date().getFullYear()}
|
||||
{"."}
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
export function BaseLoginPage() {
|
||||
return (
|
||||
<Grid container component="main" sx={{ height: "100vh" }}>
|
||||
<CssBaseline />
|
||||
<Grid
|
||||
size={{ xs: false, sm: 4, md: 7 }}
|
||||
sx={{
|
||||
backgroundImage: `url(${loginSplashImage})`,
|
||||
backgroundRepeat: "no-repeat",
|
||||
backgroundColor: (t) =>
|
||||
t.palette.mode === "light"
|
||||
? t.palette.grey[50]
|
||||
: t.palette.grey[900],
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center",
|
||||
}}
|
||||
/>
|
||||
<Grid
|
||||
size={{ xs: 12, sm: 8, md: 5 }}
|
||||
component={Paper}
|
||||
elevation={6}
|
||||
square
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flex: "1",
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<Avatar sx={{ m: 1, bgcolor: "secondary.main" }}>
|
||||
<Icon path={mdiServer} size={1} />
|
||||
</Avatar>
|
||||
<Link to="/" style={{ color: "inherit", textDecoration: "none" }}>
|
||||
<Typography component="h1" variant="h5">
|
||||
Money manager
|
||||
</Typography>
|
||||
</Link>
|
||||
<Typography
|
||||
component="h1"
|
||||
variant="h6"
|
||||
style={{ margin: " 40px 0px" }}
|
||||
>
|
||||
Open source money managment
|
||||
</Typography>
|
||||
|
||||
{/* inner page */}
|
||||
<Outlet />
|
||||
|
||||
<Copyright sx={{ mt: 5 }} style={{ margin: " 40px 0px" }} />
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
19
moneymgr_web/src/widgets/DarkThemeButton.tsx
Normal file
19
moneymgr_web/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>
|
||||
);
|
||||
}
|
53
moneymgr_web/src/widgets/MoneyNavList.tsx
Normal file
53
moneymgr_web/src/widgets/MoneyNavList.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
import { mdiApi, mdiHome } from "@mdi/js";
|
||||
import Icon from "@mdi/react";
|
||||
import {
|
||||
List,
|
||||
ListItemButton,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
} from "@mui/material";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { useAuthInfo } from "./BaseAuthenticatedPage";
|
||||
import { RouterLink } from "./RouterLink";
|
||||
|
||||
export function MoneyNavList(): React.ReactElement {
|
||||
const user = useAuthInfo().info;
|
||||
return (
|
||||
<List
|
||||
dense
|
||||
component="nav"
|
||||
sx={{
|
||||
minWidth: "200px",
|
||||
backgroundColor: "background.paper",
|
||||
}}
|
||||
>
|
||||
<NavLink
|
||||
label="Accueil"
|
||||
uri="/"
|
||||
icon={<Icon path={mdiHome} size={1} />}
|
||||
/>
|
||||
|
||||
<NavLink
|
||||
label="API Tokens"
|
||||
uri="/tokens"
|
||||
icon={<Icon path={mdiApi} size={1} />}
|
||||
/>
|
||||
</List>
|
||||
);
|
||||
}
|
||||
|
||||
function NavLink(p: {
|
||||
icon: React.ReactElement;
|
||||
uri: string;
|
||||
label: string;
|
||||
}): React.ReactElement {
|
||||
const location = useLocation();
|
||||
return (
|
||||
<RouterLink to={p.uri}>
|
||||
<ListItemButton selected={p.uri === location.pathname}>
|
||||
<ListItemIcon>{p.icon}</ListItemIcon>
|
||||
<ListItemText primary={p.label} />
|
||||
</ListItemButton>
|
||||
</RouterLink>
|
||||
);
|
||||
}
|
81
moneymgr_web/src/widgets/MoneyWebAppBar.tsx
Normal file
81
moneymgr_web/src/widgets/MoneyWebAppBar.tsx
Normal file
@ -0,0 +1,81 @@
|
||||
import { mdiCash } 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 MoneyWebAppBar(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={mdiCash} size={1} style={{ marginRight: "1rem" }} />
|
||||
|
||||
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
|
||||
<RouterLink to="/">Money Manager</RouterLink>
|
||||
</Typography>
|
||||
|
||||
<div>
|
||||
<DarkThemeButton />
|
||||
|
||||
<Button size="large" color="inherit">
|
||||
{authInfo!.info.name}
|
||||
</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}>Sign out</MenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
);
|
||||
}
|
16
moneymgr_web/src/widgets/RouterLink.tsx
Normal file
16
moneymgr_web/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>
|
||||
);
|
||||
}
|
BIN
moneymgr_web/src/widgets/mufid-majnun-LVcjYwuHQlg-unsplash.jpg
Normal file
BIN
moneymgr_web/src/widgets/mufid-majnun-LVcjYwuHQlg-unsplash.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 582 KiB |
Reference in New Issue
Block a user