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 (
+
+ Call Robot
+
+ );
+ case "offline":
+ return (
+
+ Call Robot
+
+ );
+ case "occupied":
+ return (
+
+ Call Robot
+
+ );
+ default:
+ return (
+
+ Call Robot
+
+ );
+ }
+}
+
+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.
+
+
+
+
+ Cancel
+
+ Continue
+
+
+
+ ) : (
+ 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"
+ />
+
+
+ Sign in
+
+
+
+
+
+
+ ) : (
+ 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
+
+
+ Logout
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ) : (
+
+
+
+
+ Stretch Web Teleop
+
+
+ Logout
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
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,