From 98006e85d38369bd75855df4215f8dc60717bc61 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jo=C3=A3o=20Magalh=C3=A3es?= <joamag@gmail.com>
Date: Fri, 18 Nov 2022 10:38:34 +0000
Subject: [PATCH] feat: added help panel

---
 CHANGELOG.md                                  |  1 +
 frontends/web/react/app.tsx                   | 30 ++++++++++---
 frontends/web/react/components/help/help.css  | 19 ++++++++
 frontends/web/react/components/help/help.tsx  | 44 +++++++++++++++++++
 frontends/web/react/components/index.ts       |  1 +
 .../web/react/components/modal/modal.tsx      | 33 +++++++++-----
 frontends/web/res/help.svg                    |  1 +
 7 files changed, 113 insertions(+), 16 deletions(-)
 create mode 100644 frontends/web/react/components/help/help.css
 create mode 100644 frontends/web/react/components/help/help.tsx
 create mode 100644 frontends/web/res/help.svg

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 877f3a71..0ccb5a5b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ### Added
 
 * Support for Ctrl+D (Speedup) and Ctrl+K (Keyboard toggle) shortcuts
+* Help panel
 
 ### Changed
 
diff --git a/frontends/web/react/app.tsx b/frontends/web/react/app.tsx
index 67ee850a..f8f08e9f 100644
--- a/frontends/web/react/app.tsx
+++ b/frontends/web/react/app.tsx
@@ -1,4 +1,4 @@
-import React, { FC, useEffect, useRef, useState } from "react";
+import React, { FC, ReactNode, useEffect, useRef, useState } from "react";
 import ReactDOM from "react-dom/client";
 
 declare const require: any;
