diff --git a/package.json b/package.json index 610fc50..591a674 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,8 @@ "dependencies": { "@emotion/react": "^11.13.0", "@emotion/styled": "^11.13.0", - "@mui/icons-material": "^5.16.6", - "@mui/material": "^5.16.6", + "@mui/icons-material": "^6.1.2", + "@mui/material": "^6.1.2", "@types/createjs": "^0.0.29", "@types/react": "^18.0.34", "@types/react-dom": "^18.0.11", @@ -43,7 +43,8 @@ "scripts": { "firebase": "webpack --mode development --progress --env storage='firebase'", "localstorage": "webpack --mode development --progress --env storage='localstorage'", - "styleguide": "styleguidist server" + "styleguide": "styleguidist server", + "build": "webpack --mode production --progress --env storage='firebase'" }, "eslintConfig": { "extends": [ diff --git a/server.js b/server.js index 9fccbe4..521a367 100644 --- a/server.js +++ b/server.js @@ -54,6 +54,18 @@ io.on("connect_error", (err) => { console.log(`connect_error due to ${err.message}`); }); +let protocol = undefined; // TODO(binit): ensure robot/operator protocol match +let status = "offline"; // ["online", "offline", "occupied"] +function updateRooms() { + io.emit("update_rooms", { + robot_id: { + name: process.env.HELLO_FLEET_ID, + protocol: protocol, + status: status, + }, + }); +} + io.on("connection", function (socket) { console.log("new socket.io connection"); // console.log('socket.handshake = '); @@ -68,10 +80,23 @@ io.on("connection", function (socket) { ) { socket.join(room); socket.emit("join", room, socket.id); + status = "online"; } else { console.log("room full"); socket.emit("full", room); + status = "occupied"; + } + updateRooms(); + }); + + socket.on("list_rooms", () => { + if ( + io.sockets.adapter.rooms.get("robot") && + io.sockets.adapter.rooms.get("robot").size >= 2 + ) { + status = "occupied"; } + updateRooms(); }); socket.on("add operator to robot room", (callback) => { @@ -85,10 +110,13 @@ io.on("connection", function (socket) { console.log("could not connect because robot room is full"); callback({ success: false }); } + status = "occupied"; } else { console.log("could not connect because robot is not available"); callback({ success: false }); + status = "offline"; } + updateRooms(); }); socket.on("signalling", function (message) { diff --git a/src/pages/home/css/CallRobotSelector.css b/src/pages/home/css/CallRobotSelector.css new file mode 100644 index 0000000..e281b5a --- /dev/null +++ b/src/pages/home/css/CallRobotSelector.css @@ -0,0 +1,3 @@ +.rs-container { + overflow: auto; +} diff --git a/src/pages/home/css/Changelog.css b/src/pages/home/css/Changelog.css new file mode 100644 index 0000000..27aabb7 --- /dev/null +++ b/src/pages/home/css/Changelog.css @@ -0,0 +1,3 @@ +.cv-container { + overflow: auto; +} diff --git a/src/pages/home/css/LoginView.css b/src/pages/home/css/LoginView.css new file mode 100644 index 0000000..0e8a0cf --- /dev/null +++ b/src/pages/home/css/LoginView.css @@ -0,0 +1,3 @@ +.lv-container { + padding: 20px; +} diff --git a/src/pages/home/css/SideBySideView.css b/src/pages/home/css/SideBySideView.css new file mode 100644 index 0000000..d547890 --- /dev/null +++ b/src/pages/home/css/SideBySideView.css @@ -0,0 +1,3 @@ +.sbs-container { + padding: 20px; +} diff --git a/src/pages/home/css/index.css b/src/pages/home/css/index.css new file mode 100644 index 0000000..0127118 --- /dev/null +++ b/src/pages/home/css/index.css @@ -0,0 +1,24 @@ +body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", + "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", + "Helvetica Neue", sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + background-color: rgb(245, 245, 245); +} + +html, +body { + height: 100%; + margin: 0; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", + monospace; +} + +#root { + margin: 0; + height: 100%; +} diff --git a/src/pages/home/html/index.html b/src/pages/home/html/index.html new file mode 100644 index 0000000..ddd45c1 --- /dev/null +++ b/src/pages/home/html/index.html @@ -0,0 +1,10 @@ + + + + + Home - Stretch Web Teleop + + +
+ + diff --git a/src/pages/home/tsx/components/CallRobotSelector.tsx b/src/pages/home/tsx/components/CallRobotSelector.tsx new file mode 100644 index 0000000..01a40c6 --- /dev/null +++ b/src/pages/home/tsx/components/CallRobotSelector.tsx @@ -0,0 +1,168 @@ +import "home/css/CallRobotSelector.css"; +import React, { useEffect, useState } from "react"; +import Box from "@mui/material/Box"; +import Grid from "@mui/material/Grid2"; +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import CardActions from "@mui/material/CardActions"; +import Button from "@mui/material/Button"; +import Typography from "@mui/material/Typography"; +import CircleIcon from "@mui/icons-material/Circle"; +import { green, red, yellow, grey } from "@mui/material/colors"; +import { loginHandler } from "../index"; + +function get_indicator_text(status_str) { + switch (status_str) { + case "online": + return "Online"; + case "offline": + return "Offline"; + case "occupied": + return "Occupied"; + default: + return "Unknown"; + } +} + +function get_indicator(status_str) { + let statusui; + switch (status_str) { + case "online": + statusui = { + color_name: "green", + color: green, + }; + break; + case "offline": + statusui = { + color_name: "red", + color: red, + }; + break; + case "occupied": + statusui = { + color_name: "yellow", + color: yellow, + }; + break; + default: + statusui = { + color_name: "grey", + color: grey, + }; + } + let indicator_css = { + fontSize: 12, + color: statusui["color"]["A400"], + animation: `glowing_${statusui["color_name"]} 3s linear infinite`, + }; + indicator_css[`@keyframes glowing_${statusui["color_name"]}`] = { + "0%": { + color: statusui["color"]["A400"], + }, + "50%": { + color: statusui["color"]["A200"], + }, + "100%": { + color: statusui["color"]["A400"], + }, + }; + return ; +} + +function get_action(status_str, robot_name) { + switch (status_str) { + case "online": + return ( + + ); + case "offline": + return ( + + ); + case "occupied": + return ( + + ); + default: + return ( + + ); + } +} + +const CallRobotItem = (props: { name: String; status: String }) => { + return ( + + + + {props.name} + + + {get_indicator(props.status)}{" "} + {get_indicator_text(props.status)} + + + {get_action(props.status, props.name)} + + ); +}; + +export const CallRobotSelector = (props: { style?: React.CSSProperties }) => { + const [callableRobots, setCallableRobots] = useState({}); + + useEffect(() => { + loginHandler.listRooms(setCallableRobots); + }, [props]); + + return ( + +

Robots:

+ + {Object.entries(callableRobots).map(([key, value], idx) => { + return ( + + + + ); + })} + +
+ ); +}; diff --git a/src/pages/home/tsx/components/Changelog.tsx b/src/pages/home/tsx/components/Changelog.tsx new file mode 100644 index 0000000..1894242 --- /dev/null +++ b/src/pages/home/tsx/components/Changelog.tsx @@ -0,0 +1,125 @@ +import "home/css/Changelog.css"; +import React, { useEffect, useState } from "react"; +import Box from "@mui/material/Box"; +import Grid from "@mui/material/Grid2"; +import { styled } from "@mui/material/styles"; + +const LogItem = styled(Box)(({ theme }) => ({ + ...theme.typography.body2, + paddingLeft: theme.spacing(1), + color: theme.palette.text.secondary, + fontSize: 17, +})); + +export const Changelog = (props: { style?: React.CSSProperties }) => { + return ( + +

What's new?

+ + + + + Login Page - + Oct 9th, 2024 + +

+ The new homepage shows the robots you can call, or + as "unavailable" if the robot is powered off or + occupied by another operator. There's also a "What's + New" section with details about new features being + added to the web interface. By default, you are + logged in when running the interface locally, but + there's a login screen that can be accessed by + logging out. In the future, this login screen will + enable you to call your Stretch over the internet. +

+
+
+ + + + + Homing Button + {" "} + - Sept 21st, 2024 + +

+ The operator interface now shows an banner if your + robot needs to be homed. Since some of Stretch's + encoders are relative, there's a homing sequence to + find zero for those joints when Stretch wakes up. + Previously, developers had to use a terminal to + trigger Stretch's homing sequence, but now you can + do it through the web interface. +

+
+
+ + + + + Lorem ipsum dolor et. + {" "} + - May 18st, 2024 + +

+ Lorem ipsum odor amet, consectetuer adipiscing elit. + Mattis purus potenti orci per torquent scelerisque. + Feugiat fringilla tristique various feugiat quis + cras magnis efficitur. Aptent curabitur mattis dui + congue porta cubilia. Lorem scelerisque convallis + tempor himenaeos donec inceptos ultricies dis. + Efficitur feugiat senectus nullam semper conubia + risus mi volutpat. +

+
+
+ + + + + Lorem ipsum dolor et. + {" "} + - Mar 2nd, 2024 + +

+ Lorem ipsum odor amet, consectetuer adipiscing elit. + Mattis purus potenti orci per torquent scelerisque. + Feugiat fringilla tristique various feugiat quis + cras magnis efficitur. Aptent curabitur mattis dui + congue porta cubilia. Lorem scelerisque convallis + tempor himenaeos donec inceptos ultricies dis. + Efficitur feugiat senectus nullam semper conubia + risus mi volutpat. +

+
+
+ + + + + Lorem ipsum dolor et. + {" "} + - Jan 31st, 2024 + +

+ Lorem ipsum odor amet, consectetuer adipiscing elit. + Mattis purus potenti orci per torquent scelerisque. + Feugiat fringilla tristique various feugiat quis + cras magnis efficitur. Aptent curabitur mattis dui + congue porta cubilia. Lorem scelerisque convallis + tempor himenaeos donec inceptos ultricies dis. + Efficitur feugiat senectus nullam semper conubia + risus mi volutpat. +

+
+
+
+
+ ); +}; diff --git a/src/pages/home/tsx/components/ForgotPassword.tsx b/src/pages/home/tsx/components/ForgotPassword.tsx new file mode 100644 index 0000000..5ea11e3 --- /dev/null +++ b/src/pages/home/tsx/components/ForgotPassword.tsx @@ -0,0 +1,61 @@ +// This component comes from the template at: +// https://github.com/mui/material-ui/tree/v6.1.5/docs/data/material/getting-started/templates/sign-in + +import React, { useEffect, useState } from "react"; +import { isTablet, isBrowser } from "react-device-detect"; +import Dialog from "@mui/material/Dialog"; +import DialogTitle from "@mui/material/DialogTitle"; +import DialogContent from "@mui/material/DialogContent"; +import DialogContentText from "@mui/material/DialogContentText"; +import DialogActions from "@mui/material/DialogActions"; +import OutlinedInput from "@mui/material/OutlinedInput"; +import Button from "@mui/material/Button"; + +export const ForgotPassword = (props: { + open: boolean; + handleClose: () => void; + handleExecute: (email: string) => void; +}) => { + return isTablet || isBrowser ? ( + ) => { + event.preventDefault(); + const data = new FormData(event.currentTarget); + props.handleExecute(data.get("email") as string); + props.handleClose(); + event.stopPropagation(); + }, + }} + > + Reset password + + + Enter your account's email address, and we'll send + you a link to reset your password. + + + + + + + + + ) : ( +

Not implemented

+ ); +}; diff --git a/src/pages/home/tsx/components/LoginView.tsx b/src/pages/home/tsx/components/LoginView.tsx new file mode 100644 index 0000000..0e96d07 --- /dev/null +++ b/src/pages/home/tsx/components/LoginView.tsx @@ -0,0 +1,284 @@ +// This component comes from the template at: +// https://github.com/mui/material-ui/tree/v6.1.5/docs/data/material/getting-started/templates/sign-in + +import "home/css/LoginView.css"; +import React, { useEffect, useState } from "react"; +import { isTablet, isBrowser } from "react-device-detect"; +import Box from "@mui/material/Box"; +import MuiCard from "@mui/material/Card"; +import Typography from "@mui/material/Typography"; +import FormControl from "@mui/material/FormControl"; +import FormLabel from "@mui/material/FormLabel"; +import FormControlLabel from "@mui/material/FormControlLabel"; +import TextField from "@mui/material/TextField"; +import Link from "@mui/material/Link"; +import Checkbox from "@mui/material/Checkbox"; +import Button from "@mui/material/Button"; +import Snackbar from "@mui/material/Snackbar"; +import { styled } from "@mui/material/styles"; +import { ForgotPassword } from "./ForgotPassword"; +import { loginHandler } from "../index"; + +const Card = styled(MuiCard)(({ theme }) => ({ + display: "flex", + flexDirection: "column", + alignSelf: "center", + width: "100%", + padding: theme.spacing(4), + gap: theme.spacing(2), + margin: "auto", + [theme.breakpoints.up("sm")]: { + maxWidth: "450px", + }, + boxShadow: + "hsla(220, 30%, 5%, 0.05) 0px 5px 15px 0px, hsla(220, 25%, 10%, 0.05) 0px 15px 35px -5px", + ...theme.applyStyles("dark", { + boxShadow: + "hsla(220, 30%, 5%, 0.5) 0px 5px 15px 0px, hsla(220, 25%, 10%, 0.08) 0px 15px 35px -5px", + }), +})); + +const SignInError = styled(Box)(({ theme }) => ({ + backgroundColor: "#d32f2f", + margin: `calc(-1 * ${theme.spacing(4)})`, + marginBottom: 0, + padding: 10, + color: "white", +})); + +export const LoginView = (props) => { + const [emailError, setEmailError] = useState(false); + const [emailErrorMessage, setEmailErrorMessage] = useState(""); + const [passwordError, setPasswordError] = useState(false); + const [passwordErrorMessage, setPasswordErrorMessage] = useState(""); + const [open, setOpen] = useState(false); + const [openToast, setOpenToast] = useState(false); + const [openFailureToast, setOpenFailureToast] = useState(false); + const [failureToastMessage, setfailureToastMessage] = useState(""); + const [failureLogin, setfailureLogin] = useState(false); + + const handleClickOpen = () => { + setOpen(true); + }; + + const handleForgotPassword = (email: string) => { + loginHandler + .forgot_password(email) + .then(() => { + setOpenToast(true); + }) + .catch((error) => { + setfailureToastMessage( + `Please contact Hello Robot Support. ERROR ${error.code}: ${error.message}`, + ); + setOpenFailureToast(true); + }); + }; + + const handleClose = () => { + setOpen(false); + }; + + const handleToastClose = () => { + setOpenToast(false); + }; + + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + if (emailError || passwordError) { + return; + } + + const data = new FormData(event.currentTarget); + let l = { + email: data.get("email") as string, + password: data.get("password") as string, + remember: data.get("remember") ? true : false, + }; + loginHandler + .login(l["email"], l["password"], l["remember"]) + .then(() => { + // reset failure messages + setfailureLogin(false); + setOpenFailureToast(false); + setfailureToastMessage(""); + }) + .catch((error) => { + if (error.code === "auth/invalid-login-credentials") { + setfailureLogin(true); + } else if (error.code === "auth/user-not-found") { + setfailureLogin(true); + } else if (error.code === "auth/invalid-email") { + setfailureLogin(true); + } else { + setfailureToastMessage( + `Please contact Hello Robot Support. ERROR ${error.code}: ${error.message}`, + ); + setOpenFailureToast(true); + } + }); + }; + + const validateInputs = () => { + const email = document.getElementById("email") as HTMLInputElement; + const password = document.getElementById( + "password", + ) as HTMLInputElement; + + let isValid = true; + + if (!email.value || !/\S+@\S+\.\S+/.test(email.value)) { + setEmailError(true); + setEmailErrorMessage("Please enter a valid email address."); + isValid = false; + } else { + setEmailError(false); + setEmailErrorMessage(""); + } + + if (!password.value || password.value.length < 6) { + setPasswordError(true); + setPasswordErrorMessage( + "Password must be at least 6 characters long.", + ); + isValid = false; + } else { + setPasswordError(false); + setPasswordErrorMessage(""); + } + + return isValid; + }; + + return isTablet || isBrowser ? ( + + + {failureLogin ? ( + Incorrect email or password + ) : ( + <> + )} + + Sign in + + + + Email + + + + + Password + + Forgot your password? + + + + + + } + label="Remember me" + /> + + + + + + + + ) : ( +

Not implemented

+ ); +}; diff --git a/src/pages/home/tsx/components/SideBySideView.tsx b/src/pages/home/tsx/components/SideBySideView.tsx new file mode 100644 index 0000000..1222d1f --- /dev/null +++ b/src/pages/home/tsx/components/SideBySideView.tsx @@ -0,0 +1,112 @@ +import "home/css/SideBySideView.css"; +import React, { useEffect, useState } from "react"; +import { isTablet, isBrowser } from "react-device-detect"; +import Box from "@mui/material/Box"; +import Grid from "@mui/material/Grid2"; +import AppBar from "@mui/material/AppBar"; +import Toolbar from "@mui/material/Toolbar"; +import Typography from "@mui/material/Typography"; +import Button from "@mui/material/Button"; +import Snackbar from "@mui/material/Snackbar"; +import { Changelog } from "./Changelog"; +import { CallRobotSelector } from "./CallRobotSelector"; +import { loginHandler } from "../index"; + +export const SideBySideView = (props) => { + const [openFailureToast, setOpenFailureToast] = useState(false); + const [failureToastMessage, setfailureToastMessage] = useState(""); + + const handleLogout = () => { + loginHandler.logout().catch((error) => { + setfailureToastMessage( + `Please contact Hello Robot Support. ERROR ${error.code}: ${error.message}`, + ); + setOpenFailureToast(true); + }); + }; + + return isTablet || isBrowser ? ( + + + + + Stretch Web Teleop + + + + + + + + + + + + + + + ) : ( + + + + + Stretch Web Teleop + + + + + + + + + + + + + + + ); +}; diff --git a/src/pages/home/tsx/index.tsx b/src/pages/home/tsx/index.tsx new file mode 100644 index 0000000..536c14c --- /dev/null +++ b/src/pages/home/tsx/index.tsx @@ -0,0 +1,26 @@ +import "home/css/index.css"; +import React from "react"; +import { createRoot } from "react-dom/client"; +import { createLoginHandler } from "./utils"; +import { LoginHandler } from "./login_handler/LoginHandler"; +import { SideBySideView } from "./components/SideBySideView"; +import { LoginView } from "./components/LoginView"; + +export let loginHandler: LoginHandler; +const container = document.getElementById("root"); +const root = createRoot(container!); + +const loginHandlerReadyCallback = () => { + renderHomePage(); +}; +loginHandler = createLoginHandler(loginHandlerReadyCallback); + +function renderHomePage() { + loginHandler.loginState() == "authenticated" + ? (document.title = "Home - Stretch Web Teleop") + : (document.title = "Login - Stretch Web Teleop"); + + loginHandler.loginState() == "authenticated" + ? root.render() + : root.render(); +} diff --git a/src/pages/home/tsx/login_handler/FirebaseLoginHandler.tsx b/src/pages/home/tsx/login_handler/FirebaseLoginHandler.tsx new file mode 100644 index 0000000..9b3f726 --- /dev/null +++ b/src/pages/home/tsx/login_handler/FirebaseLoginHandler.tsx @@ -0,0 +1,108 @@ +import { LoginHandler } from "./LoginHandler"; +import { initializeApp, FirebaseOptions } from "firebase/app"; +import { + getAuth, + signInWithEmailAndPassword, + onAuthStateChanged, + sendPasswordResetEmail, + signOut, + setPersistence, + browserLocalPersistence, + browserSessionPersistence, + Auth, +} from "firebase/auth"; +import { getDatabase, ref, onValue, Database } from "firebase/database"; + +export class FirebaseLoginHandler extends LoginHandler { + private auth: Auth; + private _loginState: string; + private db: Database; + private uid: string; + + constructor( + onLoginHandlerReadyCallback: () => void, + config: FirebaseOptions, + ) { + super(onLoginHandlerReadyCallback); + this._loginState = "not_authenticated"; + const app = initializeApp(config); + this.auth = getAuth(app); + this.db = getDatabase(app); + + onAuthStateChanged(this.auth, (user) => { + this.uid = user ? user.uid : undefined; + this._loginState = user ? "authenticated" : "not_authenticated"; + this.onReadyCallback(); + }); + } + + public loginState(): string { + return this._loginState; + } + + public listRooms(resultCallback) { + if (this.uid === undefined) { + throw new Error( + "FirebaseLoginHandler.listRooms(): this.uid is null", + ); + } + + onValue(ref(this.db, "rooms/" + this.uid + "/robots"), (snapshot) => { + resultCallback(snapshot.val()); + }); + } + + public logout(): Promise { + // Tutorial here: + // https://firebase.google.com/docs/auth/web/password-auth#next_steps + + return new Promise((resolve, reject) => { + signOut(this.auth) + .then(() => { + resolve(undefined); + }) + .catch(reject); + }); + } + + public login( + username: string, + password: string, + remember_me: boolean, + ): Promise { + // Tutorial here: + // https://firebase.google.com/docs/auth/web/start?hl=en#sign_in_existing_users + // Auth State Persistence tutorial here: + // https://firebase.google.com/docs/auth/web/auth-state-persistence + + return new Promise((resolve, reject) => { + setPersistence( + this.auth, + remember_me + ? browserLocalPersistence + : browserSessionPersistence, + ) + .then(() => { + signInWithEmailAndPassword(this.auth, username, password) + .then((userCredential) => { + resolve(undefined); + }) + .catch(reject); + }) + .catch(reject); + }); + } + + public forgot_password(username: string): Promise { + // Tutorial here: + // https://firebase.google.com/docs/auth/web/manage-users?hl=en#send_a_password_reset_email + + return new Promise((resolve, reject) => { + sendPasswordResetEmail(this.auth, username) + .then(() => { + resolve(undefined); + }) + .catch(reject); + }); + } +} diff --git a/src/pages/home/tsx/login_handler/LocalLoginHandler.tsx b/src/pages/home/tsx/login_handler/LocalLoginHandler.tsx new file mode 100644 index 0000000..4d4920b --- /dev/null +++ b/src/pages/home/tsx/login_handler/LocalLoginHandler.tsx @@ -0,0 +1,60 @@ +import { LoginHandler } from "./LoginHandler"; +import io, { Socket } from "socket.io-client"; + +export class LocalLoginHandler extends LoginHandler { + private socket: Socket; + private _loginState: string; + + constructor(onLoginHandlerReadyCallback: () => void) { + super(onLoginHandlerReadyCallback); + this._loginState = "authenticated"; + this.socket = io(); + this.socket.on("connect", () => { + console.log("Connected to local socket"); + }); + + this.logout = this.logout.bind(this); + // Allow the initialization process to complete before invoking the callback + setTimeout(() => { + this.onReadyCallback(); + }, 0); + } + + public loginState(): string { + return this._loginState; + } + + public listRooms(resultCallback) { + this.socket.emit("list_rooms"); + this.socket.on("update_rooms", resultCallback); + } + + public logout(): Promise { + return new Promise((resolve, reject) => { + this._loginState = "not_authenticated"; + this.onReadyCallback(); + resolve(undefined); + }); + } + + public login( + username: string, + password: string, + remember_me: boolean, + ): Promise { + return new Promise((resolve, reject) => { + this._loginState = "authenticated"; + this.onReadyCallback(); + resolve(undefined); + }); + } + + public forgot_password(username: string): Promise { + return new Promise((resolve, reject) => { + reject( + Error("LocalLoginHandler.forgot_password() is not implemented"), + ); + // resolve(undefined); + }); + } +} diff --git a/src/pages/home/tsx/login_handler/LoginHandler.tsx b/src/pages/home/tsx/login_handler/LoginHandler.tsx new file mode 100644 index 0000000..ad87537 --- /dev/null +++ b/src/pages/home/tsx/login_handler/LoginHandler.tsx @@ -0,0 +1,21 @@ +export abstract class LoginHandler { + public onReadyCallback: () => void; + + constructor(onLoginHandlerReadyCallback: () => void) { + this.onReadyCallback = onLoginHandlerReadyCallback; + } + + public abstract loginState(): string; + + public abstract listRooms(resultCallback); + + public abstract logout(): Promise; + + public abstract login( + username: string, + password: string, + remember_me: boolean, + ): Promise; + + public abstract forgot_password(username: string): Promise; +} diff --git a/src/pages/home/tsx/utils.tsx b/src/pages/home/tsx/utils.tsx new file mode 100644 index 0000000..65ed512 --- /dev/null +++ b/src/pages/home/tsx/utils.tsx @@ -0,0 +1,28 @@ +import { FirebaseOptions } from "firebase/app"; +import { FirebaseLoginHandler } from "./login_handler/FirebaseLoginHandler"; +import { LocalLoginHandler } from "./login_handler/LocalLoginHandler"; + +/** + * Creates a login handler based on the `storage` property in the process + * environment. + * + * @returns the login handler + */ +export function createLoginHandler(loginHandlerReadyCallback: () => void) { + switch (process.env.storage) { + case "firebase": + const config: FirebaseOptions = { + apiKey: process.env.apiKey, + authDomain: process.env.authDomain, + databaseURL: process.env.databaseURL, + projectId: process.env.projectId, + storageBucket: process.env.storageBucket, + messagingSenderId: process.env.messagingSenderId, + appId: process.env.appId, + measurementId: process.env.measurementId, + }; + return new FirebaseLoginHandler(loginHandlerReadyCallback, config); + default: + return new LocalLoginHandler(loginHandlerReadyCallback); + } +} diff --git a/webpack.config.js b/webpack.config.js index c704081..57dc655 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -3,7 +3,7 @@ const HtmlWebpackPlugin = require("html-webpack-plugin"); const webpack = require("webpack"); const dotenv = require("dotenv"); -const pages = ["robot", "operator"]; +const pages = ["robot", "operator", "home"]; // call dotenv and it will return an Object with a parsed key const env = dotenv.config().parsed; @@ -52,7 +52,10 @@ module.exports = (env) => { new HtmlWebpackPlugin({ inject: true, template: `./src/pages/${page}/html/index.html`, - filename: `${page}/index.html`, + filename: + page == "home" + ? "index.html" + : `${page}/index.html`, chunks: [page], }), ), @@ -98,6 +101,7 @@ module.exports = (env) => { shared: path.resolve(__dirname, "./src/shared/"), operator: path.resolve(__dirname, "./src/pages/operator/"), robot: path.resolve(__dirname, "./src/pages/robot/"), + home: path.resolve(__dirname, "./src/pages/home/"), }, fallback: { fs: false,