You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@age.apache.org by ak...@apache.org on 2023/01/27 21:24:14 UTC

[age-viewer] branch main updated: viewer tutorial/tip (#104)

This is an automated email from the ASF dual-hosted git repository.

ako pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/age-viewer.git


The following commit(s) were added to refs/heads/main by this push:
     new 95c7cc2  viewer tutorial/tip (#104)
95c7cc2 is described below

commit 95c7cc2582ef8ee7a64f324b2784e9a37ec87746
Author: MJinH <97...@users.noreply.github.com>
AuthorDate: Fri Jan 27 13:24:08 2023 -0800

    viewer tutorial/tip (#104)
    
    Co-authored-by: moon19960501@gmail.com <we...@gmail.com>
---
 .../frame/containers/ServerStatusContainer.js      |   4 +-
 .../frame/presentations/ServerStatusFrame.jsx      |  97 ++++++------
 .../containers/Tutorial.js}                        |  19 +--
 .../modal/presentations/TutorialBody.jsx           |  87 +++++++++++
 .../modal/presentations/TutorialDialog.jsx         | 166 +++++++++++++++++++++
 .../modal/presentations/TutorialFooter.jsx         |  50 +++++++
 .../presentations/TutorialHeader.jsx}              |  36 +++--
 frontend/src/features/modal/ModalSlice.js          |  13 ++
 frontend/src/images/agce.gif                       | Bin 0 -> 1692177 bytes
 frontend/src/images/copy.png                       | Bin 0 -> 1402 bytes
 frontend/src/images/graphCSV.png                   | Bin 0 -> 11339 bytes
 frontend/src/images/graphMenu.png                  | Bin 0 -> 76907 bytes
 frontend/src/images/queryEditor.png                | Bin 0 -> 7217 bytes
 frontend/src/static/style.css                      |  18 +++
 14 files changed, 419 insertions(+), 71 deletions(-)

diff --git a/frontend/src/components/frame/containers/ServerStatusContainer.js b/frontend/src/components/frame/containers/ServerStatusContainer.js
index 07d5e87..b9f4b4c 100644
--- a/frontend/src/components/frame/containers/ServerStatusContainer.js
+++ b/frontend/src/components/frame/containers/ServerStatusContainer.js
@@ -21,16 +21,18 @@ import { connect } from 'react-redux';
 import { pinFrame, removeFrame } from '../../../features/frame/FrameSlice';
 import { generateCytoscapeMetadataElement } from '../../../features/cypher/CypherUtil';
 import ServerStatusFrame from '../presentations/ServerStatusFrame';
+import { openTutorial } from '../../../features/modal/ModalSlice';
 
 const mapStateToProps = (state) => {
   const generateElements = () => generateCytoscapeMetadataElement(state.metadata.rows);
 
   return {
     serverInfo: state.database,
+    isTutorial: state.modal.isTutorial,
     data: generateElements(),
   };
 };
 
-const mapDispatchToProps = { removeFrame, pinFrame };
+const mapDispatchToProps = { removeFrame, pinFrame, openTutorial };
 
 export default connect(mapStateToProps, mapDispatchToProps)(ServerStatusFrame);
diff --git a/frontend/src/components/frame/presentations/ServerStatusFrame.jsx b/frontend/src/components/frame/presentations/ServerStatusFrame.jsx
index 5d8d036..2e0e7b2 100644
--- a/frontend/src/components/frame/presentations/ServerStatusFrame.jsx
+++ b/frontend/src/components/frame/presentations/ServerStatusFrame.jsx
@@ -18,19 +18,23 @@
  */
 
 import React, { useEffect, useState } from 'react';
+import { useDispatch } from 'react-redux';
 import PropTypes from 'prop-types';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { faPlayCircle } from '@fortawesome/free-regular-svg-icons';
+import { faPlayCircle, faQuestionCircle } from '@fortawesome/free-regular-svg-icons';
 import { Col, Row } from 'antd';
 import { Button } from 'react-bootstrap';
 import MetadataCytoscapeChart from '../../cytoscape/MetadataCytoscapeChart';
 import InitGraphModal from '../../initializer/presentation/GraphInitializer';
 import Frame from '../Frame';
 import FrameStyles from '../Frame.module.scss';
+import Tutorial from '../../modal/containers/Tutorial';
+import { openTutorial } from '../../../features/modal/ModalSlice';
 
 const ServerStatusFrame = ({
-  refKey, isPinned, reqString, serverInfo, data,
+  refKey, isPinned, reqString, serverInfo, data, isTutorial,
 }) => {
+  const dispatch = useDispatch();
   const [elements, setElements] = useState({ edges: [], nodes: [] });
   const [showModal, setShow] = useState(false);
   const {
@@ -46,50 +50,54 @@ const ServerStatusFrame = ({
   const setContent = () => {
     if (status === 'connected') {
       return (
-        <div className={FrameStyles.FlexContentWrapper}>
-          <InitGraphModal show={showModal} setShow={setShow} />
-          <Row>
-            <Col span={6}>
-              <h3>Connection Status</h3>
-              <p>This is your current connection information.</p>
-            </Col>
-            <Col span={18}>
-              <p>
-                You are connected as user&nbsp;
-                <strong>{user}</strong>
-              </p>
-              <p>
-                to&nbsp;
-                <strong>
-                  {host}
-                  :
-                  {port}
-                  /
-                  {database}
-                </strong>
-              </p>
-              <p>
-                Graph path has been set to&nbsp;
-                <strong>{graph}</strong>
-              </p>
-            </Col>
-            <Col>
-              <p>
-                <Button onClick={() => setShow(!showModal)}>Create Graph</Button>
-              </p>
-            </Col>
-          </Row>
+        <>
+          { isTutorial && <Tutorial />}
+          <div className={FrameStyles.FlexContentWrapper}>
+            <InitGraphModal show={showModal} setShow={setShow} />
+            <Row>
+              <Col span={6}>
+                <h3>Connection Status</h3>
+                <p>This is your current connection information.</p>
+              </Col>
+              <Col span={18}>
+                <p>
+                  You are connected as user&nbsp;
+                  <strong>{user}</strong>
+                </p>
+                <p>
+                  to&nbsp;
+                  <strong>
+                    {host}
+                    :
+                    {port}
+                    /
+                    {database}
+                  </strong>
+                </p>
+                <p>
+                  Graph path has been set to&nbsp;
+                  <strong>{graph}</strong>
+                </p>
+              </Col>
+              <Col>
+                <p>
+                  <Button onClick={() => setShow(!showModal)}>Create Graph</Button>
+                  <FontAwesomeIcon onClick={() => dispatch(openTutorial())} icon={faQuestionCircle} size="lg" style={{ marginLeft: '1rem', cursor: 'pointer' }} />
+                </p>
+              </Col>
+            </Row>
 
-          <hr style={{
-            color: 'rgba(0,0,0,.125)',
-            backgroundColor: '#fff',
-            margin: '0px 10px 0px 10px',
-            height: '0.3px',
-          }}
-          />
+            <hr style={{
+              color: 'rgba(0,0,0,.125)',
+              backgroundColor: '#fff',
+              margin: '0px 10px 0px 10px',
+              height: '0.3px',
+            }}
+            />
 
-          <MetadataCytoscapeChart elements={elements} />
-        </div>
+            <MetadataCytoscapeChart elements={elements} />
+          </div>
+        </>
       );
     }
     if (status === 'disconnected') {
@@ -145,6 +153,7 @@ ServerStatusFrame.propTypes = {
   }).isRequired,
   // eslint-disable-next-line react/forbid-prop-types
   data: PropTypes.any.isRequired,
+  isTutorial: PropTypes.bool.isRequired,
 };
 
 export default ServerStatusFrame;
diff --git a/frontend/src/components/frame/containers/ServerStatusContainer.js b/frontend/src/components/modal/containers/Tutorial.js
similarity index 59%
copy from frontend/src/components/frame/containers/ServerStatusContainer.js
copy to frontend/src/components/modal/containers/Tutorial.js
index 07d5e87..d44ac38 100644
--- a/frontend/src/components/frame/containers/ServerStatusContainer.js
+++ b/frontend/src/components/modal/containers/Tutorial.js
@@ -18,19 +18,10 @@
  */
 
 import { connect } from 'react-redux';
-import { pinFrame, removeFrame } from '../../../features/frame/FrameSlice';
-import { generateCytoscapeMetadataElement } from '../../../features/cypher/CypherUtil';
-import ServerStatusFrame from '../presentations/ServerStatusFrame';
+import TutorialDialog from '../presentations/TutorialDialog';
+import { closeTutorial } from '../../../features/modal/ModalSlice';
 
-const mapStateToProps = (state) => {
-  const generateElements = () => generateCytoscapeMetadataElement(state.metadata.rows);
+const mapStateToProps = () => ({});
+const mapDispatchToProps = { closeTutorial };
 
-  return {
-    serverInfo: state.database,
-    data: generateElements(),
-  };
-};
-
-const mapDispatchToProps = { removeFrame, pinFrame };
-
-export default connect(mapStateToProps, mapDispatchToProps)(ServerStatusFrame);
+export default connect(mapStateToProps, mapDispatchToProps)(TutorialDialog);
diff --git a/frontend/src/components/modal/presentations/TutorialBody.jsx b/frontend/src/components/modal/presentations/TutorialBody.jsx
new file mode 100644
index 0000000..2c313ca
--- /dev/null
+++ b/frontend/src/components/modal/presentations/TutorialBody.jsx
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React, { useState, useEffect } from 'react';
+import PropTypes from 'prop-types';
+import { Modal, Image } from 'react-bootstrap';
+import Agce from '../../../images/agce.gif';
+import Query from '../../../images/queryEditor.png';
+import Copy from '../../../images/copy.png';
+import CSV from '../../../images/graphCSV.png';
+import Menu from '../../../images/graphMenu.png';
+
+const TutorialBody = ({ page, text, addiText }) => {
+  const [curImg, setCurImg] = useState();
+  const [addiImg, setAddiImg] = useState();
+  const [link, setLink] = useState();
+  useEffect(() => {
+    if (page === 1) {
+      setCurImg(Agce);
+      setAddiImg();
+    }
+    if (page === 2) {
+      setCurImg(Query);
+      setAddiImg(Copy);
+    }
+    if (page === 3) {
+      setLink(addiText);
+      setCurImg(CSV);
+      setAddiImg();
+    }
+    if (page === 4) setCurImg(Menu);
+    if (page === 5) setCurImg();
+  }, [page]);
+
+  return (
+    <Modal.Body style={{ height: '16rem', display: 'border-box', overflowY: 'scroll' }}>
+      <p>
+        {text}
+      </p>
+      <Image className="tutorial-img" src={curImg} fluid />
+      <br />
+      <br />
+      { page === 3
+        && (
+          <p>
+            The format of the csv files is as described here.
+            <br />
+            <br />
+            {link}
+          </p>
+        )}
+      { addiImg
+        ? (
+          <div style={{ margin: '1rem 0' }}>
+            <p>
+              {addiText}
+            </p>
+            <Image className="tutorial-img" src={addiImg} fluid />
+          </div>
+        ) : (<></>)}
+    </Modal.Body>
+  );
+};
+
+TutorialBody.propTypes = {
+  page: PropTypes.number.isRequired,
+  text: PropTypes.string.isRequired,
+  addiText: PropTypes.string.isRequired,
+};
+
+export default TutorialBody;
diff --git a/frontend/src/components/modal/presentations/TutorialDialog.jsx b/frontend/src/components/modal/presentations/TutorialDialog.jsx
new file mode 100644
index 0000000..d8bc626
--- /dev/null
+++ b/frontend/src/components/modal/presentations/TutorialDialog.jsx
@@ -0,0 +1,166 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React, { useState } from 'react';
+import PropTypes from 'prop-types';
+import { Modal } from 'react-bootstrap';
+import TutorialFooter from './TutorialFooter';
+import TutorialHeader from './TutorialHeader';
+import TutorialBody from './TutorialBody';
+
+const TutorialDialog = ({ closeTutorial }) => {
+  const [page, setPage] = useState(1);
+  const firstPage = (curPage) => {
+    if (curPage === 1) {
+      return (
+        <div className="tutorialModal-container">
+          <div
+            style={{ display: 'block', position: 'initial' }}
+          >
+            <Modal
+              show
+              backdrop="static"
+              keyboard={false}
+              style={{ width: '32rem' }}
+              className="tutorial-modal"
+            >
+              <TutorialHeader page={page} />
+              <TutorialBody
+                page={curPage}
+                text="Apache-Age Viewer is a web based user interface that provides visualization of graph data stored in a postgreSQL database with AGE extension. It is graph visualisation tool, for Apache AGE."
+              />
+              <TutorialFooter page={curPage} setPage={setPage} closeTutorial={closeTutorial} />
+            </Modal>
+          </div>
+        </div>
+      );
+    }
+    if (curPage === 2) {
+      return (
+        <div className="tutorialModal-container">
+          <div
+            style={{ display: 'block', position: 'initial' }}
+          >
+            <Modal
+              show
+              backdrop="static"
+              keyboard={false}
+              style={{ width: '32rem' }}
+              className="tutorial-modal"
+            >
+              <TutorialHeader page={page} />
+              <TutorialBody
+                page={curPage}
+                text="A graph consists of a set of vertices and edges, where each individual node and edge possesses a map of properties. You can use the Query Editor to create and run scripts containing cypher query statements."
+                addiText="You can get previous commands using ctrl + ↑/ctrl+ ↓ keyboard shortcut. The below icon also allows previous queries to be copied to editor."
+              />
+              <TutorialFooter page={curPage} setPage={setPage} closeTutorial={closeTutorial} />
+            </Modal>
+          </div>
+        </div>
+      );
+    }
+    if (curPage === 3) {
+      return (
+        <div className="tutorialModal-container">
+          <div
+            style={{ display: 'block', position: 'initial' }}
+          >
+            <Modal
+              show
+              backdrop="static"
+              keyboard={false}
+              style={{ width: '32rem' }}
+              className="tutorial-modal"
+            >
+              <TutorialHeader page={page} />
+              <TutorialBody
+                page={curPage}
+                text="This feature allows users to create and initialize a graph using csv file.ID is required when initializing."
+                addiText="https://age.apache.org/age-manual/master/intro/agload.html#explanation-about-the-csv-format"
+              />
+              <TutorialFooter page={curPage} setPage={setPage} closeTutorial={closeTutorial} />
+            </Modal>
+          </div>
+        </div>
+      );
+    }
+    if (curPage === 4) {
+      return (
+        <div className="tutorialModal-container">
+          <div
+            style={{ display: 'block', position: 'initial' }}
+          >
+            <Modal
+              show
+              backdrop="static"
+              keyboard={false}
+              style={{ width: '32rem' }}
+              className="tutorial-modal"
+            >
+              <TutorialHeader page={page} />
+              <TutorialBody
+                page={curPage}
+                text="After creating nodes/edges and running queries, you can select a menu item and perform a command on either a node or a edge in frame."
+              />
+              <TutorialFooter page={curPage} setPage={setPage} closeTutorial={closeTutorial} />
+            </Modal>
+          </div>
+        </div>
+      );
+    }
+    if (curPage === 5) {
+      return (
+        <div className="tutorialModal-container">
+          <div
+            style={{ display: 'block', position: 'initial' }}
+          >
+            <Modal
+              show
+              backdrop="static"
+              keyboard={false}
+              style={{ width: '32rem' }}
+              className="tutorial-modal"
+            >
+              <TutorialHeader page={page} />
+              <TutorialBody
+                page={curPage}
+                text="There are multiple ways you can contribute to the Apache AGE and Apache AGE Viewer projects. If you are interested in the project and looking for ways to help, consult the list of projects in the Apache AGE and AGE Viewer GitHubs."
+              />
+              <TutorialFooter page={curPage} setPage={setPage} closeTutorial={closeTutorial} />
+            </Modal>
+          </div>
+        </div>
+      );
+    }
+    return null;
+  };
+
+  return (
+    <>
+      {firstPage(page)}
+    </>
+  );
+};
+
+TutorialDialog.propTypes = {
+  closeTutorial: PropTypes.func.isRequired,
+};
+
+export default TutorialDialog;
diff --git a/frontend/src/components/modal/presentations/TutorialFooter.jsx b/frontend/src/components/modal/presentations/TutorialFooter.jsx
new file mode 100644
index 0000000..8085597
--- /dev/null
+++ b/frontend/src/components/modal/presentations/TutorialFooter.jsx
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React, { useState, useEffect } from 'react';
+import PropTypes from 'prop-types';
+import { Modal, Button } from 'react-bootstrap';
+
+const TutorialFooter = ({ page, setPage, closeTutorial }) => {
+  const [curPage, setCurPage] = useState();
+
+  useEffect(() => {
+    setCurPage(page);
+  }, [page]);
+
+  return (
+    <Modal.Footer>
+      <div>
+        <Button onClick={() => closeTutorial()} className="tutorial-button" variant="secondary">Close</Button>
+      </div>
+      <div>
+        <Button className="tutorial-button" variant={curPage === 1 ? 'outline-secondary' : 'secondary'} style={{ marginRight: '1rem' }} onClick={() => { setPage(curPage > 1 ? curPage - 1 : curPage); }}>Previous Tip</Button>
+        <Button className="tutorial-button" variant={curPage === 5 ? 'outline-primary' : 'primary'} onClick={() => { setPage(curPage < 5 ? curPage + 1 : curPage); }}>Next Tip</Button>
+      </div>
+    </Modal.Footer>
+  );
+};
+
+TutorialFooter.propTypes = {
+  page: PropTypes.number.isRequired,
+  setPage: PropTypes.func.isRequired,
+  closeTutorial: PropTypes.func.isRequired,
+};
+
+export default TutorialFooter;
diff --git a/frontend/src/components/frame/containers/ServerStatusContainer.js b/frontend/src/components/modal/presentations/TutorialHeader.jsx
similarity index 53%
copy from frontend/src/components/frame/containers/ServerStatusContainer.js
copy to frontend/src/components/modal/presentations/TutorialHeader.jsx
index 07d5e87..5628a6b 100644
--- a/frontend/src/components/frame/containers/ServerStatusContainer.js
+++ b/frontend/src/components/modal/presentations/TutorialHeader.jsx
@@ -17,20 +17,32 @@
  * under the License.
  */
 
-import { connect } from 'react-redux';
-import { pinFrame, removeFrame } from '../../../features/frame/FrameSlice';
-import { generateCytoscapeMetadataElement } from '../../../features/cypher/CypherUtil';
-import ServerStatusFrame from '../presentations/ServerStatusFrame';
+import React, { useState, useEffect } from 'react';
+import PropTypes from 'prop-types';
+import { Modal } from 'react-bootstrap';
 
-const mapStateToProps = (state) => {
-  const generateElements = () => generateCytoscapeMetadataElement(state.metadata.rows);
+const TutorialHeader = ({ page }) => {
+  const [curPage, setCurPage] = useState();
 
-  return {
-    serverInfo: state.database,
-    data: generateElements(),
-  };
+  useEffect(() => {
+    setCurPage(page);
+  }, [page]);
+
+  return (
+    <Modal.Header
+      style={{
+        padding: '0.3rem 0.5rem 0 0.5rem', borderBottom: '1px solid black', margin: '0', background: '#A9A9A9',
+      }}
+    >
+      <Modal.Title style={{ fontSize: '0.88rem', paddingBottom: '0px', color: '#F0FFF0' }}>
+        {`Tip of AGE Viewer -${curPage}`}
+      </Modal.Title>
+    </Modal.Header>
+  );
 };
 
-const mapDispatchToProps = { removeFrame, pinFrame };
+TutorialHeader.propTypes = {
+  page: PropTypes.string.isRequired,
+};
 
-export default connect(mapStateToProps, mapDispatchToProps)(ServerStatusFrame);
+export default TutorialHeader;
diff --git a/frontend/src/features/modal/ModalSlice.js b/frontend/src/features/modal/ModalSlice.js
index a9003df..4eb2697 100644
--- a/frontend/src/features/modal/ModalSlice.js
+++ b/frontend/src/features/modal/ModalSlice.js
@@ -24,6 +24,7 @@ const ModalSlice = createSlice({
   name: 'modal',
   initialState: {
     isOpen: false,
+    isTutorial: false,
     graphHistory: [],
     elementHistory: [],
   },
@@ -38,6 +39,16 @@ const ModalSlice = createSlice({
         state.isOpen = false;
       },
     },
+    openTutorial: {
+      reducer: (state) => {
+        state.isTutorial = true;
+      },
+    },
+    closeTutorial: {
+      reducer: (state) => {
+        state.isTutorial = false;
+      },
+    },
     addGraphHistory: {
       reducer: (state, action) => {
         state.graphHistory.push(action.payload.graph);
@@ -66,6 +77,8 @@ const ModalSlice = createSlice({
 export const {
   openModal,
   closeModal,
+  openTutorial,
+  closeTutorial,
   addGraphHistory,
   addElementHistory,
   removeGraphHistory,
diff --git a/frontend/src/images/agce.gif b/frontend/src/images/agce.gif
new file mode 100644
index 0000000..8536175
Binary files /dev/null and b/frontend/src/images/agce.gif differ
diff --git a/frontend/src/images/copy.png b/frontend/src/images/copy.png
new file mode 100644
index 0000000..f70d29c
Binary files /dev/null and b/frontend/src/images/copy.png differ
diff --git a/frontend/src/images/graphCSV.png b/frontend/src/images/graphCSV.png
new file mode 100644
index 0000000..578f53b
Binary files /dev/null and b/frontend/src/images/graphCSV.png differ
diff --git a/frontend/src/images/graphMenu.png b/frontend/src/images/graphMenu.png
new file mode 100644
index 0000000..14bff11
Binary files /dev/null and b/frontend/src/images/graphMenu.png differ
diff --git a/frontend/src/images/queryEditor.png b/frontend/src/images/queryEditor.png
new file mode 100644
index 0000000..820e5d2
Binary files /dev/null and b/frontend/src/images/queryEditor.png differ
diff --git a/frontend/src/static/style.css b/frontend/src/static/style.css
index 2dd133d..62a8f43 100644
--- a/frontend/src/static/style.css
+++ b/frontend/src/static/style.css
@@ -808,8 +808,26 @@ textarea.editorTextarea
     height: 100%;
     background: rgba(0, 0, 0, 0.7);
     z-index: 9999;
+    display: flex;
     font-size: 16px;
     display:flex;
     align-items: center;
     justify-content: center;
+}
+
+.tutorial-modal {
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%);
+    display:flex;
+    align-items: center;
+    justify-content: center;
+    height: auto;
+}
+
+.tutorial-button {
+    font-size: 0.7rem;
+    font-weight: bold;
+    padding: 0.2rem 0.35rem;
 }
\ No newline at end of file