Sfoglia il codice sorgente

Add studio space to create games

Jeremie Pardou-Piquemal 3 anni fa
parent
commit
f54e5aeb03

+ 6 - 5
src/App.jsx

@@ -13,8 +13,9 @@ import { ToastContainer } from "react-toastify";
 
 import "react-toastify/dist/ReactToastify.css";
 import "rc-slider/assets/index.css";
+import "./react-confirm-alert.css";
 
-import GameListView from "./views/GameListView";
+import Home from "./views/Home";
 import GameView from "./views/GameView";
 import AuthView from "./views/AuthView";
 
@@ -42,13 +43,13 @@ const App = () => {
             <Route path="/game/:gameId?">
               <GameView edit />
             </Route>
-            <Route exact path="/games/">
-              <GameListView />
-            </Route>
             <Route exact path="/login/:userHash/:token">
               <AuthView />
             </Route>
-            <Redirect from="/" to="/games/" />
+            <Redirect from="/" to="/games/" exact />
+            <Route path="/">
+              <Home />
+            </Route>
           </Switch>
         </Router>
       </RecoilRoot>

+ 0 - 1
src/components/SelectedItemsPane.jsx

@@ -19,7 +19,6 @@ import SidePanel from "../ui/SidePanel";
 import ItemFormFactory from "./boardComponents/ItemFormFactory";
 
 // import { confirmAlert } from "react-confirm-alert";
-import "react-confirm-alert/src/react-confirm-alert.css";
 
 import { useTranslation } from "react-i18next";
 

+ 76 - 0
src/react-confirm-alert.css

@@ -0,0 +1,76 @@
+body.react-confirm-alert-body-element {
+  overflow: hidden;
+}
+
+.react-confirm-alert-blur {
+  filter: blur(2px);
+}
+
+.react-confirm-alert-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  z-index: 99;
+  background: rgba(0, 0, 0, 0.4);
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  opacity: 0;
+  animation: react-confirm-alert-fadeIn 0.3s 0.2s forwards;
+}
+
+.react-confirm-alert-body {
+  font-family: Arial, Helvetica, sans-serif;
+  width: 400px;
+  padding: 30px;
+  text-align: left;
+  background-color: var(--color-blueGrey);
+  color: var(--font-color);
+  border-radius: 5px;
+  box-shadow: rgba(0, 0, 0, 0.19) 0px 10px 20px, rgba(0, 0, 0, 0.23) 0px 6px 6px;
+}
+
+.react-confirm-alert-svg {
+  position: absolute;
+  top: 0;
+  left: 0;
+}
+
+.react-confirm-alert-body > h1 {
+  margin-top: 0;
+}
+
+.react-confirm-alert-body > h3 {
+  margin: 0;
+  font-size: 16px;
+}
+
+.react-confirm-alert-button-group {
+  display: flex;
+  justify-content: flex-end;
+  margin-top: 40px;
+}
+
+.react-confirm-alert-button-group > button {
+  outline: none;
+  background: #333;
+  border: none;
+  display: inline-block;
+  padding: 6px 18px;
+  color: var(--font-color);
+  margin-right: 10px;
+  border-radius: 2px;
+  font-size: 16px;
+  cursor: pointer;
+}
+
+@keyframes react-confirm-alert-fadeIn {
+  from {
+    opacity: 0;
+  }
+  to {
+    opacity: 1;
+  }
+}

+ 14 - 10
src/ui/Touch.jsx

@@ -1,5 +1,7 @@
 import React from "react";
+import { Link } from "react-router-dom";
 import styled from "styled-components";
+import { useHistory } from "react-router-dom";
 
 const StyledWrapper = styled.div`
   display: flex;
@@ -32,19 +34,21 @@ const StyledButton = styled.div.attrs(({ active }) => ({
   box-shadow: 3px 3px 6px #00000050;
 `;
 
