瀏覽代碼

Add spinner while loading game data

Jeremie Pardou-Piquemal 3 年之前
父節點
當前提交
de69149223
共有 4 個文件被更改,包括 105 次插入58 次删除
  1. 53 0
      src/ui/Spinner.jsx
  2. 10 2
      src/views/GameListItem.jsx
  3. 15 11
      src/views/GameListView.jsx
  4. 27 45
      src/views/GameStudio.jsx

+ 53 - 0
src/ui/Spinner.jsx

@@ -0,0 +1,53 @@
+import React from "react";
+import styled, { keyframes } from "styled-components";
+
+const spin = keyframes`
+100% {
+  transform: rotate(360deg);
+}
+`;
+
+const StyledSpinner = styled.div`
+  animation: ${spin} 1s infinite linear;
+  border: solid ${({ size }) => size / 10}px transparent;
+  border-radius: 50%;
+  border-right-color: var(--color-primary);
+  border-top-color: var(--color-primary);
+  box-sizing: border-box;
+  height: ${({ size }) => size}px;
+  width: ${({ size }) => size}px;
+  margin: 0 auto;
+  z-index: 1;
+  &:before {
+    animation: ${spin} 2s infinite linear;
+    border: solid ${({ size }) => size / 10}px transparent;
+    border-radius: 50%;
+    border-right-color: var(--color-primary);
+    border-top-color: var(--color-primary);
+    box-sizing: border-box;
+    content: "";
+    height: ${({ size }) => size - (2 * size) / 10}px;
+    width: ${({ size }) => size - (2 * size) / 10}px;
+    position: absolute;
+    top: 0;
+    left: 0;
+  }
+  &:after {
+    animation: ${spin} 3s infinite linear;
+    border: solid ${({ size }) => size / 10}px transparent;
+    border-radius: 50%;
+    border-right-color: var(--color-primary);
+    border-top-color: var(--color-primary);
+    box-sizing: border-box;
+    content: "";
+    height: ${({ size }) => size - (4 * size) / 10}px;
+    width: ${({ size }) => size - (4 * size) / 10}px;
+    position: absolute;
+    top: ${({ size }) => size / 10}px;
+    left: ${({ size }) => size / 10}px;
+  }
+`;
+
+const Spinner = ({ size = 100 }) => <StyledSpinner size={size} />;
+
+export default Spinner;

+ 10 - 2
src/views/GameListItem.jsx

@@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next";
 import { deleteGame, getBestTranslationFromConfig } from "../utils/api";
 import { confirmAlert } from "react-confirm-alert";
 import { toast } from "react-toastify";
+import { useMutation, useQueryClient } from "react-query";
 import { media2Url } from "../components/mediaLibrary";
 
 const Game = styled.li`
@@ -153,6 +154,13 @@ const GameListItem = ({
 }) => {
   const { t, i18n } = useTranslation();
   const history = useHistory();
+  const queryClient = useQueryClient();
+  const deleteMutation = useMutation((gameId) => deleteGame(gameId), {
+    onSuccess: () => {
+      queryClient.invalidateQueries("ownGames");
+      queryClient.invalidateQueries("games");
+    },
+  });
 
   const translation = React.useMemo(
     () => getBestTranslationFromConfig(game, i18n.languages),
@@ -176,8 +184,8 @@ const GameListItem = ({
           label: t("Yes"),
           onClick: async () => {
             try {
-              await deleteGame(id);
-              onDelete(id);
+              deleteMutation.mutate(id);
+              if (onDelete) onDelete(id);
               toast.success(t("Game deleted"), { autoClose: 1500 });
             } catch (e) {
               if (e.message === "Forbidden") {

+ 15 - 11
src/views/GameListView.jsx

@@ -6,6 +6,7 @@ import { useQuery } from "react-query";
 
 import { getGames } from "../utils/api";
 import SliderRange from "../ui/SliderRange";
+import Spinner from "../ui/Spinner";
 
 import { StyledGameList } from "./StyledGameList";
 
@@ -220,18 +221,14 @@ const GameListView = () => {
       : [];
   }, [gameList, filterCriteria]);
 
-  if (isLoading) {
-    return null;
-  }
-
-  const onChangeNbOfPlayersSlider = function (values) {
+  const onChangeNbOfPlayersSlider = (values) => {
     setFilterCriteria({
       ...filterCriteria,
       nbOfPlayers: values,
     });
   };
 
-  const onChangeDurationSlider = function (values) {
+  const onChangeDurationSlider = (values) => {
     setFilterCriteria({
       ...filterCriteria,
       durations: values,
@@ -345,11 +342,18 @@ const GameListView = () => {
             {t("games-available", { nbOfGames: `${filteredGameList.length}` })}
           </StyledGameResultNumber>
         </Filter>
-        <StyledGameList>
-          {filteredGameList.map((game) => (
-            <GameListItem key={game.id} game={game} />
-          ))}
-        </StyledGameList>
+        {!isLoading && (
+          <StyledGameList>
+            {filteredGameList.map((game) => (
+              <GameListItem key={game.id} game={game} />
+            ))}
+          </StyledGameList>
+        )}
+        {isLoading && (
+          <div style={{ padding: "1em" }}>
+            <Spinner />
+          </div>
+        )}
       </Content>
     </>
   );

+ 27 - 45
src/views/GameStudio.jsx

@@ -2,6 +2,7 @@ import React from "react";
 import { Redirect } from "react-router-dom";
 import { useTranslation } from "react-i18next";
 import styled from "styled-components";
+import { useQuery } from "react-query";
 
 import { getGames } from "../utils/api";
 import useAuth from "../hooks/useAuth";
@@ -9,6 +10,7 @@ import useAuth from "../hooks/useAuth";
 import { StyledGameList } from "./StyledGameList";
 import NewGameItem from "./NewGameItem";
 import GameListItem from "./GameListItem";
+import Spinner from "../ui/Spinner";
 
 const Filter = styled.div`
   & .incentive {
@@ -32,44 +34,25 @@ const Content = styled.div`
 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);
-    });
-  }, []);
+  const { isLoading, data: gameList } = useQuery("ownGames", async () =>
+    (await getGames())
+      .filter(({ owner }) => userId && (!owner || owner === userId))
+      .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;
+      })
+  );
 
   if (!isAuthenticated) {
     return <Redirect to="/games/" />;
@@ -82,16 +65,15 @@ const GameListView = () => {
       </Filter>
       <StyledGameList>
         <NewGameItem />
-        {gameList
-          .filter(({ owner }) => userId && (!owner || owner === userId))
-          .map((game) => (
-            <GameListItem
-              key={game.id}
-              game={game}
-              userId={userId}
-              onDelete={onGameDelete}
-            />
+        {!isLoading &&
+          gameList.map((game) => (
+            <GameListItem key={game.id} game={game} userId={userId} />
           ))}
+        {isLoading && (
+          <div style={{ paddingTop: "4em" }}>
+            <Spinner />
+          </div>
+        )}
       </StyledGameList>
     </Content>
   );