diff --git a/central_frontend/index.html b/central_frontend/index.html index e4b78ea..48cd3b2 100644 --- a/central_frontend/index.html +++ b/central_frontend/index.html @@ -2,9 +2,9 @@ - + - Vite + React + TS + SolarEnergy
diff --git a/central_frontend/public/sun.jpg b/central_frontend/public/sun.jpg new file mode 100644 index 0000000..91cf0ca Binary files /dev/null and b/central_frontend/public/sun.jpg differ diff --git a/central_frontend/public/sunny.svg b/central_frontend/public/sunny.svg new file mode 100644 index 0000000..029b895 --- /dev/null +++ b/central_frontend/public/sunny.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/central_frontend/public/vite.svg b/central_frontend/public/vite.svg deleted file mode 100644 index e7b8dfb..0000000 --- a/central_frontend/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/central_frontend/src/App.css b/central_frontend/src/App.css deleted file mode 100644 index b9d355d..0000000 --- a/central_frontend/src/App.css +++ /dev/null @@ -1,42 +0,0 @@ -#root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} - -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } -} - -.card { - padding: 2em; -} - -.read-the-docs { - color: #888; -} diff --git a/central_frontend/src/App.tsx b/central_frontend/src/App.tsx index afe48ac..59eb8b2 100644 --- a/central_frontend/src/App.tsx +++ b/central_frontend/src/App.tsx @@ -1,35 +1,5 @@ -import { useState } from 'react' -import reactLogo from './assets/react.svg' -import viteLogo from '/vite.svg' -import './App.css' +import { LoginRoute } from "./routes/LoginRoute"; -function App() { - const [count, setCount] = useState(0) - - return ( - <> -
- - Vite logo - - - React logo - -
-

Vite + React

-
- -

- Edit src/App.tsx and save to test HMR -

-
-

- Click on the Vite and React logos to learn more -