@@ -12,6 +12,7 @@ import {
     Display,
     DrawHandler,
     Footer,
+    Help,
     Info,
     KeyboardChip8,
     KeyboardGB,
@@ -66,6 +67,7 @@ export const EmulatorApp: FC<EmulatorAppProps> = ({
     const [keyaction, setKeyaction] = useState<string>();
     const [modalTitle, setModalTitle] = useState<string>();
     const [modalText, setModalText] = useState<string>();
+    const [modalContents, setModalContents] = useState<ReactNode>();
     const [modalVisible, setModalVisible] = useState(false);
     const [toastText, setToastText] = useState<string>();
     const [toastError, setToastError] = useState(false);
@@ -213,17 +215,22 @@ export const EmulatorApp: FC<EmulatorAppProps> = ({
     const getBackground = () => backgrounds[backgroundIndex];
 
     const showModal = async (
-        text: string,
-        title = "Alert"
+        title = "Alert",
+        text?: string,
+        contents?: ReactNode
     ): Promise<boolean> => {
-        setModalText(text);
         setModalTitle(title);
+        setModalText(text);
+        setModalContents(contents);
         setModalVisible(true);
         const result = (await new Promise((resolve) => {
             modalCallbackRef.current = resolve;
         })) as boolean;
         return result;
     };
+    const showHelp = async (title = "Help") => {
+        await showModal(title, undefined, <Help />);
+    };
     const showToast = async (text: string, error = false, timeout = 3500) => {
         setToastText(text);
         setToastError(error);
@@ -286,8 +293,8 @@ export const EmulatorApp: FC<EmulatorAppProps> = ({
     const onBenchmarkClick = async () => {
         if (!emulator.benchmark) return;
         const result = await showModal(
-            "Are you sure you want to start a benchmark?\nThe benchmark is considered an expensive operation!",
-            "Confirm"
+            "Confirm",
+            "Are you sure you want to start a benchmark?\nThe benchmark is considered an expensive operation!"
         );
         if (!result) return;
         const { delta, count, frequency_mhz } = emulator.benchmark();
@@ -310,6 +317,9 @@ export const EmulatorApp: FC<EmulatorAppProps> = ({
     const onInformationClick = () => {
         setInfoVisible(!infoVisible);
     };
+    const onHelpClick = () => {
+        showHelp();
+    };
     const onDebugClick = () => {
         setDebugVisible(!debugVisible);
     };
@@ -378,6 +388,7 @@ export const EmulatorApp: FC<EmulatorAppProps> = ({
             <Modal
                 title={modalTitle}
                 text={modalText}
+                contents={modalContents}
                 visible={modalVisible}
                 onConfirm={onModalConfirm}
                 onCancel={onModalCancel}
@@ -621,6 +632,13 @@ export const EmulatorApp: FC<EmulatorAppProps> = ({
                             style={["simple", "border", "padded"]}
                             onClick={onInformationClick}
                         />
+                        <Button
+                            text={"Help"}
+                            image={require("../res/help.svg")}
+                            imageAlt="help"
+                            style={["simple", "border", "padded"]}
+                            onClick={onHelpClick}
+                        />
                         {hasFeature(Feature.Debug) && (
                             <Button
                                 text={"Debug"}
diff --git a/frontends/web/react/components/help/help.css b/frontends/web/react/components/help/help.css
new file mode 100644
index 00000000..33a52b36
--- /dev/null
+++ b/frontends/web/react/components/help/help.css
@@ -0,0 +1,19 @@
+.help > ul {
+    padding-left: 0px;
+    font-size: 18px;
+    line-height: 22px;
+    list-style: none;
+    margin: 0px 0px 0px 0px;
+}
+
+.help > ul > li {
+    margin: 10px 0px 4px 0px;
+}
+
+.help > ul .key {
+    border: 1px solid #ffffff;
+    display: inline-block;
+    padding: 0px 6px 0px 6px;
+    border-radius: 6px;
+    font-size: 16px;
+}
diff --git a/frontends/web/react/components/help/help.tsx b/frontends/web/react/components/help/help.tsx
new file mode 100644
index 00000000..f7511cf9
--- /dev/null
+++ b/frontends/web/react/components/help/help.tsx
@@ -0,0 +1,44 @@
+import React, { FC } from "react";
+
+import "./help.css";
+
+type HelpProps = {
+    style?: string[];
+};
+
+export const Help: FC<HelpProps> = ({ style = [] }) => {
+    const classes = () => ["help", ...style].join(" ");
+    return (
+        <div className={classes()}>
+            <ul>
+                <li>
+                    <span className="key">Enter</span> - Start
+                </li>
+                <li>
+                    <span className="key">Space</span> - Select
+                </li>
+                <li>
+                    <span className="key">A</span> - A
+                </li>
+                <li>
+                    <span className="key">S</span> - B
+                </li>
+                <li>
+                    <span className="key">Escape</span> - Exit fullscreen
+                </li>
+                <li>
+                    <span className="key">Ctrl + D</span> - Turbo speed
+                </li>
+                <li>
+                    <span className="key">Ctrl + F</span> - Toggle fullscreen
+                </li>
+                <li>
+                    <span className="key">Ctrl + K</span> - Toggle on-screen
+                    keyboard
+                </li>
+            </ul>
+        </div>
+    );
+};
+
+export default Help;
diff --git a/frontends/web/react/components/index.ts b/frontends/web/react/components/index.ts
index a2980dab..693c7490 100644
--- a/frontends/web/react/components/index.ts
+++ b/frontends/web/react/components/index.ts
@@ -5,6 +5,7 @@ export * from "./button-switch/button-switch";
 export * from "./canvas/canvas";
 export * from "./display/display";
 export * from "./footer/footer";
+export * from "./help/help";
 export * from "./info/info";
 export * from "./keyboard-chip8/keyboard-chip8";
 export * from "./keyboard-gb/keyboard-gb";
diff --git a/frontends/web/react/components/modal/modal.tsx b/frontends/web/react/components/modal/modal.tsx
index 7740e413..e9004167 100644
--- a/frontends/web/react/components/modal/modal.tsx
+++ b/frontends/web/react/components/modal/modal.tsx
@@ -1,4 +1,4 @@
-import React, { FC, useEffect, useRef } from "react";
+import React, { FC, ReactNode, useEffect, useRef } from "react";
 import Button from "../button/button";
 
 import "./modal.css";
@@ -8,6 +8,7 @@ declare const require: any;
 type ModalProps = {
     title?: string;
     text?: string;
+    contents?: ReactNode;
     visible?: boolean;
     buttons?: boolean;
     overlayClose?: boolean;
@@ -18,9 +19,10 @@ type ModalProps = {
 
 export const Modal: FC<ModalProps> = ({
     title = "Alert",
-    text = "Do you confirm the following operation?",
+    text,
+    contents,
     visible = false,
-    buttons = true,
+    buttons,
     overlayClose = true,
     style = [],
     onConfirm,
@@ -28,6 +30,10 @@ export const Modal: FC<ModalProps> = ({
 }) => {
     const classes = () =>
         ["modal", visible ? "visible" : "", ...style].join(" ");
+    text =
+        text ??
+        (contents ? undefined : "Do you confirm the following operation?");
+    buttons = buttons ?? (contents ? false : true);
     const modalRef = useRef<HTMLDivElement>(null);
     useEffect(() => {
         const onKeyDown = (event: KeyboardEvent) => {
@@ -45,9 +51,12 @@ export const Modal: FC<ModalProps> = ({
             modalRef.current?.focus();
         }
     }, [visible]);
-    const getTextHtml = (separator = /\n/g) => ({
-        __html: text.replace(separator, "<br/>")
-    });
+    const getTextHtml = (separator = /\n/g) =>
+        text
+            ? {
+                  __html: text.replace(separator, "<br/>")
+              }
+            : undefined;
     const onWindowClick = (
         event: React.MouseEvent<HTMLDivElement, MouseEvent>
     ) => {
@@ -73,10 +82,14 @@ export const Modal: FC<ModalProps> = ({
                     />
                 </div>
                 <h2 className="modal-title">{title}</h2>
-                <p
-                    className="modal-text"
-                    dangerouslySetInnerHTML={getTextHtml()}
-                ></p>
+                {contents ? (
+                    contents
+                ) : (
+                    <p
+                        className="modal-text"
+                        dangerouslySetInnerHTML={getTextHtml()}
+                    ></p>
+                )}
                 {buttons && (
                     <div className="modal-buttons">
                         <Button
diff --git a/frontends/web/res/help.svg b/frontends/web/res/help.svg
new file mode 100644
index 00000000..d450cbaa
--- /dev/null
+++ b/frontends/web/res/help.svg
@@ -0,0 +1 @@
+<svg role="img" xmlns="http://www.w3.org/2000/svg" width="48px" height="48px" viewBox="0 0 24 24" aria-labelledby="helpIconTitle" stroke="#ffffff" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter" fill="none" color="#ffffff"> <title id="helpIconTitle">Help</title> <path d="M12 14C12 12 13.576002 11.6652983 14.1186858 11.1239516 14.663127 10.5808518 15 9.82976635 15 9 15 7.34314575 13.6568542 6 12 6 11.1040834 6 10.2998929 6.39272604 9.75018919 7.01541737 9.49601109 7.30334431 9.29624369 7.64043912 9.16697781 8.01061095"/> <line x1="12" y1="17" x2="12" y2="17"/> <circle cx="12" cy="12" r="10"/> </svg>
\ No newline at end of file
-- 
GitLab