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 + 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}
+
+
+ >
+ );
+}
+
+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}
+
+
+
+ >
+ );
+}
+
+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}
+
+
+
+ >
+ );
+}
+
+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)}
+ />
+
+
+
+
+
+
+
+
+ );
+}