- - ) +export function App() { + return ; } - -export default App diff --git a/central_frontend/src/assets/react.svg b/central_frontend/src/assets/react.svg deleted file mode 100644 index 6c87de9..0000000 --- a/central_frontend/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/central_frontend/src/hooks/context_providers/AlertDialogProvider.tsx b/central_frontend/src/hooks/context_providers/AlertDialogProvider.tsx new file mode 100644 index 0000000..d0d996a --- /dev/null +++ b/central_frontend/src/hooks/context_providers/AlertDialogProvider.tsx @@ -0,0 +1,68 @@ +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, +} from "@mui/material"; +import React, { PropsWithChildren } from "react"; + +type AlertContext = (message: string, title?: string) => Promise; + +const AlertContextK = React.createContext(null); + +export function AlertDialogProvider(p: PropsWithChildren): React.ReactElement { + const [open, setOpen] = React.useState(false); + + const [title, setTitle] = React.useState(undefined); + const [message, setMessage] = React.useState(""); + + const cb = React.useRef void)>(null); + + const handleClose = () => { + setOpen(false); + + if (cb.current !== null) cb.current(); + cb.current = null; + }; + + const hook: AlertContext = (message, title) => { + setTitle(title); + setMessage(message); + setOpen(true); + + return new Promise((res) => { + cb.current = res; + }); + }; + + return ( + <> + {p.children} + + + {title && {title}} + + + {message} + + + + + + + + ); +} + +export function useAlert(): AlertContext { + return React.useContext(AlertContextK)!; +} diff --git a/central_frontend/src/hooks/context_providers/ConfirmDialogProvider.tsx b/central_frontend/src/hooks/context_providers/ConfirmDialogProvider.tsx new file mode 100644 index 0000000..792b569 --- /dev/null +++ b/central_frontend/src/hooks/context_providers/ConfirmDialogProvider.tsx @@ -0,0 +1,88 @@ +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, +} from "@mui/material"; +import React, { PropsWithChildren } from "react"; + +type ConfirmContext = ( + message: string, + title?: string, + confirmButton?: string +) => Promise; + +const ConfirmContextK = React.createContext(null); + +export function ConfirmDialogProvider( + p: PropsWithChildren +): React.ReactElement { + const [open, setOpen] = React.useState(false); + + const [title, setTitle] = React.useState(undefined); + const [message, setMessage] = React.useState(""); + const [confirmButton, setConfirmButton] = React.useState( + undefined + ); + + const cb = React.useRef void)>(null); + + const handleClose = (confirm: boolean) => { + setOpen(false); + + if (cb.current !== null) cb.current(confirm); + cb.current = null; + }; + + const hook: ConfirmContext = (message, title, confirmButton) => { + setTitle(title); + setMessage(message); + setConfirmButton(confirmButton); + setOpen(true); + + return new Promise((res) => { + cb.current = res; + }); + }; + + const keyUp = (e: React.KeyboardEvent) => { + if (e.code === "Enter") handleClose(true); + }; + + return ( + <> + + {p.children} + + + handleClose(false)} + aria-labelledby="alert-dialog-title" + aria-describedby="alert-dialog-description" + onKeyUp={keyUp} + > + {title && {title}} + + + {message} + + + + + + + + + ); +} + +export function useConfirm(): ConfirmContext { + return React.useContext(ConfirmContextK)!; +} diff --git a/central_frontend/src/hooks/context_providers/DarkThemeProvider.tsx b/central_frontend/src/hooks/context_providers/DarkThemeProvider.tsx new file mode 100644 index 0000000..68df8f8 --- /dev/null +++ b/central_frontend/src/hooks/context_providers/DarkThemeProvider.tsx @@ -0,0 +1,50 @@ +import { ThemeProvider, createTheme } from "@mui/material/styles"; +import React from "react"; +import { PropsWithChildren } from "react"; + +const localStorageKey = "dark-theme"; + +const darkTheme = createTheme({ + palette: { + mode: "dark", + }, +}); + +const lightTheme = createTheme({ + palette: { + mode: "light", + }, +}); + +interface DarkThemeContext { + enabled: boolean; + setEnabled: (enabled: boolean) => void; +} + +const DarkThemeContextK = React.createContext(null); + +export function DarkThemeProvider(p: PropsWithChildren): React.ReactElement { + const [enabled, setEnabled] = React.useState( + localStorage.getItem(localStorageKey) !== "false" + ); + + return ( + + + {p.children} + + + ); +} + +export function useDarkTheme(): DarkThemeContext { + return React.useContext(DarkThemeContextK)!; +} diff --git a/central_frontend/src/hooks/context_providers/LoadingMessageProvider.tsx b/central_frontend/src/hooks/context_providers/LoadingMessageProvider.tsx new file mode 100644 index 0000000..6c0c826 --- /dev/null +++ b/central_frontend/src/hooks/context_providers/LoadingMessageProvider.tsx @@ -0,0 +1,64 @@ +import { + CircularProgress, + Dialog, + DialogContent, + DialogContentText, +} from "@mui/material"; +import React, { PropsWithChildren } from "react"; + +type LoadingMessageContext = { + show: (message: string) => void; + hide: () => void; +}; + +const LoadingMessageContextK = + React.createContext(null); + +export function LoadingMessageProvider( + p: PropsWithChildren +): React.ReactElement { + const [open, setOpen] = React.useState(false); + + const [message, setMessage] = React.useState(""); + + const hook: LoadingMessageContext = { + show(message) { + setMessage(message); + setOpen(true); + }, + hide() { + setMessage(""); + setOpen(false); + }, + }; + + return ( + <> + + {p.children} + + + + + +
+ + + {message} +
+
+
+
+ + ); +} + +export function useLoadingMessage(): LoadingMessageContext { + return React.useContext(LoadingMessageContextK)!; +} diff --git a/central_frontend/src/hooks/context_providers/SnackbarProvider.tsx b/central_frontend/src/hooks/context_providers/SnackbarProvider.tsx new file mode 100644 index 0000000..203b3c9 --- /dev/null +++ b/central_frontend/src/hooks/context_providers/SnackbarProvider.tsx @@ -0,0 +1,43 @@ +import { Snackbar } from "@mui/material"; + +import React, { PropsWithChildren } from "react"; + +type SnackbarContext = (message: string, duration?: number) => void; + +const SnackbarContextK = React.createContext(null); + +export function SnackbarProvider(p: PropsWithChildren): React.ReactElement { + const [open, setOpen] = React.useState(false); + + const [message, setMessage] = React.useState(""); + const [duration, setDuration] = React.useState(0); + + const handleClose = () => { + setOpen(false); + }; + + const hook: SnackbarContext = (message, duration) => { + setMessage(message); + setDuration(duration ?? 6000); + setOpen(true); + }; + + return ( + <> + + {p.children} + + + + + ); +} + +export function useSnackbar(): SnackbarContext { + return React.useContext(SnackbarContextK)!; +} diff --git a/central_frontend/src/index.css b/central_frontend/src/index.css index 6119ad9..b303f80 100644 --- a/central_frontend/src/index.css +++ b/central_frontend/src/index.css @@ -1,68 +1,9 @@ -:root { - font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; - - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; - - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} -a:hover { - color: #535bf2; -} - body { margin: 0; - display: flex; - place-items: center; - min-width: 320px; - min-height: 100vh; } -h1 { - font-size: 3.2em; - line-height: 1.1; -} - -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; -} -button:hover { - border-color: #646cff; -} -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; -} - -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - a:hover { - color: #747bff; - } - button { - background-color: #f9f9f9; - } +html, +body, +#root { + height: 100%; } diff --git a/central_frontend/src/main.tsx b/central_frontend/src/main.tsx index f0e3e2f..8848b9e 100644 --- a/central_frontend/src/main.tsx +++ b/central_frontend/src/main.tsx @@ -1,15 +1,29 @@ -import React from "react"; -import ReactDOM from "react-dom/client"; -import App from "./App.tsx"; -import "./index.css"; - import "@fontsource/roboto/300.css"; import "@fontsource/roboto/400.css"; import "@fontsource/roboto/500.css"; import "@fontsource/roboto/700.css"; +import React from "react"; +import ReactDOM from "react-dom/client"; +import { App } from "./App"; +import { AlertDialogProvider } from "./hooks/context_providers/AlertDialogProvider"; +import { ConfirmDialogProvider } from "./hooks/context_providers/ConfirmDialogProvider"; +import { DarkThemeProvider } from "./hooks/context_providers/DarkThemeProvider"; +import { LoadingMessageProvider } from "./hooks/context_providers/LoadingMessageProvider"; +import { SnackbarProvider } from "./hooks/context_providers/SnackbarProvider"; +import "./index.css"; ReactDOM.createRoot(document.getElementById("root")!).render( - + + + + + + + + + + + ); diff --git a/central_frontend/src/routes/LoginRoute.tsx b/central_frontend/src/routes/LoginRoute.tsx new file mode 100644 index 0000000..c153909 --- /dev/null +++ b/central_frontend/src/routes/LoginRoute.tsx @@ -0,0 +1,118 @@ +import * as React from "react"; +import Avatar from "@mui/material/Avatar"; +import Button from "@mui/material/Button"; +import CssBaseline from "@mui/material/CssBaseline"; +import TextField from "@mui/material/TextField"; +import FormControlLabel from "@mui/material/FormControlLabel"; +import Checkbox from "@mui/material/Checkbox"; +import Link from "@mui/material/Link"; +import Paper from "@mui/material/Paper"; +import Box from "@mui/material/Box"; +import Grid from "@mui/material/Grid"; +import LockOutlinedIcon from "@mui/icons-material/LockOutlined"; +import Typography from "@mui/material/Typography"; + +function Copyright(props: any) { + return ( + + {"Copyright © "} + + Pierre HUBERT + {" "} + {new Date().getFullYear()} + {"."} + + ); +} + +export function LoginRoute() { + const [user, setUser] = React.useState(""); + const [password, setPassword] = React.useState(""); + + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + // TODO + }; + + return ( + + + + t.palette.mode === "light" + ? t.palette.grey[50] + : t.palette.grey[900], + backgroundSize: "cover", + backgroundPosition: "left", + }} + /> + + + + + + + SolarEnergy + + + setUser(v.target.value)} + /> + + setPassword(v.target.value)} + /> + + + + + + + + + ); +}