Procházet zdrojové kódy

Add demo games for learning purposes

Jeremie Pardou-Piquemal před 1 rokem
rodič
revize
b1eafc78f3

+ 7 - 7
package-lock.json

@@ -39,7 +39,7 @@
         "react-query": "^3.13.4",
         "react-query": "^3.13.4",
         "react-router": "^5.2.0",
         "react-router": "^5.2.0",
         "react-router-dom": "^5.2.0",
         "react-router-dom": "^5.2.0",
-        "react-sync-board": "^0.6.2",
+        "react-sync-board": "^0.7.0",
         "react-toastify": "^6.1.0",
         "react-toastify": "^6.1.0",
         "react-useportal": "^1.0.14",
         "react-useportal": "^1.0.14",
         "recoil": "^0.7.0",
         "recoil": "^0.7.0",
@@ -8878,9 +8878,9 @@
       "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
       "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
     },
     },
     "node_modules/react-sync-board": {
     "node_modules/react-sync-board": {
-      "version": "0.6.2",
-      "resolved": "https://registry.npmjs.org/react-sync-board/-/react-sync-board-0.6.2.tgz",
-      "integrity": "sha512-Yf1m2xD4Ih7ysfBqRi9kg/K0OzFN1LomZBue/FnMB2sl07xmJestjhLBqHco6bdpj4debdU7KiImKfq7A4WLeg==",
+      "version": "0.7.0",
+      "resolved": "https://registry.npmjs.org/react-sync-board/-/react-sync-board-0.7.0.tgz",
+      "integrity": "sha512-blsmSFLKPP9OJW5UMBiBGcN6dSgHT+RTCxfcXQYOUCPh4qil7/RypdBJxwPvv0RCoQDogiJdPMi9lULJ3tD01w==",
       "dependencies": {
       "dependencies": {
         "@emotion/react": "^11.9.0",
         "@emotion/react": "^11.9.0",
         "@emotion/styled": "^11.8.1",
         "@emotion/styled": "^11.8.1",
@@ -17258,9 +17258,9 @@
       }
       }
     },
     },
     "react-sync-board": {
     "react-sync-board": {
-      "version": "0.6.2",
-      "resolved": "https://registry.npmjs.org/react-sync-board/-/react-sync-board-0.6.2.tgz",
-      "integrity": "sha512-Yf1m2xD4Ih7ysfBqRi9kg/K0OzFN1LomZBue/FnMB2sl07xmJestjhLBqHco6bdpj4debdU7KiImKfq7A4WLeg==",
+      "version": "0.7.0",
+      "resolved": "https://registry.npmjs.org/react-sync-board/-/react-sync-board-0.7.0.tgz",
+      "integrity": "sha512-blsmSFLKPP9OJW5UMBiBGcN6dSgHT+RTCxfcXQYOUCPh4qil7/RypdBJxwPvv0RCoQDogiJdPMi9lULJ3tD01w==",
       "requires": {
       "requires": {
         "@emotion/react": "^11.9.0",
         "@emotion/react": "^11.9.0",
         "@emotion/styled": "^11.8.1",
         "@emotion/styled": "^11.8.1",

+ 1 - 1
package.json

@@ -34,7 +34,7 @@
     "react-query": "^3.13.4",
     "react-query": "^3.13.4",
     "react-router": "^5.2.0",
     "react-router": "^5.2.0",
     "react-router-dom": "^5.2.0",
     "react-router-dom": "^5.2.0",
-    "react-sync-board": "^0.6.2",
+    "react-sync-board": "^0.7.0",
     "react-toastify": "^6.1.0",
     "react-toastify": "^6.1.0",
     "react-useportal": "^1.0.14",
     "react-useportal": "^1.0.14",
     "recoil": "^0.7.0",
     "recoil": "^0.7.0",

binární
public/game_assets/default.png


binární
public/game_assets/testgame.png


+ 16 - 6
src/gameComponents/Zone.jsx

@@ -21,20 +21,29 @@ const ZoneWrapper = styled.div`
     background-color: ${backgroundColor};
     background-color: ${backgroundColor};
     border-radius: 5px;
     border-radius: 5px;
     position: relative;
     position: relative;
-    & > div {
+    & .zone__label {
       font-size: 1.5em;
       font-size: 1.5em;
-      letter-spacing: -3px;
       user-select: none;
       user-select: none;
       background-color: ${opacify(borderColor, 1)};
       background-color: ${opacify(borderColor, 1)};
       position: absolute;
       position: absolute;
+      border-radius: 0.5em;
+      color: var(--color-darkGrey);
+    }
+
+    & .zone__label.left {
       padding: 1em 0em;
       padding: 1em 0em;
       top: 1em;
       top: 1em;
       left: -1em;
       left: -1em;
-      border-radius: 0.5em;
-      color: var(--color-darkGrey);
+      letter-spacing: -3px;
       writing-mode: vertical-rl;
       writing-mode: vertical-rl;
       text-orientation: upright;
       text-orientation: upright;
     }
     }
+
+    & .zone__label.top {
+      padding: 0em 1em;
+      top: -1em;
+      left: 1em;
+    }
   `}
   `}
 `;
 `;
 
 
@@ -46,6 +55,7 @@ const Zone = ({
   borderColor,
   borderColor,
   borderStyle,
   borderStyle,
   backgroundColor,
   backgroundColor,
+  labelPosition = "left",
 }) => {
 }) => {
   const { register } = useItemInteraction("place");
   const { register } = useItemInteraction("place");
   const zoneRef = React.useRef(null);
   const zoneRef = React.useRef(null);
@@ -56,6 +66,7 @@ const Zone = ({
       const insideItems = itemIds.filter((itemId) =>
       const insideItems = itemIds.filter((itemId) =>
         isItemInsideElement(getItemElement(itemId), zoneRef.current)
         isItemInsideElement(getItemElement(itemId), zoneRef.current)
       );
       );
+
       if (!insideItems.length) return;
       if (!insideItems.length) return;
 
 
       const onItemActions = onItem.map((action) => {
       const onItemActions = onItem.map((action) => {
@@ -64,7 +75,6 @@ const Zone = ({
         }
         }
         return action;
         return action;
       });
       });
-
       onItemActions.forEach(({ name, args }) => {
       onItemActions.forEach(({ name, args }) => {
         switch (name) {
         switch (name) {
           case "reveal":
           case "reveal":
@@ -107,7 +117,7 @@ const Zone = ({
       borderColor={borderColor}
       borderColor={borderColor}
       backgroundColor={backgroundColor}
       backgroundColor={backgroundColor}
     >
     >
-      <div>{label}</div>
+      <div className={`zone__label ${labelPosition}`}>{label}</div>
     </ZoneWrapper>
     </ZoneWrapper>
   );
   );
 };
 };

+ 13 - 0
src/gameComponents/forms/ZoneFormFields.jsx

@@ -100,6 +100,19 @@ const Form = ({ initialValues }) => {
         </Field>
         </Field>
       </Label>
       </Label>
 
 
+      <Label>
+        {t("Label position")}
+        <Field
+          name="labelPosition"
+          component="select"
+          initialValue={initialValues.labelPosition || "left"}
+          style={{ width: "10em" }}
+        >
+          <option value="left">{t("Left")}</option>
+          <option value="top">{t("Top")}</option>
+        </Field>
+      </Label>
+
       <h3>{t("Interactions")}</h3>
       <h3>{t("Interactions")}</h3>
       <Hint>{t("Interaction help")}</Hint>
       <Hint>{t("Interaction help")}</Hint>
       <Label>
       <Label>

+ 255 - 0
src/games/demo_en.json

@@ -0,0 +1,255 @@
+{
+  "items": [
+    {
+      "id": "taBETVuRcF",
+      "type": "note",
+      "x": 124,
+      "y": -259,
+      "value": "- Two fingers to move the board\n- Pinch to zoom",
+      "label": "With a touchpad"
+    },
+    {
+      "id": "g5fAVbjPH4",
+      "layer": -1,
+      "type": "zone",
+      "x": -267.5,
+      "y": -288.5,
+      "label": "Navigation",
+      "width": "1070.8",
+      "height": "328.8"
+    },
+    {
+      "id": "x3wQ2rR5vS",
+      "type": "note",
+      "x": -190,
+      "y": -259,
+      "value": "- Middle or right click on the board to move the board\n- Wheel to zoom",
+      "label": "With a mouse"
+    },
+    {
+      "id": "NrcuGDrmW8",
+      "type": "note",
+      "x": -190,
+      "y": 852.5,
+      "value": "- When an item is selected, click on the action icon inside the action menu just above the item\n- You can double click on an item to trigger his main action\n- You also can use the keyboard shortcuts shown when you hover an action in the menu",
+      "label": "A single item",
+      "width": "300.0",
+      "height": "295.1",
+      "color": "#ffc",
+      "fontSize": 20,
+      "fontFamily": "Roboto",
+      "textColor": "#000",
+      "actions": [
+        { "name": "shuffle" },
+        { "name": "clone" },
+        { "name": "lock" },
+        { "name": "remove" }
+      ]
+    },
+    {
+      "id": "rdmKGYgYqX",
+      "type": "note",
+      "x": -189.5,
+      "y": 1474.5,
+      "value": "- Click to select a card just above\n- Double click to flip it\n- Try another action of the menu\n- Try to do action with keyboard shortcuts\n- Select all cards and try the new actions\n\n",
+      "label": "Let's try it",
+      "width": "331.0",
+      "height": "255.2",
+      "color": "rgba(184, 233, 134, 1)"
+    },
+    {
+      "id": "Nv8WebDYgD",
+      "type": "image",
+      "x": 197,
+      "y": 1645.5,
+      "content": { "type": "external", "content": "/game_assets/BH.jpg" },
+      "backContent": {
+        "type": "external",
+        "content": "/game_assets/Red_back.jpg"
+      },
+      "actions": [
+        { "name": "flip" },
+        { "name": "tap" },
+        { "name": "stack" },
+        { "name": "shuffle" }
+      ],
+      "flipped": false,
+      "unflippedFor": null
+    },
+    {
+      "id": "nU3xCrYnPr",
+      "type": "note",
+      "x": 439,
+      "y": -259,
+      "value": "- Two fingers to move the board\n- Pinch to zoom",
+      "label": "With a toushscreen"
+    },
+    {
+      "id": "nsYMqz673q",
+      "type": "note",
+      "x": -189.5,
+      "y": 1234.5,
+      "value": "- The last icon is not an action but toggle the edit panel\n- You can edit every aspect of this item this way\n- You shouldn't need it while playing but can be useful sometimes",
+      "label": "Item edition",
+      "width": "611.3",
+      "height": "103.2"
+    },
+    {
+      "id": "GzxEj35Jy6",
+      "layer": -1,
+      "type": "zone",
+      "x": -261,
+      "y": 813,
+      "label": "Item interaction",
+      "width": "730.4",
+      "height": "1037.8"
+    },
+    {
+      "id": "rHA3e6TbM2",
+      "layer": -1,
+      "type": "zone",
+      "x": -259,
+      "y": 123,
+      "label": "Item manipulation",
+      "width": "726.1",
+      "height": "653.6"
+    },
+    {
+      "id": "5KujHvza7f",
+      "type": "note",
+      "x": 135,
+      "y": 152,
+      "value": "- You can start to select multiple items by clicking outside of any item and moving the mouse.\n- Then you can move all items",
+      "label": "Multiple items"
+    },
+    {
+      "id": "WE8k4Tv7BS",
+      "type": "note",
+      "x": -199,
+      "y": 151.5,
+      "value": "- You can drag and drop item by clicking on any unlocked item.\n- You can select one item by clicking on it\n- you can clear the selection by clicking outside an item",
+      "label": "A single item",
+      "width": "300.0",
+      "height": "201.3"
+    },
+    {
+      "id": "6nMUFs4FZK",
+      "type": "note",
+      "x": -198,
+      "y": 451.5,
+      "value": "- Try to move the meeples on the right\n- Try to select them all\n- Try to move them all at the same time",
+      "label": "Let's try it",
+      "width": "299.7",
+      "height": "202.5",
+      "color": "rgba(184, 233, 134, 1)"
+    },
+    {
+      "id": "qdyg4sgfvn",
+      "type": "meeple",
+      "name": "Meeple rouge",
+      "color": "#D82735",
+      "x": 268,
+      "y": 488,
+      "actions": []
+    },
+    {
+      "id": "cFdSexMN79",
+      "type": "meeple",
+      "name": "Meeple orange",
+      "color": "#FF7435",
+      "x": 269,
+      "y": 558,
+      "actions": []
+    },
+    {
+      "id": "yGuZG5hGgE",
+      "type": "meeple",
+      "name": "Meeple jaune",
+      "color": "#F9DF00",
+      "x": 269,
+      "y": 633,
+      "actions": []
+    },
+    {
+      "id": "XSkQfZdat2",
+      "type": "note",
+      "x": 126.5,
+      "y": 851,
+      "value": "- Use the action menu for the selected items, only actions available for all items are displayed\n- You may have noticed that some actions are only visible when you've selected multiple items like, the shuffle action\n- You can also use keyboard shortcuts here\n",
+      "label": "Multiple items",
+      "width": "296.7",
+      "height": "300.1"
+    },
+    {
+      "id": "N3xNz4ksyV",
+      "type": "image",
+      "x": 260,
+      "y": 1480.5,
+      "content": { "type": "external", "content": "/game_assets/AS.jpg" },
+      "backContent": {
+        "type": "external",
+        "content": "/game_assets/Red_back.jpg"
+      },
+      "actions": [
+        { "name": "flip" },
+        { "name": "tap" },
+        { "name": "stack" },
+        { "name": "shuffle" }
+      ],
+      "flipped": false,
+      "unflippedFor": [],
+      "rotation": 0
+    },
+    {
+      "id": "aPhbdhXuFt",
+      "type": "image",
+      "x": 324,
+      "y": 1645.5,
+      "content": { "type": "external", "content": "/game_assets/JC.jpg" },
+      "backContent": {
+        "type": "external",
+        "content": "/game_assets/Red_back.jpg"
+      },
+      "actions": [
+        { "name": "flip" },
+        { "name": "tap" },
+        { "name": "stack" },
+        { "name": "shuffle" }
+      ],
+      "flipped": false,
+      "unflippedFor": null,
+      "rotation": 0
+    }
+  ],
+  "board": {
+    "size": 2000,
+    "scale": 1,
+    "defaultName": "How to play?",
+    "bgType": "default",
+    "playerCount": [1, 9],
+    "duration": [],
+    "gridSize": 1,
+    "imageUrl": "/game_assets/default.png",
+    "keepTitle": true,
+    "initialBoardPosition": {
+      "top": 24700,
+      "left": 24750,
+      "width": 1000,
+      "height": 300
+    },
+    "defaultLanguage": "en",
+    "materialLanguage": "Multi-lang",
+    "defaultBaseline": "Learn how to play with Airboardgame",
+    "defaultDescription": "# Demo game\n\nThis is a demo game to learn how to play with Airboardgame.\n\nFor other games, you can find useful information about the game like the creator name or the rules.",
+    "translations": [
+      {
+        "language": "fr",
+        "name": "Comment jouer ?",
+        "baseline": "Apprenez à jouer avec Airboardgame",
+        "description": "# Démonstration\n\nCe jeu vous permet d'apprendre à jouer avec Airboardgame.\n\nPour les autres jeux, vous trouverez dans cette section différentes choses utiles comme le nom de l'auteur ou les règles."
+      }
+    ],
+    "published": true
+  },
+  "availableItems": []
+}

+ 276 - 0
src/games/demo_fr.json

@@ -0,0 +1,276 @@
+{
+  "items": [
+    {
+      "id": "g5fAVbjPH4",
+      "layer": -1,
+      "type": "zone",
+      "x": -267.5,
+      "y": -290.5,
+      "label": "Navigation",
+      "width": "1070.8",
+      "height": "328.8",
+      "labelPosition": "top",
+      "locked": true
+    },
+    {
+      "id": "taBETVuRcF",
+      "type": "note",
+      "x": 124,
+      "y": -256,
+      "value": "- Utilisez deux doigts pour déplacer le plateau\n- Pincez pour zoomer",
+      "label": "Avec un pavé tactile",
+      "locked": true
+    },
+    {
+      "id": "x3wQ2rR5vS",
+      "type": "note",
+      "x": -190,
+      "y": -256,
+      "value": "- Cliquez avec le bouton du milieu ou de gauche pour déplacer le plateau\n- La molette permet de zoomer",
+      "label": "Avec une souris",
+      "locked": true
+    },
+    {
+      "id": "nU3xCrYnPr",
+      "type": "note",
+      "x": 439,
+      "y": -256,
+      "value": "- Utilisez deux doigts pour déplacer le plateau\n- Pincez pour zoomer",
+      "label": "Avec un écran tactile",
+      "locked": true
+    },
+    {
+      "id": "qdyg4sgfvn",
+      "type": "meeple",
+      "name": "Meeple rouge",
+      "color": "#D82735",
+      "x": 258,
+      "y": 478,
+      "actions": []
+    },
+    {
+      "id": "cFdSexMN79",
+      "type": "meeple",
+      "name": "Meeple orange",
+      "color": "#FF7435",
+      "x": 259,
+      "y": 548,
+      "actions": []
+    },
+    {
+      "id": "yGuZG5hGgE",
+      "type": "meeple",
+      "name": "Meeple jaune",
+      "color": "#F9DF00",
+      "x": 259,
+      "y": 623,
+      "actions": []
+    },
+    {
+      "id": "XSkQfZdat2",
+      "type": "note",
+      "x": 123.5,
+      "y": 862,
+      "value": "- Utilisez le menu d'action pour tous les éléments sélectionnés. Seules les actions disponibles sur tous les éléments sélectionnés sont affichées\n- Certaines actions sont visibles uniquement quand vous sélectionnez plusieurs éléments comme l'action « mélanger »\n- Vous pouvez également utiliser les raccourcis clavier\n",
+      "label": "Plusieurs éléments",
+      "width": "296.7",
+      "height": "348.3",
+      "locked": true
+    },
+    {
+      "id": "NrcuGDrmW8",
+      "type": "note",
+      "x": -190,
+      "y": 864.5,
+      "value": "- Quand un élément est sélectionné, cliquez sur le menu d'action juste au dessus de l'élément\n- Vous pouvez double-cliquez pour effectuer son action principale\n- Vous pouvez également utiliser les raccourcis clavier qui s'affichent quand vous survolez une action dans le menu",
+      "label": "Un seul élément",
+      "width": "300.0",
+      "height": "347.2",
+      "color": "#ffc",
+      "fontSize": 20,
+      "fontFamily": "Roboto",
+      "textColor": "#000",
+      "actions": [
+        { "name": "shuffle" },
+        { "name": "clone" },
+        { "name": "lock" },
+        { "name": "remove" }
+      ],
+      "locked": true
+    },
+    {
+      "id": "rHA3e6TbM2",
+      "layer": -1,
+      "type": "zone",
+      "x": -262,
+      "y": 98,
+      "label": "Déplacement des éléments",
+      "width": "726.1",
+      "height": "653.6",
+      "labelPosition": "top",
+      "locked": true
+    },
+    {
+      "id": "WE8k4Tv7BS",
+      "type": "note",
+      "x": -206,
+      "y": 146.5,
+      "value": "- Vous pouvez déplacer un élément en cliquant dessus et en maintenant le bouton.\n- Vous pouvez sélectionner un unique élément en cliquant dessus\n- Vous pouvez tout désélectionner en cliquant en dehors d'un élément",
+      "label": "Un seul élément",
+      "width": "300.0",
+      "height": "224.6",
+      "locked": true
+    },
+    {
+      "id": "6nMUFs4FZK",
+      "type": "note",
+      "x": -204,
+      "y": 452.5,
+      "value": "- Essayez de déplacer un Meeple sur la droite\n- Essayez de les sélectionner tous\n- Essayez de les déplacer tous en même temp",
+      "label": "Testez-le",
+      "width": "299.7",
+      "height": "202.5",
+      "color": "rgba(184, 233, 134, 1)",
+      "locked": true
+    },
+    {
+      "id": "5KujHvza7f",
+      "type": "note",
+      "x": 125,
+      "y": 142,
+      "value": "- Pour sélectionner plusieurs élément cliquez en dehors d'un élément et tout en maintenant le bouton déplacez la souris. Relachez quand vous êtes satisfait\n- Vous pouvez alors déplacer tous les élements en même temps",
+      "label": "Plusieurs éléments",
+      "width": "300.0",
+      "height": "224.0",
+      "locked": true
+    },
+    {
+      "id": "Nv8WebDYgD",
+      "type": "image",
+      "x": 194,
+      "y": 1710.5,
+      "content": { "type": "external", "content": "/game_assets/BH.jpg" },
+      "backContent": {
+        "type": "external",
+        "content": "/game_assets/Red_back.jpg"
+      },
+      "actions": [
+        { "name": "flip" },
+        { "name": "tap" },
+        { "name": "stack" },
+        { "name": "shuffle" }
+      ],
+      "flipped": false,
+      "unflippedFor": null
+    },
+    {
+      "id": "N3xNz4ksyV",
+      "type": "image",
+      "x": 257,
+      "y": 1545.5,
+      "content": { "type": "external", "content": "/game_assets/AS.jpg" },
+      "backContent": {
+        "type": "external",
+        "content": "/game_assets/Red_back.jpg"
+      },
+      "actions": [
+        { "name": "flip" },
+        { "name": "tap" },
+        { "name": "stack" },
+        { "name": "shuffle" }
+      ],
+      "flipped": false,
+      "unflippedFor": [],
+      "rotation": 0
+    },
+    {
+      "id": "aPhbdhXuFt",
+      "type": "image",
+      "x": 321,
+      "y": 1710.5,
+      "content": { "type": "external", "content": "/game_assets/JC.jpg" },
+      "backContent": {
+        "type": "external",
+        "content": "/game_assets/Red_back.jpg"
+      },
+      "actions": [
+        { "name": "flip" },
+        { "name": "tap" },
+        { "name": "stack" },
+        { "name": "shuffle" }
+      ],
+      "flipped": false,
+      "unflippedFor": null,
+      "rotation": 0
+    },
+    {
+      "id": "rdmKGYgYqX",
+      "type": "note",
+      "x": -192.5,
+      "y": 1536.5,
+      "value": "- Cliquez pour sélectionner une carte\n- Double-cliquez pour la retourner\n- Essayez une autre action du menu\n- Essayez les raccourcis clavier\n- Sélectionnez toutes les cartes et testez les nouvelles actions\n\n",
+      "label": "Testez-le",
+      "width": "331.0",
+      "height": "255.2",
+      "color": "rgba(184, 233, 134, 1)",
+      "locked": true
+    },
+    {
+      "id": "GzxEj35Jy6",
+      "layer": -1,
+      "type": "zone",
+      "x": -261,
+      "y": 813.5,
+      "label": "Interaction avec les éléments",
+      "width": "730.4",
+      "height": "1116.9",
+      "labelPosition": "top",
+      "locked": true
+    },
+    {
+      "id": "nsYMqz673q",
+      "type": "note",
+      "x": -193.5,
+      "y": 1302.5,
+      "value": "- La dernière icône du menu d'action permet d'afficher le panneau d'édition de l'élément\n- Vous pouvez ainsi modifier tous ses aspects\n- Certaines actions ne sont disponibles que quand ce panneau est affiché\n- Vous ne devriez avoir besoin de ce panneau que rarement",
+      "label": "Modification des éléments",
+      "width": "611.3",
+      "height": "154.8",
+      "locked": true
+    }
+  ],
+  "board": {
+    "size": 2000,
+    "scale": 1,
+    "defaultName": "How to play?",
+    "bgType": "default",
+    "playerCount": [1, 9],
+    "duration": [],
+    "gridSize": 1,
+    "imageUrl": "/game_assets/default.png",
+    "keepTitle": true,
+    "initialBoardPosition": {
+      "top": 24700,
+      "left": 24750,
+      "width": 1000,
+      "height": 300
+    },
+    "defaultLanguage": "en",
+    "materialLanguage": "Multi-lang",
+    "defaultBaseline": "Learn how to play with Airboardgame",
+    "defaultDescription": "# Demo game\n\nThis is a demo game to learn how to play with Airboardgame.\n\nFor other games, you can find useful information about the game like the creator name or the rules.",
+    "translations": [
+      {
+        "language": "fr",
+        "name": "Comment jouer ?",
+        "baseline": "Apprenez à jouer avec Airboardgame",
+        "description": "# Démonstration\n\nCe jeu vous permet d'apprendre à jouer avec Airboardgame.\n\nPour les autres jeux, vous trouverez dans cette section différentes choses utiles comme le nom de l'auteur ou les règles."
+      }
+    ],
+    "published": true
+  },
+  "availableItems": [],
+  "messages": [],
+  "timestamp": 1654541565654,
+  "gameId": "demo"
+}

+ 4 - 2
src/games/perfGame.js

@@ -26,10 +26,11 @@ const genGame = () => {
       scale: 0.5,
       scale: 0.5,
       name: "Perf Game",
       name: "Perf Game",
       published: true,
       published: true,
+      keepTitle: true,
       translations: [
       translations: [
         {
         {
           language: "fr",
           language: "fr",
-          name: "1 Jeu test de performances et des extrèmes",
+          name: "1 Jeu test de performances et des extrêmes",
           baseline:
           baseline:
             "Un jeu pour tester les performances mais également les limites des différentes saisies. Le texte est tellement long qu'il doit être caché au final.",
             "Un jeu pour tester les performances mais également les limites des différentes saisies. Le texte est tellement long qu'il doit être caché au final.",
         },
         },
@@ -42,9 +43,10 @@ const genGame = () => {
       materialLanguage: "Multi-lang",
       materialLanguage: "Multi-lang",
       minAge: "10",
       minAge: "10",
       duration: [30, 90],
       duration: [30, 90],
-      imageUrl: "/game_assets/testgame.png",
+      imageUrl: "/game_assets/default.png",
       gridSize: 1,
       gridSize: 1,
     },
     },
+    id: "perf",
   };
   };
 };
 };
 
 

+ 3 - 1
src/games/testGame.js

@@ -299,6 +299,7 @@ const genGame = () => {
       scale: 1,
       scale: 1,
       name: "Test Game",
       name: "Test Game",
       published: true,
       published: true,
+      keepTitle: true,
       translations: [
       translations: [
         {
         {
           language: "fr",
           language: "fr",
@@ -315,9 +316,10 @@ const genGame = () => {
       materialLanguage: "Multi-lang",
       materialLanguage: "Multi-lang",
       minAge: "10",
       minAge: "10",
       duration: [30, 90],
       duration: [30, 90],
-      imageUrl: "/game_assets/testgame.png",
+      imageUrl: "/game_assets/default.png",
       gridSize: 1,
       gridSize: 1,
     },
     },
+    id: "test",
   };
   };
 };
 };
 
 

+ 6 - 4
src/games/unpublishedGame.js

@@ -26,23 +26,25 @@ const genGame = () => {
       scale: 0.5,
       scale: 0.5,
       name: "Unpublished Game",
       name: "Unpublished Game",
       published: false,
       published: false,
+      keepTitle: true,
       translations: [
       translations: [
         {
         {
           language: "fr",
           language: "fr",
-          name: "2 Jeu non-publie",
-          description: "Un jeu non-publie pour tester",
+          name: "2 Jeu non publié",
+          description: "Un jeu non publié pour tester",
         },
         },
       ],
       ],
       playerCount: [1, 9],
       playerCount: [1, 9],
       defaultName: "2 Unpublished Game",
       defaultName: "2 Unpublished Game",
       defaultLanguage: "en",
       defaultLanguage: "en",
-      defaultDescription: "A non-published game",
+      defaultDescription: "A non published game",
       materialLanguage: "Multi-lang",
       materialLanguage: "Multi-lang",
       minAge: "10",
       minAge: "10",
       duration: [30, 90],
       duration: [30, 90],
-      imageUrl: "/game_assets/testgame.png",
+      imageUrl: "/game_assets/default.png",
       gridSize: 1,
       gridSize: 1,
     },
     },
+    id: "unpublished",
   };
   };
 };
 };
 
 

+ 20 - 4
src/hooks/useSession.jsx

@@ -5,19 +5,25 @@ import {
   useBoardConfig,
   useBoardConfig,
   useWire,
   useWire,
 } from "react-sync-board";
 } from "react-sync-board";
+import { useTranslation } from "react-i18next";
 
 
 import SubscribeSessionEvents from "./SubscribeSessionEvents";
 import SubscribeSessionEvents from "./SubscribeSessionEvents";
 
 
 import { updateSession, getSession, getGame } from "../utils/api";
 import { updateSession, getSession, getGame } from "../utils/api";
 
 
+import demoEn from "../games/demo_en.json?url";
+import demoFr from "../games/demo_fr.json?url";
+
 export const SessionContext = React.createContext({});
 export const SessionContext = React.createContext({});
 
 
+const demos = {
+  fr: demoFr,
+};
+
 const emtpyBoard = {
 const emtpyBoard = {
   items: [],
   items: [],
   availableItems: [],
   availableItems: [],
   board: {
   board: {
-    size: 1000,
-    scale: 1,
     translations: [
     translations: [
       {
       {
         language: "fr",
         language: "fr",
@@ -33,6 +39,7 @@ const emtpyBoard = {
 };
 };
 
 
 export const SessionProvider = ({ sessionId, fromGameId, children }) => {
 export const SessionProvider = ({ sessionId, fromGameId, children }) => {
+  const { i18n } = useTranslation();
   const { setItemList, getItemList } = useItemActions();
   const { setItemList, getItemList } = useItemActions();
   const { messages, setMessages } = useMessage();
   const { messages, setMessages } = useMessage();
   const [availableItems, setAvailableItems] = React.useState([]);
   const [availableItems, setAvailableItems] = React.useState([]);
@@ -61,14 +68,23 @@ export const SessionProvider = ({ sessionId, fromGameId, children }) => {
     } catch {
     } catch {
       if (fromGameId) {
       if (fromGameId) {
         // Then from initial game
         // Then from initial game
-        sessionData = await getGame(fromGameId);
+        if (fromGameId === "demo") {
+          const foundLang = i18n.languages.find((lang) => demos[lang]);
+          if (foundLang) {
+            sessionData = await (await fetch(demos[foundLang])).json();
+          } else {
+            sessionData = await (await fetch(demoEn)).json();
+          }
+        } else {
+          sessionData = await getGame(fromGameId);
+        }
       } else {
       } else {
         // Empty board
         // Empty board
         sessionData = emtpyBoard;
         sessionData = emtpyBoard;
       }
       }
     }
     }
     return sessionData;
     return sessionData;
-  }, [fromGameId, sessionId]);
+  }, [fromGameId, i18n.languages, sessionId]);
 
 
   const setSession = React.useCallback(
   const setSession = React.useCallback(
     async (newData, sync = false) => {
     async (newData, sync = false) => {

+ 6 - 1
src/i18n/en.json

@@ -295,5 +295,10 @@
   "Let this field empty to snap all items, or set a comma separated list of families that will be snapped.": "Let this field empty to snap all items, or set a comma separated list of families that will be snapped.",
   "Let this field empty to snap all items, or set a comma separated list of families that will be snapped.": "Let this field empty to snap all items, or set a comma separated list of families that will be snapped.",
   "Family": "Family",
   "Family": "Family",
   "Optional - Use the same family name for items that are part of the same set.": "Optional - Use the same family name for items that are part of the same set.",
   "Optional - Use the same family name for items that are part of the same set.": "Optional - Use the same family name for items that are part of the same set.",
-  "Anchor": "Anchor"
+  "Anchor": "Anchor",
+  "Label position": "Label position",
+  "Left": "Left",
+  "Top": "Top",
+  "Keep title": "Show title",
+  "Check it to keep the game title above the game image.": "Check it to keep the game title above the game image."
 }
 }

+ 8 - 3
src/i18n/fr.json

@@ -74,7 +74,7 @@
   "Game deleted": "Jeu supprimé",
   "Game deleted": "Jeu supprimé",
   "Game information": "Informations sur le jeu",
   "Game information": "Informations sur le jeu",
   "Game name": "Nom du jeu",
   "Game name": "Nom du jeu",
-  "Game saved": "Partie sauvegardée",
+  "Game saved": "Sauvegarde effectuée",
   "Game studio": "Créer",
   "Game studio": "Créer",
   "Game": "Jeu",
   "Game": "Jeu",
   "Generating export": "Génération de l'export",
   "Generating export": "Génération de l'export",
@@ -92,7 +92,7 @@
   "Hide": "Cacher",
   "Hide": "Cacher",
   "Horizontal hexagons": "Hexagones horizontaux",
   "Horizontal hexagons": "Hexagones horizontaux",
   "I want to play...": "Je veux jouer...",
   "I want to play...": "Je veux jouer...",
-  "If you have checked the publish checkbox your game will be public.": "Si vous avait coché l'option « Publier », votre jeu sera visibile publiquement.",
+  "If you have checked the publish checkbox your game will be public.": "Si vous avait coché l'option « Publier », votre jeu sera visible publiquement.",
   "Image": "Image",
   "Image": "Image",
   "In progress...": "Demande en cours…",
   "In progress...": "Demande en cours…",
   "Informations": "Informations",
   "Informations": "Informations",
@@ -295,5 +295,10 @@
   "Let this field empty to snap all items, or set a comma separated list of families that will be snapped.": "Laissez ce champ vide pour aimanter tous les éléments ou définissez une liste de familles d'élément à aimanter séparées par une virgule.",
   "Let this field empty to snap all items, or set a comma separated list of families that will be snapped.": "Laissez ce champ vide pour aimanter tous les éléments ou définissez une liste de familles d'élément à aimanter séparées par une virgule.",
   "Family": "Famille",
   "Family": "Famille",
   "Optional - Use the same family name for items that are part of the same set.": "Optionnel : définissez un nom de famille aux éléments appartenant à un même ensemble",
   "Optional - Use the same family name for items that are part of the same set.": "Optionnel : définissez un nom de famille aux éléments appartenant à un même ensemble",
-  "Anchor": "Ancre"
+  "Anchor": "Ancre",
+  "Label position": "Position du label",
+  "Left": "Gauche",
+  "Top": "Haut",
+  "Keep title": "Afficher le titre",
+  "Check it to keep the game title above the game image.": "Cochez pour conserver le titre sur l'image du jeu."
 }
 }

+ 26 - 1
src/ui/formUtils/ColorPicker.jsx

@@ -7,6 +7,25 @@ import styled from "styled-components";
 
 
 import backgroundGrid from "../../media/images/background-grid.png";
 import backgroundGrid from "../../media/images/background-grid.png";
 
 
+const defaultColors = [
+  "#FFF1E8",
+  "#FFEC27",
+  "#29ADFF",
+  "#FFCCAA",
+  "#00E436",
+  "#C2C3C7",
+  "#FFA300",
+  "#FF77A8",
+  "#83769C",
+  "#FF004D",
+  "#008751",
+  "#AB5236",
+  "#5F574F",
+  "#1D2B53",
+  "#7E2553",
+  "#000000 ",
+];
+
 const Color = styled.div`
 const Color = styled.div`
   position: relative;
   position: relative;
   background-image: url(${backgroundGrid});
   background-image: url(${backgroundGrid});
@@ -35,7 +54,12 @@ const ColorPickerWrapper = styled.div`
   flex-direction: column;
   flex-direction: column;
 `;
 `;
 
 
-const ColorPicker = ({ value, onChange, disableAlpha = true }) => {
+const ColorPicker = ({
+  value,
+  onChange,
+  disableAlpha = true,
+  colors = defaultColors,
+}) => {
   const [showPicker, setShowPicker] = React.useState(false);
   const [showPicker, setShowPicker] = React.useState(false);
   const [currentColor, setCurrentColor] = React.useState(() => {
   const [currentColor, setCurrentColor] = React.useState(() => {
     if (value === "") {
     if (value === "") {
@@ -83,6 +107,7 @@ const ColorPicker = ({ value, onChange, disableAlpha = true }) => {
             onChange={handleChange}
             onChange={handleChange}
             disableAlpha={disableAlpha}
             disableAlpha={disableAlpha}
             onChangeComplete={handleChangeComplete}
             onChangeComplete={handleChangeComplete}
+            presetColors={colors}
           />
           />
           <button onClick={handleClick}>{t("Close")}</button>
           <button onClick={handleClick}>{t("Close")}</button>
         </ColorPickerWrapper>
         </ColorPickerWrapper>

+ 33 - 30
src/utils/api.js

@@ -90,6 +90,35 @@ export const getBestTranslationFromConfig = (
   return translationsMap[defaultLanguage || "en"];
   return translationsMap[defaultLanguage || "en"];
 };
 };
 
 
+const demoGame = {
+  id: "demo",
+  owner: "nobody",
+  board: {
+    published: true,
+    defaultName: "How to play?",
+    bgType: "default",
+    playerCount: [],
+    duration: [],
+    gridSize: 1,
+    defaultLanguage: "en",
+    materialLanguage: "Multi-lang",
+    defaultBaseline: "Learn how to play with Airboardgame",
+    imageUrl: "/game_assets/default.png",
+    keepTitle: true,
+    defaultDescription:
+      "# Demo game\n\nThis is a demo game to learn how to play with Airboardgame.\n\nFor other games, you can find useful information about the game like the creator name or the rules.",
+    translations: [
+      {
+        language: "fr",
+        name: "Comment jouer ?",
+        baseline: "Apprenez à jouer avec Airboardgame",
+        description:
+          "# Démonstration\n\nCe jeu vous permet d'apprendre à jouer avec Airboardgame.\n\nPour les autres jeux, vous trouverez dans cette section différentes choses utiles comme le nom de l'auteur ou les règles.",
+      },
+    ],
+  },
+};
+
 export const getGames = async () => {
 export const getGames = async () => {
   const fetchParams = new URLSearchParams({
   const fetchParams = new URLSearchParams({
     fields: "_id,board,owner",
     fields: "_id,board,owner",
@@ -106,44 +135,18 @@ export const getGames = async () => {
     const serverGames = await result.json();
     const serverGames = await result.json();
 
 
     gameList = serverGames.map((game) => ({
     gameList = serverGames.map((game) => ({
-      name: game.board.defaultName || game.board.name,
       id: game._id,
       id: game._id,
       owner: game.owner,
       owner: game.owner,
-      ...game.board,
       board: game.board,
       board: game.board,
       url: `${gameURI}/${game._id}`,
       url: `${gameURI}/${game._id}`,
     }));
     }));
   }
   }
   if (!IS_PRODUCTION || import.meta.env.VITE_CI) {
   if (!IS_PRODUCTION || import.meta.env.VITE_CI) {
-    gameList = [
-      {
-        ...testGame,
-        name: "Test Game",
-        data: testGame,
-        id: "test",
-        published: true,
-        ...testGame.board,
-      },
-      {
-        ...perfGame,
-        name: "Perf Test",
-        data: perfGame,
-        id: "perf",
-        published: true,
-        ...perfGame.board,
-      },
-      {
-        ...unpublishedGame,
-        name: "Unpublished Game",
-        data: unpublishedGame,
-        id: "unpublished",
-        published: false,
-        ...unpublishedGame.board,
-      },
-      ...gameList,
-    ];
+    gameList = [testGame, perfGame, unpublishedGame, ...gameList];
   }
   }
 
 
+  gameList = [demoGame, ...gameList];
+
   return gameList;
   return gameList;
 };
 };
 
 
@@ -195,7 +198,7 @@ export const createGame = async (data) => {
 
 
 export const updateGame = async (gameId, data) => {
 export const updateGame = async (gameId, data) => {
   // fake games
   // fake games
-  if (["test", "perf", "unpublished"].includes(gameId)) {
+  if (["test", "perf", "unpublished", "demo"].includes(gameId)) {
     return data;
     return data;
   }
   }
   const result = await fetch(`${gameURI}/${gameId}`, {
   const result = await fetch(`${gameURI}/${gameId}`, {

+ 13 - 0
src/views/BoardView/BoardForm.jsx

@@ -188,6 +188,19 @@ const BoardConfigForm = () => {
         </Label>
         </Label>
 
 
         <Label>
         <Label>
+          {t("Keep title")}
+          <Field
+            name="keepTitle"
+            component="input"
+            type="checkbox"
+            initialValue={boardConfig.keepTitle}
+          />
+          <Hint>
+            {t("Check it to keep the game title above the game image.")}
+          </Hint>
+        </Label>
+
+        <Label>
           {t("Baseline")}
           {t("Baseline")}
           <Field
           <Field
             name="defaultBaseline"
             name="defaultBaseline"

+ 3 - 3
src/views/BoardView/ChangeGameModal.jsx

@@ -23,16 +23,16 @@ const ChangeGameModalContent = ({ onLoad }) => {
 
 
   const { isLoading, data: gameList } = useQuery("games", async () =>
   const { isLoading, data: gameList } = useQuery("games", async () =>
     (await getGames())
     (await getGames())
-      .filter((game) => game.published)
+      .filter((game) => game.board.published)
       .sort((a, b) => {
       .sort((a, b) => {
         const [nameA, nameB] = [
         const [nameA, nameB] = [
           a.board.defaultName || a.board.name,
           a.board.defaultName || a.board.name,
           b.board.defaultName || b.board.name,
           b.board.defaultName || b.board.name,
         ];
         ];
-        if (nameA < nameB) {
+        if (nameA < nameB || a.id === "demo") {
           return -1;
           return -1;
         }
         }
-        if (nameA > nameB) {
+        if (nameA > nameB || b.id === "demo") {
           return 1;
           return 1;
         }
         }
         return 0;
         return 0;

+ 25 - 20
src/views/GameListItem.jsx

@@ -12,14 +12,14 @@ const Game = styled.li`
   position: relative;
   position: relative;
   padding: 0em;
   padding: 0em;
   margin: 0px;
   margin: 0px;
-  min-width: 0; /* Fix for elipsis */
+  min-width: 0; /* Fix for ellipsis */
 
 
   & .game-name {
   & .game-name {
     max-width: 80%;
     max-width: 80%;
     line-height: 1.1em;
     line-height: 1.1em;
     overflow: hidden;
     overflow: hidden;
     margin-bottom: 3px;
     margin-bottom: 3px;
-    margin: 0.2em 0 0.5em 0;
+    margin: 0.1em 0 0.1em 0;
     font-size: 2.2vw;
     font-size: 2.2vw;
     white-space: nowrap;
     white-space: nowrap;
     overflow: hidden;
     overflow: hidden;
@@ -77,10 +77,7 @@ const Game = styled.li`
       ${({ other }) => (!other ? "" : "border: 1px solid red")};
       ${({ other }) => (!other ? "" : "border: 1px solid red")};
 
 
       position: absolute;
       position: absolute;
-      top: 0;
-      left: 0;
-      bottom: 0;
-      right: 0;
+      inset: 0;
       overflow: hidden;
       overflow: hidden;
       display: block;
       display: block;
       display: flex;
       display: flex;
@@ -91,19 +88,24 @@ const Game = styled.li`
         filter: blur(5px);
         filter: blur(5px);
         background-size: cover;
         background-size: cover;
         position: absolute;
         position: absolute;
-        top: 0;
-        left: 0;
-        bottom: 0;
-        right: 0;
+        inset: 0;
       }
       }
       & > h2 {
       & > h2 {
+        position: absolute;
+        width: 100%;
+        top: calc(50%-0.6em);
+        z-index: 200;
+        left: 0;
         text-align: center;
         text-align: center;
         display: inline;
         display: inline;
-        font-size: 3em;
+        font-size: 2em;
         white-space: nowrap;
         white-space: nowrap;
         overflow: hidden;
         overflow: hidden;
         text-overflow: ellipsis;
         text-overflow: ellipsis;
-        margin: 0 0.5em;
+        margin: 0;
+        padding: 0.2em 0.5em;
+        line-height: 1.2em;
+        background-color: #111111a0;
       }
       }
     }
     }
   }
   }
@@ -165,14 +167,17 @@ const getGameUrl = (id) => `${window.location.origin}/playgame/${id}`;
 
 
 const GameListItem = ({
 const GameListItem = ({
   game: {
   game: {
-    published,
     owner,
     owner,
     id,
     id,
-    minAge,
-    materialLanguage,
-    duration,
-    playerCount,
-    imageUrl,
+    board: {
+      minAge,
+      materialLanguage,
+      duration,
+      playerCount,
+      published,
+      imageUrl,
+      keepTitle,
+    },
   },
   },
   game,
   game,
   userId,
   userId,
@@ -196,7 +201,7 @@ const GameListItem = ({
   const [showImage, setShowImage] = React.useState(Boolean(realImageUrl));
   const [showImage, setShowImage] = React.useState(Boolean(realImageUrl));
 
 
   const translation = React.useMemo(
   const translation = React.useMemo(
-    () => getBestTranslationFromConfig(game, i18n.languages),
+    () => getBestTranslationFromConfig(game.board, i18n.languages),
     [game, i18n.languages]
     [game, i18n.languages]
   );
   );
 
 
@@ -312,7 +317,7 @@ const GameListItem = ({
               />
               />
             </>
             </>
           )}
           )}
-          {!showImage && <h2>{translation.name}</h2>}
+          {(!showImage || keepTitle) && <h2>{translation.name}</h2>}
         </span>
         </span>
       </a>
       </a>
       <span className="extra-actions">
       <span className="extra-actions">

+ 12 - 9
src/views/GameListView.jsx

@@ -174,9 +174,9 @@ const hasAllowedMaterialLanguage = (filterCriteria, game) => {
   const MULTI_LANG_KEYWORD = "Multi-lang";
   const MULTI_LANG_KEYWORD = "Multi-lang";
 
 
   return (
   return (
-    !game.materialLanguage ||
-    game.materialLanguage === MULTI_LANG_KEYWORD ||
-    filterCriteria.languages.includes(game.materialLanguage)
+    !game.board.materialLanguage ||
+    game.board.materialLanguage === MULTI_LANG_KEYWORD ||
+    filterCriteria.languages.includes(game.board.materialLanguage)
   );
   );
 };
 };
 
 
@@ -193,16 +193,16 @@ const GameListView = () => {
 
 
   const { isLoading, data: gameList } = useQuery("games", async () =>
   const { isLoading, data: gameList } = useQuery("games", async () =>
     (await getGames())
     (await getGames())
-      .filter((game) => game.published)
+      .filter((game) => game.board.published)
       .sort((a, b) => {
       .sort((a, b) => {
         const [nameA, nameB] = [
         const [nameA, nameB] = [
           a.board.defaultName || a.board.name,
           a.board.defaultName || a.board.name,
           b.board.defaultName || b.board.name,
           b.board.defaultName || b.board.name,
         ];
         ];
-        if (nameA < nameB) {
+        if (nameA < nameB || a.id === "demo") {
           return -1;
           return -1;
         }
         }
-        if (nameA > nameB) {
+        if (nameA > nameB || b.id === "demo") {
           return 1;
           return 1;
         }
         }
         return 0;
         return 0;
@@ -214,9 +214,12 @@ const GameListView = () => {
       return gameList.filter(
       return gameList.filter(
         (game) =>
         (game) =>
           (filterCriteria.searchTerm === NULL_SEARCH_TERM ||
           (filterCriteria.searchTerm === NULL_SEARCH_TERM ||
-            search(filterCriteria.searchTerm, game.defaultName)) &&
-          hasRequestedValues(filterCriteria.nbOfPlayers, game.playerCount) &&
-          hasRequestedValues(filterCriteria.durations, game.duration) &&
+            search(filterCriteria.searchTerm, game.board.defaultName)) &&
+          hasRequestedValues(
+            filterCriteria.nbOfPlayers,
+            game.board.playerCount
+          ) &&
+          hasRequestedValues(filterCriteria.durations, game.board.duration) &&
           hasAllowedMaterialLanguage(filterCriteria, game)
           hasAllowedMaterialLanguage(filterCriteria, game)
       );
       );
     }
     }

+ 1 - 1
src/views/GameView.jsx

@@ -50,7 +50,7 @@ const adaptItems = (nodes) => {
 const newGameData = {
 const newGameData = {
   items: [],
   items: [],
   availableItems: [],
   availableItems: [],
-  board: { size: 2000, scale: 1 },
+  board: { size: 2000, scale: 1, imageUrl: "/game_assets/default.png" },
 };
 };
 
 
 export const GameView = ({ create = false }) => {
 export const GameView = ({ create = false }) => {

+ 39 - 22
src/views/SessionRestoreDim.jsx

@@ -1,4 +1,3 @@
-import React from "react";
 import useAsyncEffect from "use-async-effect";
 import useAsyncEffect from "use-async-effect";
 import { useBoardPosition } from "react-sync-board";
 import { useBoardPosition } from "react-sync-board";
 
 
@@ -10,37 +9,55 @@ import useLocalStorage from "../hooks/useLocalStorage";
 const MAX_SESSION_DIM_RETENTION = 1000 * 60 * 60 * 24 * 150;
 const MAX_SESSION_DIM_RETENTION = 1000 * 60 * 60 * 24 * 150;
 
 
 export const SessionRestoreDim = () => {
 export const SessionRestoreDim = () => {
-  const { sessionLoaded, sessionId } = useSession();
+  const { sessionLoaded, sessionId, getSession } = useSession();
 
 
   const [sessionDimensions, setSessionDimensions] = useLocalStorage(
   const [sessionDimensions, setSessionDimensions] = useLocalStorage(
     "sessionDimensions",
     "sessionDimensions",
     {}
     {}
   );
   );
-  const { getDim, setDim } = useBoardPosition();
+  const { getDim, setDim, zoomToExtent } = useBoardPosition();
 
 
   /**
   /**
    * Load the previous dimension for this session if exists
    * Load the previous dimension for this session if exists
    */
    */
-  React.useEffect(() => {
-    if (sessionLoaded) {
-      if (sessionDimensions[sessionId]) {
-        setTimeout(() => {
-          const dim = { ...sessionDimensions[sessionId], timestamp: undefined };
-          setDim(() => dim);
-        }, 500);
-      }
-      const now = Date.now();
+  useAsyncEffect(
+    async (isMounted) => {
+      if (sessionLoaded) {
+        if (sessionDimensions[sessionId]) {
+          const dim = {
+            ...sessionDimensions[sessionId],
+            timestamp: undefined,
+          };
+          setTimeout(() => {
+            if (isMounted) {
+              setDim(() => dim);
+            }
+          }, 500);
+        } else {
+          const session = await getSession();
+          if (!isMounted) return;
+
+          if (session.board.initialBoardPosition) {
+            setTimeout(() => {
+              zoomToExtent(session.board.initialBoardPosition);
+            }, 500);
+          }
+        }
+
+        const now = Date.now();
 
 
-      const newDim = Object.fromEntries(
-        Object.entries(sessionDimensions).filter(([, { timestamp }]) => {
-          return timestamp && now - timestamp < MAX_SESSION_DIM_RETENTION;
-        })
-      );
-      setSessionDimensions(newDim);
-    }
-    // We want to set dimension only when session is loaded
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [sessionLoaded]);
+        const newDim = Object.fromEntries(
+          Object.entries(sessionDimensions).filter(([, { timestamp }]) => {
+            return timestamp && now - timestamp < MAX_SESSION_DIM_RETENTION;
+          })
+        );
+        setSessionDimensions(newDim);
+      }
+      // We want to set dimension only when session is loaded
+      // eslint-disable-next-line react-hooks/exhaustive-deps
+    },
+    [sessionLoaded]
+  );
 
 
   /**
   /**
    * Save board dimension in localstorage every 2 seconds for next visit
    * Save board dimension in localstorage every 2 seconds for next visit