-const Touch = ({
-  onClick = () => {},
-  icon,
-  title,
-  alt,
-  active,
-  label,
-  ...rest
-}) => {
+const Touch = ({ onClick, to, icon, title, alt, active, label, ...rest }) => {
   // Touch icon
+  const history = useHistory();
+  const handleClick = React.useCallback(() => {
+    if (onClick) {
+      onClick();
+    }
+    if (to) {
+      history.push(to);
+    }
+  }, [history, to, onClick]);
+
   return (
     <StyledWrapper {...rest}>
-      <StyledButton onClick={onClick} active={active}>
+      <StyledButton onClick={handleClick} active={active}>
         <img
           src={`https://icongr.am/entypo/${icon}.svg?size=24&color=f9fbfa`}
           alt={alt}

+ 29 - 194
src/views/GameListView.jsx

@@ -1,66 +1,13 @@
 import React from "react";
-import { Link, useLocation } from "react-router-dom";
 import { useTranslation, Trans } from "react-i18next";
 import styled from "styled-components";
-//import CookieConsent from "react-cookie-consent";
 
 import { getGames } from "../utils/api";
-import Account from "../components/Account";
 import useAuth from "../hooks/useAuth";
 
-import "react-confirm-alert/src/react-confirm-alert.css";
-import useLocalStorage from "../hooks/useLocalStorage";
+import StyledGameList from "./StyledGameList";
 import GameListItem from "./GameListItem";
 
-import AboutModal from "./AboutModal";
-import Brand from "./Brand";
-
-const GameView = styled.div`
-  min-height: 100vh;
-  flex-direction: column;
-  & > footer {
-    width: 100%;
-    grid-column: 1 / 4;
-    padding: 0.5em 0;
-    text-align: center;
-    background-color: #00000099;
-  }
-`;
-
-const Nav = styled.nav`
-  background-color: var(--bg-color);
-  position: relative;
-  padding: 0 5%;
-  display: flex;
-  align-items: center;
-
-  & button,
-  & .button {
-    background: none;
-    text-transform: uppercase;
-    font-weight: 300;
-    font-size: 1.3em;
-    border-radius: 0;
-    color: var(--font-color2);
-    margin: 0 0.5em;
-    letter-spacing: -1px;
-    padding: 0;
-  }
-
-  & button:hover,
-  & .button:hover {
-    color: var(--font-color);
-    border-bottom: 1px solid var(--color-primary);
-  }
-
-  @media screen and (max-width: 640px) {
-    & .button,
-    & button {
-      display: none;
-    }
-  }
-`;
-
 const Header = styled.header`
   background-color: var(--bg-color);
   position: relative;
@@ -131,48 +78,11 @@ const Content = styled.div`
   background-color: var(--bg-secondary-color);
 `;
 
-const GameList = styled.ul`
-  list-style: none;
-  margin: 0;
-  padding: 0 5%;
-  display: grid;
-  grid-template-columns: repeat(3, 1fr);
-  grid-gap: 20px;
-  grid-auto-rows: minmax(100px, auto);
-
-  @media screen and (max-width: 1024px) {
-    grid-template-columns: repeat(2, 1fr);
-  }
-
-  @media screen and (max-width: 640px) {
-    grid-template-columns: repeat(1, 1fr);
-  }
-`;
-
-const useQuery = () => {
-  return new URLSearchParams(useLocation().search);
-};
-
 const GameListView = () => {
   const { t } = useTranslation();
 
-  const [isBeta, setIsBeta] = useLocalStorage("isBeta", false);
-
-  if (isBeta) {
-    console.log("Beta activated");
-  }
-
-  /*const [cookieConsent, setCookieConsent] = useLocalStorage(
-    "cookieConsent",
-    false
-  );*/
-  const cookieConsent = true;
-  const [showAboutModal, setShowAboutModal] = React.useState(false);
-
-  let query = useQuery();
-
   const [gameList, setGameList] = React.useState([]);
-  const { isAuthenticated, userId } = useAuth();
+  const { userId } = useAuth();
 
   React.useEffect(() => {
     let mounted = true;
@@ -204,110 +114,35 @@ const GameListView = () => {
     };
   }, []);
 
-  const onGameDelete = React.useCallback((idToRemove) => {
-    setGameList((prevList) => {
-      return prevList.filter(({ id }) => id !== idToRemove);
-    });
-  }, []);
-
-  const forceBeta = query.get("beta") === "true";
-
-  React.useEffect(() => {
-    if (forceBeta) {
-      setIsBeta(true);
-    }
-  }, [forceBeta, setIsBeta]);
-
   return (
     <>
-      <GameView>
-        <Nav>
-          <Brand />
-          {isAuthenticated && (
-            <Link to={"/game/"} className="button new-game">
-              {t("Create new game")}
-            </Link>
-          )}
-          <Account className="login" disabled={!cookieConsent} />
-          <a
-            className="icon button"
-            href="https://github.com/jrmi/airboardgame"
-            target="_blank"
-            rel="noreferrer"
-          >
-            <img
-              src="https://icongr.am/feather/github.svg?size=16&color=ffffff"
-              alt={t("Github")}
-              title={t("Github")}
-            />
-          </a>
-        </Nav>
-        <Header>
-          <Trans i18nKey="baseline">
-            <h2 className="baseline">
-              Play board games online
-              <br />
-              with your friends - for free!
-            </h2>
-            <p className="subbaseline">
-              Choose from our selection or create your own.
-              <br />
-              No need to sign up. Just start a game and share the link with your
-              friends.
-            </p>
-          </Trans>
-        </Header>
-        <Content>
-          <Filter>
-            <h2 className="incentive">{t("Start a game now")}</h2>
-          </Filter>
-          <GameList>
-            {gameList
-              .filter(
-                ({ published, owner }) =>
-                  published || (userId && (!owner || owner === userId))
-              )
-              .map((game) => (
-                <GameListItem
-                  key={game.id}
-                  game={game}
-                  userId={userId}
-                  onDelete={onGameDelete}
-                />
-              ))}
-          </GameList>
-        </Content>
-        <footer>
-          <button
-            className="button clear"
-            onClick={() => setShowAboutModal(true)}
-          >
-            {t("About")}
-          </button>
-        </footer>
-      </GameView>
-      <AboutModal show={showAboutModal} setShow={setShowAboutModal} />
-      {/*<CookieConsent
-        location="bottom"
-        buttonText={t("Got it!")}
-        enableDeclineButton
-        declineButtonText={t("Refuse")}
-        cookieName="cookieConsent"
-        onAccept={() => setCookieConsent(true)}
-        containerClasses="cookie"
-        expires={150}
-        buttonStyle={{
-          color: "var(--font-color)",
-          backgroundColor: "var(--color-secondary)",
-        }}
-        style={{
-          backgroundColor: "#000000CE",
-          boxShadow:
-            "rgba(50, 50, 93, 0.25) 0px 6px 12px -2px, rgba(0, 0, 0, 0.3) 0px 3px 7px -3px",
-        }}
-      >
-        {t("This site use a cookie only to know if your are authenticated.")}
-      </CookieConsent>*/}
+      <Header>
+        <Trans i18nKey="baseline">
+          <h2 className="baseline">
+            Play board games online
+            <br />
+            with your friends - for free!
+          </h2>
+          <p className="subbaseline">
+            Choose from our selection or create your own.
+            <br />
+            No need to sign up. Just start a game and share the link with your
+            friends.
+          </p>
+        </Trans>
+      </Header>
+      <Content>
+        <Filter>
+          <h2 className="incentive">{t("Start a game now")}</h2>
+        </Filter>
+        <StyledGameList>
+          {gameList
+            .filter(({ published }) => published)
+            .map((game) => (
+              <GameListItem key={game.id} game={game} userId={userId} />
+            ))}
+        </StyledGameList>
+      </Content>
     </>
   );
 };

+ 100 - 0
src/views/GameStudio.jsx

@@ -0,0 +1,100 @@
+import React from "react";
+import { Redirect } from "react-router-dom";
+import { useTranslation } from "react-i18next";
+import styled from "styled-components";
+
+import { getGames } from "../utils/api";
+import useAuth from "../hooks/useAuth";
+
+import StyledGameList from "./StyledGameList";
+import NewGameItem from "./NewGameItem";
+import GameListItem from "./GameListItem";
+
+const Filter = styled.div`
+  & .incentive {
+    width: 100%;
+    text-align: center;
+    font-size: 3.5vw;
+    padding: 0.5em;
+    margin: 0;
+  }
+  @media screen and (max-width: 1024px) {
+    & .incentive {
+      font-size: 32px;
+    }
+  }
+`;
+
+const Content = styled.div`
+  background-color: var(--bg-secondary-color);
+`;
+
+const GameListView = () => {
+  const { t } = useTranslation();
+
+  const [gameList, setGameList] = React.useState([]);
+  const { isAuthenticated, userId } = useAuth();
+
+  React.useEffect(() => {
+    let mounted = true;
+
+    const loadGames = async () => {
+      const content = await getGames();
+      if (!mounted) return;
+
+      setGameList(
+        content.sort((a, b) => {
+          const [nameA, nameB] = [
+            a.board.defaultName || a.board.name,
+            b.board.defaultName || b.board.name,
+          ];
+          if (nameA < nameB) {
+            return -1;
+          }
+          if (nameA > nameB) {
+            return 1;
+          }
+          return 0;
+        })
+      );
+    };
+
+    loadGames();
+    return () => {
+      mounted = false;
+    };
+  }, []);
+
+  const onGameDelete = React.useCallback((idToRemove) => {
+    setGameList((prevList) => {
+      return prevList.filter(({ id }) => id !== idToRemove);
+    });
+  }, []);
+
+  if (!isAuthenticated) {
+    return <Redirect to="/games/" />;
+  }
+
+  return (
+    <Content>
+      <Filter>
+        <h2 className="incentive">{t("Your games")}</h2>
+      </Filter>
+      <StyledGameList>
+        <NewGameItem />
+        {gameList
+          .filter(({ owner }) => userId && (!owner || owner === userId))
+          .map((game) => (
+            <GameListItem
+              key={game.id}
+              game={game}
+              userId={userId}
+              onDelete={onGameDelete}
+            />
+          ))}
+      </StyledGameList>
+    </Content>
+  );
+};
+
+export default GameListView;

+ 103 - 0
src/views/Home.jsx

@@ -0,0 +1,103 @@
+import React from "react";
+import { Switch, Route, useLocation } from "react-router-dom";
+import { useTranslation } from "react-i18next";
+import styled from "styled-components";
+//import CookieConsent from "react-cookie-consent";
+
+import useLocalStorage from "../hooks/useLocalStorage";
+
+import HomeNav from "./HomeNav";
+import AboutModal from "./AboutModal";
+import GameListView from "./GameListView";
+import GameStudio from "./GameStudio";
+
+const StyledHome = styled.div`
+  min-height: 100vh;
+  flex-direction: column;
+  & > footer {
+    width: 100%;
+    grid-column: 1 / 4;
+    padding: 0.5em 0;
+    text-align: center;
+    background-color: #00000099;
+  }
+`;
+
+const useQuery = () => {
+  return new URLSearchParams(useLocation().search);
+};
+
+const Home = () => {
+  const { t } = useTranslation();
+
+  const [isBeta, setIsBeta] = useLocalStorage("isBeta", false);
+
+  if (isBeta) {
+    console.log("Beta activated");
+  }
+
+  /*const [cookieConsent, setCookieConsent] = useLocalStorage(
+    "cookieConsent",
+    false
+  );*/
+  const cookieConsent = true;
+  const [showAboutModal, setShowAboutModal] = React.useState(false);
+
+  let query = useQuery();
+
+  const forceBeta = query.get("beta") === "true";
+
+  React.useEffect(() => {
+    if (forceBeta) {
+      setIsBeta(true);
+    }
+  }, [forceBeta, setIsBeta]);
+
+  return (
+    <>
+      <StyledHome>
+        <HomeNav cookieConsent={cookieConsent} />
+        <Switch>
+          <Route exact path="/games">
+            <GameListView />
+          </Route>
+          <Route path="/studio">
+            <GameStudio />
+          </Route>
+        </Switch>
+        <footer>
+          <button
+            className="button clear"
+            onClick={() => setShowAboutModal(true)}
+          >
+            {t("About")}
+          </button>
+        </footer>
+      </StyledHome>
+      <AboutModal show={showAboutModal} setShow={setShowAboutModal} />
+      {/*<CookieConsent
+        location="bottom"
+        buttonText={t("Got it!")}
+        enableDeclineButton
+        declineButtonText={t("Refuse")}
+        cookieName="cookieConsent"
+        onAccept={() => setCookieConsent(true)}
+        containerClasses="cookie"
+        expires={150}
+        buttonStyle={{
+          color: "var(--font-color)",
+          backgroundColor: "var(--color-secondary)",
+        }}
+        style={{
+          backgroundColor: "#000000CE",
+          boxShadow:
+            "rgba(50, 50, 93, 0.25) 0px 6px 12px -2px, rgba(0, 0, 0, 0.3) 0px 3px 7px -3px",
+        }}
+      >
+        {t("This site use a cookie only to know if your are authenticated.")}
+      </CookieConsent>*/}
+    </>
+  );
+};
+
+export default Home;

+ 85 - 0
src/views/HomeNav.jsx

@@ -0,0 +1,85 @@
+import React from "react";
+import { NavLink } from "react-router-dom";
+import { useTranslation } from "react-i18next";
+import styled from "styled-components";
+
+import Account from "../components/Account";
+import useAuth from "../hooks/useAuth";
+
+import Brand from "./Brand";
+
+const Nav = styled.nav`
+  background-color: var(--bg-color);
+  position: relative;
+  padding: 0 5%;
+  display: flex;
+  align-items: center;
+
+  & button,
+  & .button {
+    background: none;
+    text-transform: uppercase;
+    font-weight: 300;
+    font-size: 1.3em;
+    border-radius: 0;
+    color: var(--font-color2);
+    margin: 0 0.5em;
+    letter-spacing: -1px;
+    padding: 0;
+  }
+
+  & button:hover,
+  & .button:hover,
+  & .button.active {
+    color: var(--font-color);
+    border-bottom: 1px solid var(--color-primary);
+  }
+
+  @media screen and (max-width: 640px) {
+    & .button,
+    & button {
+      display: none;
+    }
+  }
+`;
+
+const HomeNav = () => {
+  const { t } = useTranslation();
+
+  const cookieConsent = true;
+
+  const { isAuthenticated } = useAuth();
+
+  return (
+    <>
+      <Nav>
+        <Brand />
+        {isAuthenticated && (
+          <>
+            <NavLink to={"/games/"} className="button">
+              {t("All games")}
+            </NavLink>
+            <NavLink to={"/studio/"} className="button">
+              {t("Game studio")}
+            </NavLink>
+          </>
+        )}
+        <Account className="login" disabled={!cookieConsent} />
+        <a
+          className="icon button"
+          href="https://github.com/jrmi/airboardgame"
+          target="_blank"
+          rel="noreferrer"
+        >
+          <img
+            src="https://icongr.am/feather/github.svg?size=16&color=ffffff"
+            alt={t("Github")}
+            title={t("Github")}
+          />
+        </a>
+      </Nav>
+    </>
+  );
+};
+
+export default HomeNav;

+ 36 - 5
src/views/NavBar.jsx

@@ -1,6 +1,8 @@
 import React from "react";
 
 import styled from "styled-components";
+import { useTranslation } from "react-i18next";
+import { useHistory } from "react-router-dom";
 
 import HelpModal from "../views/HelpModal";
 import InfoModal from "../views/InfoModal";
@@ -12,11 +14,10 @@ import { getBestTranslationFromConfig } from "../utils/api";
 import Touch from "../ui/Touch";
 
 import useBoardConfig from "../components/useBoardConfig";
-
-import { useTranslation } from "react-i18next";
-
-import Brand from "./Brand";
 import { useC2C } from "../hooks/useC2C";
+import Brand from "./Brand";
+
+import { confirmAlert } from "react-confirm-alert";
 
 const StyledNavBar = styled.div.attrs(() => ({ className: "nav" }))`
   position: fixed;
@@ -60,6 +61,7 @@ const StyledNavBar = styled.div.attrs(() => ({ className: "nav" }))`
       flex: 1;
     }
     padding-left: 40px;
+    justify-content: flex-start;
   }
 
   & .nav-right {
@@ -123,6 +125,7 @@ const StyledNavBar = styled.div.attrs(() => ({ className: "nav" }))`
 const NavBar = ({ editMode }) => {
   const { t, i18n } = useTranslation();
   const [, , isMaster] = useC2C();
+  const history = useHistory();
   const [showLoadGameModal, setShowLoadGameModal] = React.useState(false);
   const [showSaveGameModal, setShowSaveGameModal] = React.useState(false);
   const [showHelpModal, setShowHelpModal] = React.useState(false);
@@ -135,11 +138,39 @@ const NavBar = ({ editMode }) => {
     [boardConfig, i18n.languages]
   );
 
+  const handleGoBack = React.useCallback(() => {
+    confirmAlert({
+      title: t("Confirmation"),
+      message: t("Do you really want to quit game edition?"),
+      buttons: [
+        {
+          label: t("Yes"),
+          onClick: async () => {
+            history.push("/studio");
+          },
+        },
+        {
+          label: t("No"),
+          onClick: () => {},
+        },
+      ],
+    });
+  }, [history, t]);
+
   return (
     <>
       <StyledNavBar>
         <div className="nav-left">
-          <Brand />
+          {!editMode && <Brand />}
+          {editMode && (
+            <Touch
+              onClick={handleGoBack}
+              alt={t("Go back to studio")}
+              title={t("Go back to studio")}
+              icon={"chevron-left"}
+              style={{ display: "inline" }}
+            />
+          )}
         </div>
 
         <div className="nav-center">

+ 89 - 0
src/views/NewGameItem.jsx

@@ -0,0 +1,89 @@
+import React from "react";
+import styled from "styled-components";
+import { Link } from "react-router-dom";
+import { useTranslation } from "react-i18next";
+
+const Game = styled.li`
+  position: relative;
+  padding: 0em;
+  margin: 0px;
+
+  & .game-name {
+    max-width: 80%;
+    line-height: 1.2em;
+    overflow: hidden;
+    margin-bottom: 3px;
+    margin: 0.2em 0 0.5em 0;
+    font-size: 2.3vw;
+  }
+
+  & .img-wrapper {
+    display: block;
+    position: relative;
+    width: 100%;
+    padding-top: 64.5%;
+    & > span {
+      background-color: var(--color-blueGrey);
+      position: absolute;
+      top: 0;
+      left: 0;
+      bottom: 0;
+      right: 0;
+      overflow: hidden;
+      display: flex;
+      justify-content: center;
+      border-radius: 5px;
+      & img {
+        flex: 0;
+      }
+    }
+  }
+
+  @media screen and (max-width: 1024px) {
+    & {
+      flex-basis: 45%;
+    }
+    & .details {
+      font-size: 12px;
+    }
+    & .game-name {
+      font-size: 28px;
+    }
+  }
+
+  @media screen and (max-width: 640px) {
+    & {
+      flex-basis: 100%;
+    }
+    & .game-name {
+      font-size: 24px;
+    }
+  }
+`;
+
+const GameListItem = () => {
+  const { t } = useTranslation();
+
+  return (
+    <Game>
+      <Link to={"/game/"} className="img-wrapper">
+        <span>
+          <img
+            src={
+              "https://icongr.am/entypo/squared-plus.svg?size=128&color=f9fbfa"
+            }
+            alt={"alt"}
+            title={"title"}
+          />
+        </span>
+      </Link>
+      <div className="details">
+        <span></span>
+      </div>
+
+      <h2 className="game-name">{t("Create a new game")}</h2>
+    </Game>
+  );
+};
+
+export default GameListItem;

+ 21 - 0
src/views/StyledGameList.jsx

@@ -0,0 +1,21 @@
+import styled from "styled-components";
+
+const StyledGameList = styled.ul`
+  list-style: none;
+  margin: 0;
+  padding: 0 5%;
+  display: grid;
+  grid-template-columns: repeat(3, 1fr);
+  grid-gap: 20px;
+  grid-auto-rows: minmax(100px, auto);
+
+  @media screen and (max-width: 1024px) {
+    grid-template-columns: repeat(2, 1fr);
+  }
+
+  @media screen and (max-width: 640px) {
+    grid-template-columns: repeat(1, 1fr);
+  }
+`;
+
+export default StyledGameList;