diff --git a/web/package-lock.json b/web/package-lock.json
index 49146151..668350d5 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "indihu-web",
- "version": "2.2.0",
+ "version": "2.3.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
diff --git a/web/package.json b/web/package.json
index 24f3a0dc..30899382 100644
--- a/web/package.json
+++ b/web/package.json
@@ -1,6 +1,6 @@
{
"name": "indihu-web",
- "version": "2.2.0",
+ "version": "2.3.0",
"private": true,
"dependencies": {
"@emotion/react": "^11.11.1",
diff --git a/web/public/locales/cs/expo-editor.json b/web/public/locales/cs/expo-editor.json
index a4db1017..332c4fcf 100644
--- a/web/public/locales/cs/expo-editor.json
+++ b/web/public/locales/cs/expo-editor.json
@@ -234,7 +234,9 @@
"imageResultLabel": "Výsledek",
"imageResultTooltip": "Z knihovny dokumentů vyberte obrázek, kde je znázorněno správné místo, které měli návštěvníci najít.",
"showUsersTip": "Zobrazit tipy návštěvníků",
- "showUsersTipTooltip": "Pokud chcete, aby návštěvníci viděli správné řešení i svůj tip, zaškrtněte toto pole."
+ "showUsersTipTooltip": "Pokud chcete, aby návštěvníci viděli správné řešení i svůj tip, zaškrtněte toto pole.",
+ "numberOfPinsLabel": "Počet bodů k nalezení",
+ "numberOfPinsTooltip": "Zadejte počet bodů, které chcete, aby návštěvník našel. Každý bod bude moci mít následně přidělen svůj vlastní jedinečný text."
},
"gameDrawScreen": {
"taskTooltip": "Co nejpřesněji popište návštěvníkům jejich úkol (např. Dokreslete do grafu, jak se vyvíjela těžba uhlí po roce 1950).",
@@ -243,7 +245,11 @@
"imageResultLabel": "Výsledek",
"imageResultTooltip": "Z knihovny dokumentů vyberte kompletní obrázek.",
"showUsersDrawing": "Zobrazit kresbu návštěvníků",
- "showUsersDrawingTooltip": "Pokud chcete, aby návštěvníci viděli správné řešení i svoje tipy, zaškrtněte toto pole."
+ "showUsersDrawingTooltip": "Pokud chcete, aby návštěvníci viděli správné řešení i svoje tipy, zaškrtněte toto pole.",
+ "initialDrawingSettingsTitle": "Počáteční nastavení pro kreslení",
+ "initialDrawingColorLabel": "Počáteční barva",
+ "initialDrawingThicknessLabel": "Počáteční tloušťka",
+ "resetInitialDrawingSettingsBtnLabel": "Reset"
},
"gameWipeScreen": {
"taskTooltip": "Co nejpřesněji popište návštěvníkům jejich úkol (např. Podívejte se, jak vypadala budova před její rekonstrukcí). ",
@@ -278,7 +284,8 @@
"imageResultTooltip": "Z knihovny dokumentů vyberte výsledný kompletní obrázek.",
"object": "Objekt",
"objectSelectLabel": "Vybrat",
- "objectTooltip": "Z knihovny dokumentů vyberte obrázek s částmi, ze kterých budou návštěvníci vybírat."
+ "objectTooltip": "Z knihovny dokumentů vyberte obrázek s částmi, ze kterých budou návštěvníci vybírat.",
+ "screenPreviewText": "Náhled obrazovky"
},
"gameQuizScreen": {
"nameLabel": "Název",
diff --git a/web/public/locales/cs/view-exhibition.json b/web/public/locales/cs/view-exhibition.json
index ca0a9fee..83ae3f05 100644
--- a/web/public/locales/cs/view-exhibition.json
+++ b/web/public/locales/cs/view-exhibition.json
@@ -27,5 +27,6 @@
"untitled-chapter": "Nepojmenovaná kapitola",
"untitled-screen": "Nepojmenovaná obrazovka",
"rate-expo": "Hodnotit výstavu",
- "landscapeModeRecommendation": " Doporučujeme prohlížení výstavy v režimu na šířku."
+ "landscapeModeRecommendationSnackbarText": " Doporučujeme prohlížení výstavy v režimu na šířku.",
+ "audioWarningSnackbarText": "Z důvodu omezení iOS může být expozice přerušena, protože přehrávání zvuku vyžaduje interakci uživatele. Pokud se tak stane, klepněte na tlačítko přehrávání."
}
diff --git a/web/public/locales/cs/view-screen.json b/web/public/locales/cs/view-screen.json
index 0d7b464a..09596b55 100644
--- a/web/public/locales/cs/view-screen.json
+++ b/web/public/locales/cs/view-screen.json
@@ -82,7 +82,6 @@
},
"game": {
- "solution": "Správné řešení",
"controls": {
"finished": "Hotovo",
"play-again": "Zahrát znovu"
@@ -90,25 +89,25 @@
},
"game-find": {
- "task": "Najdi na obrázku"
+ "solution": "Správne riešenie"
},
"game-draw": {
- "task": "Dokresli obrázek",
+ "solution": "Výborně! Splnil jsi zadání a toto je výsledek",
"switchToPencilAction": "Přepnout na tužku",
"switchToEraserAction": "Přepnout na gumu",
"thicknessChooserAction": "Výběr tloušťky",
"colorChooserAction": "Výběr barvy"
},
"game-erase": {
- "task": "Vymaž obrázek"
+ "solution": "Výborně! Splnil jsi zadání a toto je výsledek"
},
"game-sizing": {
- "task": "Uhodni správnou velikost"
+ "solution": "Správne riešenie"
},
"game-move": {
- "task": "Posuň na správné místo"
+ "solution": "Správne riešenie"
},
"game-quiz": {
- "task": "Vyber správné možnosti"
+ "solution": "Správne riešenie"
}
}
diff --git a/web/public/locales/en/expo-editor.json b/web/public/locales/en/expo-editor.json
index e5aa08bb..cc34138a 100644
--- a/web/public/locales/en/expo-editor.json
+++ b/web/public/locales/en/expo-editor.json
@@ -234,7 +234,9 @@
"imageResultLabel": "Result",
"imageResultTooltip": "Select an image from the document library that shows the correct location that the user should have found.",
"showUsersTip": "View user's guess",
- "showUsersTipTooltip": "Check this box if you want the user to see both the correct solution and their guess."
+ "showUsersTipTooltip": "Check this box if you want the user to see both the correct solution and their guess.",
+ "numberOfPinsLabel": "Number of points to find",
+ "numberOfPinsTooltip": "Enter the number of points you want the visitor to find. Each point will then be able to have its own unique text assigned."
},
"gameDrawScreen": {
"taskTooltip": "Describe the user's task as precisely as possible (e.g. complete the graph of how coal mining developed after 1950).",
@@ -243,7 +245,11 @@
"imageResultLabel": "Result",
"imageResultTooltip": "Select a complete image from the document library.",
"showUsersDrawing": "View user's drawing",
- "showUsersDrawingTooltip": "Check this box if you want the user to see both the correct solution and their guess."
+ "showUsersDrawingTooltip": "Check this box if you want the user to see both the correct solution and their guess.",
+ "initialDrawingSettingsTitle": "Initial settings for drawing",
+ "initialDrawingColorLabel": "Initial color",
+ "initialDrawingThicknessLabel": "Initial thickness",
+ "resetInitialDrawingSettingsBtnLabel": "Reset"
},
"gameWipeScreen": {
"taskTooltip": "Describe the user's task as precisely as possible (e.g. see how the building looked before its renovation).",
@@ -278,7 +284,8 @@
"imageResultTooltip": "Select the complete image from the document library.",
"object": "Object",
"objectSelectLabel": "Select",
- "objectTooltip": "From the document library, select an image with parts for the user to select from."
+ "objectTooltip": "From the document library, select an image with parts for the user to select from.",
+ "screenPreviewText": "Screen preview"
},
"gameQuizScreen": {
"nameLabel": "Title",
diff --git a/web/public/locales/en/view-exhibition.json b/web/public/locales/en/view-exhibition.json
index 9af6dd0a..3aa32f82 100644
--- a/web/public/locales/en/view-exhibition.json
+++ b/web/public/locales/en/view-exhibition.json
@@ -27,5 +27,6 @@
"untitled-chapter": "Untitled chapter",
"untitled-screen": "Untitled screen",
"rate-expo": "Rate the exhibition",
- "landscapeModeRecommendation": "We recommend viewing the exhibition in landscape mode."
+ "landscapeModeRecommendationSnackbarText": "We recommend viewing the exhibition in landscape mode.",
+ "audioWarningSnackbarText": "Due to iOS restrictions, the exposition may be interrupted because audio playback requires user interaction. If that happens, please tap the play button to continue."
}
diff --git a/web/public/locales/en/view-screen.json b/web/public/locales/en/view-screen.json
index 1ff500bb..1f78eba3 100644
--- a/web/public/locales/en/view-screen.json
+++ b/web/public/locales/en/view-screen.json
@@ -82,7 +82,6 @@
},
"game": {
- "solution": "The correct solution",
"controls": {
"finished": "Finished",
"play-again": "Play again"
@@ -90,25 +89,25 @@
},
"game-find": {
- "task": "Find in the image"
+ "solution": "The correct solution"
},
"game-draw": {
- "task": "Draw in the image",
+ "solution": "Great! You completed the assignment and this is the result",
"switchToPencilAction": "Switch to pencil",
"switchToEraserAction": "Switch to eraser",
"thicknessChooserAction": "Choice of thickness",
"colorChooserAction": "Choice of color"
},
"game-erase": {
- "task": "Erase the image"
+ "solution": "Great! You completed the assignment and this is the result"
},
"game-sizing": {
- "task": "Guess the right size"
+ "solution": "The correct solution"
},
"game-move": {
- "task": "Move to the correct position"
+ "solution": "The correct solution"
},
"game-quiz": {
- "task": "Select the correct option(s)"
+ "solution": "The correct solution"
}
}
diff --git a/web/public/locales/sk/expo-editor.json b/web/public/locales/sk/expo-editor.json
index 5d3f99af..d3120344 100644
--- a/web/public/locales/sk/expo-editor.json
+++ b/web/public/locales/sk/expo-editor.json
@@ -234,7 +234,9 @@
"imageResultLabel": "Výsledok",
"imageResultTooltip": "Z knižnice dokumentov vyberte obrázok, kde je znázornené správne miesto, ktoré mali návštevníci nájsť.",
"showUsersTip": "Zobraziť tipy návštevníkov",
- "showUsersTipTooltip": "Ak chcete, aby návštevníci videli správne riešenie aj svoj tip, zaškrtnite toto pole."
+ "showUsersTipTooltip": "Ak chcete, aby návštevníci videli správne riešenie aj svoj tip, zaškrtnite toto pole.",
+ "numberOfPinsLabel": "Počet bodov na nájdenie",
+ "numberOfPinsTooltip": "Zadajte počet bodov, ktoré chcete aby návštevník našiel. Každý bod bude môcť mať následne pridelený svoj vlastný jedinečný text."
},
"gameDrawScreen": {
"taskTooltip": "Čo najpresnejšie popíšte návštevníkom ich úlohu (napr. dokreslite do grafu, ako sa vyvíjala ťažba uhlia po roku 1950).",
@@ -243,7 +245,11 @@
"imageResultLabel": "Výsledok",
"imageResultTooltip": "Z knižnice dokumentov vyberte kompletný obrázok.",
"showUsersDrawing": "Zobraziť kresbu návštevníkov",
- "showUsersDrawingTooltip": "Ak chcete, aby návštevníci videli správne riešenie aj svoje tipy, zaškrtnite toto pole."
+ "showUsersDrawingTooltip": "Ak chcete, aby návštevníci videli správne riešenie aj svoje tipy, zaškrtnite toto pole.",
+ "initialDrawingSettingsTitle": "Počiatočné nastavenia pre kreslenie",
+ "initialDrawingColorLabel": "Počiatočná farba",
+ "initialDrawingThicknessLabel": "Počiatočná hrúbka",
+ "resetInitialDrawingSettingsBtnLabel": "Reset"
},
"gameWipeScreen": {
"taskTooltip": "Čo najpresnejšie popíšte návštevníkom ich úlohu (napr. Pozrite sa, ako vyzerala budova pred jej rekonštrukciou). ",
@@ -278,7 +284,8 @@
"imageResultTooltip": "Z knižnice dokumentov vyberte výsledný kompletný obrázok.",
"object": "Objekt",
"objectSelectLabel": "Vybrať",
- "objectTooltip": "Z knižnice dokumentov vyberte obrázok s časťami, z ktorých budú návštevníci vyberať."
+ "objectTooltip": "Z knižnice dokumentov vyberte obrázok s časťami, z ktorých budú návštevníci vyberať.",
+ "screenPreviewText": "Náhľad obrazovky"
},
"gameQuizScreen": {
"nameLabel": "Názov",
diff --git a/web/public/locales/sk/view-exhibition.json b/web/public/locales/sk/view-exhibition.json
index e3f8a06b..42dd131e 100644
--- a/web/public/locales/sk/view-exhibition.json
+++ b/web/public/locales/sk/view-exhibition.json
@@ -27,5 +27,6 @@
"untitled-chapter": "Nepomenovaná kapitola",
"untitled-screen": "Nepomenovaná obrazovka",
"rate-expo": "Hodnotiť výstavu",
- "landscapeModeRecommendation": " Odporúčame prezeranie výstavy v režime na šírku."
+ "landscapeModeRecommendationSnackbarText": " Odporúčame prezeranie výstavy v režime na šírku.",
+ "audioWarningSnackbarText": "Z dôvodu obmedzenia iOS môže byť expozícia prerušená, pretože prehrávanie zvuku vyžaduje interakciu užívateľa. Ak sa tak stane, kliknite na tlačidlo prehrávania."
}
diff --git a/web/public/locales/sk/view-screen.json b/web/public/locales/sk/view-screen.json
index 32d4532a..d8fa2df9 100644
--- a/web/public/locales/sk/view-screen.json
+++ b/web/public/locales/sk/view-screen.json
@@ -82,7 +82,6 @@
},
"game": {
- "solution": "Správne riešenie",
"controls": {
"finished": "Hotovo",
"play-again": "Zahrať znova"
@@ -90,25 +89,25 @@
},
"game-find": {
- "task": "Nájdi na obrázku"
+ "solution": "Správne riešenie"
},
"game-draw": {
- "task": "Dokresli obrázok",
+ "solution": "Výborne! Splnil si zadanie a toto je výsledok",
"switchToPencilAction": "Prepnúť na ceruzku",
"switchToEraserAction": "Prepnúť na gumu",
"thicknessChooserAction": "Výber hrúbky",
"colorChooserAction": "Výber farby"
},
"game-erase": {
- "task": "Vymaž obrázok"
+ "solution": "Výborne! Splnil si zadanie a toto je výsledok"
},
"game-sizing": {
- "task": "Uhádni správnu veľkosť"
+ "solution": "Správne riešenie"
},
"game-move": {
- "task": "Posuň na správne miesto"
+ "solution": "Správne riešenie"
},
"game-quiz": {
- "task": "Vyber správne možnosti"
+ "solution": "Správne riešenie"
}
}
diff --git a/web/src/components/app-header/LoginAppHeader.tsx b/web/src/components/app-header/LoginAppHeader.tsx
index c736e785..31582960 100644
--- a/web/src/components/app-header/LoginAppHeader.tsx
+++ b/web/src/components/app-header/LoginAppHeader.tsx
@@ -9,11 +9,6 @@ import { IconButton, Menu, MenuItem } from "@mui/material";
import MenuIcon from "@mui/icons-material/Menu";
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
-import { MdEdit } from "react-icons/md";
-import { MdEditOff } from "react-icons/md";
-
-import { BasicTooltip } from "components/tooltip/tooltip";
-
// Models
import { ActiveExpo, Screen } from "models";
import { AppDispatch } from "store/store";
@@ -24,6 +19,7 @@ import { signOut } from "actions/user-actions";
import { openInNewTab } from "utils";
import { mapScreenTypeValuesToKeys } from "enums/screen-type";
import { useActiveExpoAccess } from "context/active-expo-access-provider/active-expo-access-provider";
+import { Icon } from "components/icon/icon";
// - -
@@ -116,26 +112,39 @@ const LoginAppHeader = ({
{isReadWriteAccess ? (
-
)}
diff --git a/web/src/components/button/button.tsx b/web/src/components/button/button.tsx
index c0828178..156546cd 100644
--- a/web/src/components/button/button.tsx
+++ b/web/src/components/button/button.tsx
@@ -3,15 +3,8 @@ import { CSSProperties, forwardRef } from "react";
import { useExpoDesignData } from "hooks/view-hooks/expo-design-data-hook";
import cx from "classnames";
-import { BasicTooltip } from "components/tooltip/tooltip";
-import { PlacesType } from "react-tooltip";
-
-type TooltipOption = {
- id: string;
- content: string;
- variant?: "light" | "dark"; // if undefined, based on selected theme
- place?: PlacesType;
-};
+import { BasicTooltip } from "components/tooltip/BasicTooltip";
+import { BasicTooltipProps } from "components/tooltip/tooltip-props";
interface ButtonProps {
color?: "default" | "primary" | "secondary" | "white" | "expoTheme";
@@ -27,7 +20,7 @@ interface ButtonProps {
shadow?: boolean;
iconBefore?: React.ReactNode;
iconAfter?: React.ReactNode;
- tooltip?: TooltipOption;
+ tooltip?: BasicTooltipProps;
children?: React.ReactNode;
}
diff --git a/web/src/components/dialogs/audio-dialog/audio-dialog.tsx b/web/src/components/dialogs/audio-dialog/audio-dialog.tsx
index b0466891..9fd7025f 100644
--- a/web/src/components/dialogs/audio-dialog/audio-dialog.tsx
+++ b/web/src/components/dialogs/audio-dialog/audio-dialog.tsx
@@ -12,8 +12,6 @@ import { Icon } from "components/icon/icon";
import { Divider } from "components/divider/divider";
import { Slider } from "@mui/material";
-import { Tooltip } from "react-tooltip";
-
// Actions
import {
setExpoVolumes,
@@ -160,17 +158,18 @@ const AudioSlider = ({
})
);
}}
+ tooltip={{
+ id: `tooltip-${volumeKey}`,
+ content: tooltipContent ?? "",
+ place: "top",
+ style: { zIndex: 1 },
+ }}
>
-
-
-
+
{volumeObj.actualVolume}
-
);
};
diff --git a/web/src/components/dialogs/chapters-dialog/screen-item.tsx b/web/src/components/dialogs/chapters-dialog/screen-item.tsx
index 4d7d9423..4024acb3 100644
--- a/web/src/components/dialogs/chapters-dialog/screen-item.tsx
+++ b/web/src/components/dialogs/chapters-dialog/screen-item.tsx
@@ -85,7 +85,7 @@ export const ScreenItem = ({
)}
diff --git a/web/src/components/dialogs/files-dialog/file-item.tsx b/web/src/components/dialogs/files-dialog/file-item.tsx
index 917fe5aa..53290187 100644
--- a/web/src/components/dialogs/files-dialog/file-item.tsx
+++ b/web/src/components/dialogs/files-dialog/file-item.tsx
@@ -44,7 +44,7 @@ export const FileItem = ({
)}
>
@@ -137,7 +137,7 @@ const OverlayDialog = ({
}
onClick={shouldIncrement ? pause : play}
@@ -149,7 +149,7 @@ const OverlayDialog = ({
diff --git a/web/src/components/dialogs/share-expo-dialog/CopyClipboardBox.tsx b/web/src/components/dialogs/share-expo-dialog/CopyClipboardBox.tsx
index f8bfb542..8f36ef99 100644
--- a/web/src/components/dialogs/share-expo-dialog/CopyClipboardBox.tsx
+++ b/web/src/components/dialogs/share-expo-dialog/CopyClipboardBox.tsx
@@ -1,7 +1,6 @@
import { useMemo } from "react";
import Snackbar from "react-md/lib/Snackbars";
import CopyToClipboard from "react-copy-to-clipboard";
-import { Tooltip } from "react-tooltip";
import { Icon } from "components/icon/icon";
import { useBoolean } from "hooks/boolean-hook";
@@ -32,11 +31,7 @@ export const CopyClipboardBox = ({
{text}
-
+
{
@@ -44,21 +39,21 @@ export const CopyClipboardBox = ({
onCopy?.();
}}
>
-
+
- {tooltipText && (
-
- )}
-
{
setCurrZoom((prevZoom) => prevZoom + 0.2);
}}
@@ -436,7 +436,7 @@ const SettingsPanel = ({
{
setCurrZoom(1);
}}
@@ -450,7 +450,7 @@ const SettingsPanel = ({
{
setCurrZoom((prevZoom) =>
prevZoom <= 1 ? prevZoom : prevZoom - 0.2
@@ -469,7 +469,7 @@ const SettingsPanel = ({
{
changeImage();
}}
@@ -483,7 +483,7 @@ const SettingsPanel = ({
{
const imageEditorObj = {
expoId: expoId,
@@ -503,7 +503,7 @@ const SettingsPanel = ({
{
dispatch(
setDialog(DialogType.ConfirmDialog, {
diff --git a/web/src/components/editors/game-description.tsx b/web/src/components/editors/game-description.tsx
index a4998216..573a1bff 100644
--- a/web/src/components/editors/game-description.tsx
+++ b/web/src/components/editors/game-description.tsx
@@ -13,6 +13,7 @@ import { Screen } from "models";
import { getFileById } from "actions/file-actions-typed";
import { helpIconText } from "enums/text";
+import { GAME_SCREEN_DEFAULT_RESULT_TIME } from "constants/screen";
// - -
@@ -49,7 +50,9 @@ const GameDescription = ({
diff --git a/web/src/components/form/formik/ColorPicker.tsx b/web/src/components/form/formik/ColorPicker.tsx
index 0a0b1237..bb367fd0 100644
--- a/web/src/components/form/formik/ColorPicker.tsx
+++ b/web/src/components/form/formik/ColorPicker.tsx
@@ -79,17 +79,17 @@ const ColorPicker = ({ name, color, setColor, label }: ColorPickerProps) => {
setIsColorEditModeOn((prev) => !prev)}
/>
!isColorEditModeOn && openColorPicker()}
/>
diff --git a/web/src/components/icon/icon.tsx b/web/src/components/icon/icon.tsx
index 89637cd8..4ff9adc5 100644
--- a/web/src/components/icon/icon.tsx
+++ b/web/src/components/icon/icon.tsx
@@ -1,23 +1,18 @@
import { CSSProperties, ReactNode } from "react";
-
import { useExpoDesignData } from "hooks/view-hooks/expo-design-data-hook";
+// Components
import { Icon as MuiIcon } from "@mui/material";
import FontIcon from "react-md/lib/FontIcons/FontIcon";
+import { BasicTooltip } from "components/tooltip/BasicTooltip";
+import { BasicTooltipProps } from "components/tooltip/tooltip-props";
-import { BasicTooltip } from "components/tooltip/tooltip";
-import { PlacesType } from "react-tooltip";
-
+// Utils
import cx from "classnames";
-type TooltipOption = {
- id: string;
- content: string;
- variant?: "light" | "dark"; // if undefined, based on selected theme
- place?: PlacesType;
-};
+// - - - -
-interface IconProps {
+type IconProps = {
name: string | ReactNode; // name either for FontIcon from 'react-md' or Icon from 'material-ui'
useMaterialUiIcon?: boolean;
color?:
@@ -30,19 +25,25 @@ interface IconProps {
| "inheritFromParents";
onClick?: () => void;
noCenterPlace?: boolean; // by default, icon should be inside some container and centered
- className?: string;
- style?: CSSProperties;
- tooltip?: TooltipOption;
-}
+ containerClassName?: string;
+ containerStyle?: CSSProperties;
+ iconClassName?: string;
+ iconStyle?: CSSProperties;
+ tooltip?: BasicTooltipProps;
+};
+
+// - - - -
export const Icon = ({
name,
useMaterialUiIcon = false,
color = "inheritFromParents",
- className,
- style,
onClick,
noCenterPlace = false,
+ containerClassName,
+ containerStyle,
+ iconClassName,
+ iconStyle,
tooltip,
}: IconProps) => {
const { expoDesignData, fgThemingIf } = useExpoDesignData();
@@ -65,20 +66,29 @@ export const Icon = ({
"text-muted-400": color === "muted-400",
"hover:cursor-pointer": !!onClick,
},
- className
+ containerClassName
)}
style={{
+ ...containerStyle,
color: themeEnabledIconsColor,
}}
data-tooltip-id={tooltip?.id ?? undefined}
>
{/* Icon color either inherited from Button parent or div if some color prop is used */}
{useMaterialUiIcon ? (
-
+
{name}
) : (
-
+
{name}
)}
diff --git a/web/src/components/infopoint/ScreenAnchorInfopoint.tsx b/web/src/components/infopoint/ScreenAnchorInfopoint.tsx
deleted file mode 100644
index 90a72a1a..00000000
--- a/web/src/components/infopoint/ScreenAnchorInfopoint.tsx
+++ /dev/null
@@ -1,186 +0,0 @@
-import { DetailedHTMLProps, HTMLAttributes } from "react";
-import { animated, useSpring } from "react-spring";
-import cx from "classnames";
-import { Infopoint } from "models";
-
-// - - - - - - - -
-
-type BaseProps = {
- id: string; // used as "data-tooltip-id"
- top: number;
- left: number;
- infopoint: Infopoint;
- color?: "primary"; // backward compatibility
-};
-
-type ScreenAnchorInfopointProps = BaseProps &
- DetailedHTMLProps, HTMLDivElement>;
-
-const ScreenAnchorInfopoint = (props: ScreenAnchorInfopointProps) => {
- if (props.infopoint?.shape === "CIRCLE") {
- return ;
- }
- if (props.infopoint?.shape === "ICON") {
- return ;
- }
- return ;
-};
-
-// - - - - - - - -
-
-const CircleAnchorInfopoint = ({
- id, // used as "data-tooltip-id"
- top,
- left,
- infopoint,
- ...otherProps
-}: ScreenAnchorInfopointProps) => {
- // Animation
- const { opacity, scale } = useSpring({
- from: { opacity: 1, scale: 1 },
- to: { opacity: 0, scale: 1.75 },
- loop: true,
- config: {
- friction: 50,
- },
- });
-
- return (
- {
- e.stopPropagation();
- }}
- >
-
-
- );
-};
-
-// - - - - - - - -
-
-const IconAnchorInfopoint = ({
- id,
- top,
- left,
- infopoint,
- ...otherProps
-}: ScreenAnchorInfopointProps) => {
- if (!infopoint.iconFile?.fileId) {
- return null;
- }
-
- return (
- {
- e.stopPropagation();
- }}
- >
-
-
- );
-};
-
-// - - - - - - - -
-
-const SquareAnchorInfopoint = ({
- id, // used as "data-tooltip-id"
- top,
- left,
- infopoint,
- color = "primary",
- ...otherProps
-}: ScreenAnchorInfopointProps) => {
- // Animation
- const { opacity, scale } = useSpring({
- from: { opacity: 1, scale: 1 },
- to: { opacity: 0, scale: 1.75 },
- loop: true,
- config: {
- friction: 50,
- },
- });
-
- return (
- {
- e.stopPropagation();
- }}
- >
-
-
-
-
- );
-};
-
-// - - - - - - - -
-
-export default ScreenAnchorInfopoint;
diff --git a/web/src/components/infopoint/components/anchor-infopoint/CircleAnchorInfopoint.tsx b/web/src/components/infopoint/components/anchor-infopoint/CircleAnchorInfopoint.tsx
new file mode 100644
index 00000000..abc98d70
--- /dev/null
+++ b/web/src/components/infopoint/components/anchor-infopoint/CircleAnchorInfopoint.tsx
@@ -0,0 +1,64 @@
+import { useSpring, animated } from "react-spring";
+import { AnchorInfopointProps } from ".";
+import cx from "classnames";
+
+const CircleAnchorInfopoint = ({
+ id, // used as "data-tooltip-id"
+ top,
+ left,
+ infopoint,
+ ...otherProps
+}: AnchorInfopointProps) => {
+ // Animation
+ const { opacity, scale } = useSpring({
+ from: { opacity: 1, scale: 1 },
+ to: { opacity: 0, scale: 1.75 },
+ loop: true,
+ config: {
+ friction: 50,
+ },
+ });
+
+ return (
+ {
+ e.stopPropagation();
+ }}
+ >
+
+
+ );
+};
+
+export default CircleAnchorInfopoint;
diff --git a/web/src/components/infopoint/components/anchor-infopoint/IconAnchorInfopoint.tsx b/web/src/components/infopoint/components/anchor-infopoint/IconAnchorInfopoint.tsx
new file mode 100644
index 00000000..e944c928
--- /dev/null
+++ b/web/src/components/infopoint/components/anchor-infopoint/IconAnchorInfopoint.tsx
@@ -0,0 +1,41 @@
+import { AnchorInfopointProps } from ".";
+import cx from "classnames";
+
+const IconAnchorInfopoint = ({
+ id,
+ top,
+ left,
+ infopoint,
+ ...otherProps
+}: AnchorInfopointProps) => {
+ if (!infopoint.iconFile?.fileId) {
+ return null;
+ }
+
+ return (
+ {
+ e.stopPropagation();
+ }}
+ >
+
+
+ );
+};
+
+export default IconAnchorInfopoint;
diff --git a/web/src/components/infopoint/components/anchor-infopoint/SquareAnchorInfopoint.tsx b/web/src/components/infopoint/components/anchor-infopoint/SquareAnchorInfopoint.tsx
new file mode 100644
index 00000000..4231192f
--- /dev/null
+++ b/web/src/components/infopoint/components/anchor-infopoint/SquareAnchorInfopoint.tsx
@@ -0,0 +1,58 @@
+import { useSpring, animated } from "react-spring";
+import { AnchorInfopointProps } from ".";
+import cx from "classnames";
+
+const SquareAnchorInfopoint = ({
+ id, // used as "data-tooltip-id"
+ top,
+ left,
+ infopoint,
+ color = "primary",
+ ...otherProps
+}: AnchorInfopointProps) => {
+ // Animation
+ const { opacity, scale } = useSpring({
+ from: { opacity: 1, scale: 1 },
+ to: { opacity: 0, scale: 1.75 },
+ loop: true,
+ config: {
+ friction: 50,
+ },
+ });
+
+ return (
+ {
+ e.stopPropagation();
+ }}
+ >
+
+
+
+
+ );
+};
+
+export default SquareAnchorInfopoint;
diff --git a/web/src/components/infopoint/components/anchor-infopoint/index.tsx b/web/src/components/infopoint/components/anchor-infopoint/index.tsx
new file mode 100644
index 00000000..3ad56b13
--- /dev/null
+++ b/web/src/components/infopoint/components/anchor-infopoint/index.tsx
@@ -0,0 +1,36 @@
+import { DetailedHTMLProps, HTMLAttributes } from "react";
+
+// Components
+import SquareAnchorInfopoint from "./SquareAnchorInfopoint";
+import IconAnchorInfopoint from "./IconAnchorInfopoint";
+import CircleAnchorInfopoint from "./CircleAnchorInfopoint";
+
+// Models
+import { Infopoint } from "models";
+
+// - - - - - - - -
+
+type BaseProps = {
+ id: string; // used as "data-tooltip-id"
+ top: number;
+ left: number;
+ infopoint: Infopoint;
+ color?: "primary"; // backward compatibility
+};
+
+export type AnchorInfopointProps = BaseProps &
+ DetailedHTMLProps, HTMLDivElement>;
+
+const AnchorInfopoint = (props: AnchorInfopointProps) => {
+ if (props.infopoint.shape === "CIRCLE") {
+ return ;
+ }
+ if (props.infopoint.shape === "ICON") {
+ return ;
+ }
+
+ // Defaults to Square, because of backward compatibility
+ return ;
+};
+
+export default AnchorInfopoint;
diff --git a/web/src/components/infopoint/InfopointBody.tsx b/web/src/components/infopoint/components/tooltip-infopoint/InfopointBody.tsx
similarity index 100%
rename from web/src/components/infopoint/InfopointBody.tsx
rename to web/src/components/infopoint/components/tooltip-infopoint/InfopointBody.tsx
diff --git a/web/src/components/infopoint/TooltipInfopoint.tsx b/web/src/components/infopoint/components/tooltip-infopoint/TooltipInfopoint.tsx
similarity index 71%
rename from web/src/components/infopoint/TooltipInfopoint.tsx
rename to web/src/components/infopoint/components/tooltip-infopoint/TooltipInfopoint.tsx
index 3099c354..8766bc4f 100644
--- a/web/src/components/infopoint/TooltipInfopoint.tsx
+++ b/web/src/components/infopoint/components/tooltip-infopoint/TooltipInfopoint.tsx
@@ -1,20 +1,24 @@
import { useState, Dispatch, SetStateAction } from "react";
-import { Tooltip } from "react-tooltip";
-
import { useExpoDesignData } from "hooks/view-hooks/expo-design-data-hook";
+
+import { Tooltip } from "react-tooltip";
import InfopointBody from "./InfopointBody";
-import { InfopointStatusObject } from "./parseScreenMaps";
+// Models
+import { InfopointStatusObject } from "../../useTooltipInfopoint";
import { Infopoint } from "models";
+// Utils
import cx from "classnames";
import { getTooltipArrowBorderClassName } from "utils/view-utils";
+// - - - -
+
type TooltipInfoPointProps = {
id: string;
infopoint: Infopoint;
- infopointOpenStatusMap: Record;
- setInfopointOpenStatusMap: Dispatch<
+ infopointStatusMap: Record;
+ setInfopointStatusMap: Dispatch<
SetStateAction>
>;
primaryKey: string;
@@ -22,38 +26,15 @@ type TooltipInfoPointProps = {
canBeOpen?: boolean;
};
-const TooltipInfoPoint = (props: TooltipInfoPointProps) => {
- const keyMap =
- props.secondaryKey === undefined
- ? `${props.primaryKey}`
- : `${props.primaryKey}-${props.secondaryKey}`;
-
- return ;
-};
-
-export default TooltipInfoPoint;
-
-// - - - -
-
-interface Props {
- id: string;
- infopoint: Infopoint;
- infopointOpenStatusMap: Record;
- setInfopointOpenStatusMap: Dispatch<
- SetStateAction>
- >;
- keyMap: string;
- canBeOpen?: boolean;
-}
-
-const BasicTooltipInfopoint = ({
+const TooltipInfoPoint = ({
id,
infopoint,
- infopointOpenStatusMap,
- setInfopointOpenStatusMap,
- keyMap,
+ infopointStatusMap,
+ setInfopointStatusMap,
+ primaryKey,
+ secondaryKey,
canBeOpen = true,
-}: Props) => {
+}: TooltipInfoPointProps) => {
const { isLightMode } = useExpoDesignData();
const [isVideoLoaded, setIsVideoLoaded] = useState(false); // infopoints's video if video type
@@ -70,6 +51,11 @@ const BasicTooltipInfopoint = ({
? "right"
: undefined;
+ const keyMap =
+ secondaryKey === undefined
+ ? `${primaryKey}`
+ : `${primaryKey}-${secondaryKey}`;
+
return (
{
const closeThisInfopoint = () => {
- setInfopointOpenStatusMap((prevMap) => ({
+ setInfopointStatusMap((prevMap) => ({
...prevMap,
[keyMap]: { ...prevMap[keyMap], isOpen: false },
}));
@@ -102,10 +88,10 @@ const BasicTooltipInfopoint = ({
setIsVideoLoaded,
});
}}
- isOpen={infopointOpenStatusMap[keyMap].isOpen && canBeOpen}
+ isOpen={infopointStatusMap[keyMap].isOpen && canBeOpen}
setIsOpen={(isOpen) => {
if (isOpen) {
- setInfopointOpenStatusMap((prevMap) => ({
+ setInfopointStatusMap((prevMap) => ({
...prevMap,
[keyMap]: { ...prevMap[keyMap], isOpen: !prevMap[keyMap].isOpen },
}));
@@ -115,3 +101,5 @@ const BasicTooltipInfopoint = ({
/>
);
};
+
+export default TooltipInfoPoint;
diff --git a/web/src/components/infopoint/VideoContentBody.tsx b/web/src/components/infopoint/components/tooltip-infopoint/VideoContentBody.tsx
similarity index 98%
rename from web/src/components/infopoint/VideoContentBody.tsx
rename to web/src/components/infopoint/components/tooltip-infopoint/VideoContentBody.tsx
index 80faa2e0..de6a152a 100644
--- a/web/src/components/infopoint/VideoContentBody.tsx
+++ b/web/src/components/infopoint/components/tooltip-infopoint/VideoContentBody.tsx
@@ -143,7 +143,7 @@ const VideoContentBody = ({
useMaterialUiIcon
name={isVideoPlaying ? "pause" : "play_arrow"}
onClick={() => playPauseVideo()}
- style={{ fontSize: "22px" }}
+ iconStyle={{ fontSize: "22px" }}
/>
@@ -176,8 +176,8 @@ const VideoContentBody = ({
setVideoVolume(prevVideoVolume);
}
}}
- style={{ fontSize: "22px" }}
- className="ml-auto"
+ iconStyle={{ fontSize: "22px" }}
+ containerClassName="ml-auto"
/>
diff --git a/web/src/components/infopoint/hooks/useInfopointClosing.ts b/web/src/components/infopoint/hooks/useInfopointClosing.ts
new file mode 100644
index 00000000..a76ab572
--- /dev/null
+++ b/web/src/components/infopoint/hooks/useInfopointClosing.ts
@@ -0,0 +1,81 @@
+import { useCallback, Dispatch, SetStateAction } from "react";
+
+import {
+ InfopointStatusMap,
+ InfopointSupportedScreens,
+} from "../useTooltipInfopoint";
+
+import {
+ GameQuizScreen,
+ ImageScreen,
+ ImageChangeScreen,
+ SlideshowScreen,
+} from "models";
+
+import { screenType } from "enums/screen-type";
+
+// - - - -
+
+type UseInfopointClosingProps = {
+ infopointStatusMap: InfopointStatusMap;
+ setInfopointStatusMap: Dispatch>;
+};
+
+export const useInfopointClosing = ({
+ infopointStatusMap,
+ setInfopointStatusMap,
+}: UseInfopointClosingProps) => {
+ // 1. Iterates through whole map and closes all of the infopoints
+ const closeAllInfopoints = useCallback(() => {
+ if (!infopointStatusMap) {
+ return;
+ }
+
+ const entries = Object.entries(infopointStatusMap);
+ const closedEntries = entries.map(([key, infopoint]) => {
+ return [key, { ...infopoint, isOpen: false }];
+ });
+
+ const newMap = Object.fromEntries(closedEntries);
+ setInfopointStatusMap(newMap);
+ }, [infopointStatusMap, setInfopointStatusMap]);
+
+ // 2. Iterates through whole map, but closes only infopoints of current photo (primary-key)
+ const closePhotoInfopoints = useCallback(
+ (photoIndex: number) => {
+ if (!infopointStatusMap) {
+ return;
+ }
+ const entries = Object.entries(infopointStatusMap);
+ const closedEntries = entries.map(([key, infopoint]) => {
+ const parsedPhotoKey = parseInt(key.charAt(0));
+ if (!isNaN(parsedPhotoKey) && parsedPhotoKey === photoIndex) {
+ return [key, { ...infopoint, isOpen: false }];
+ }
+ return [key, { ...infopoint }];
+ });
+
+ const newMap = Object.fromEntries(closedEntries);
+ setInfopointStatusMap(newMap);
+ },
+ [infopointStatusMap, setInfopointStatusMap]
+ );
+
+ // 3. Wrapper for closing infopoints for particular screen
+ // Called e.g. when ESC was pressed or when clicking on the image outside any infopoint
+ // NOTE: Typescript method overloading
+ function closeInfopoints(screen: GameQuizScreen): () => void;
+ function closeInfopoints(screen: ImageScreen): () => void;
+ function closeInfopoints(screen: ImageChangeScreen): () => void;
+ function closeInfopoints(
+ screen: SlideshowScreen
+ ): (photoIndex: number) => void;
+ function closeInfopoints(screen: InfopointSupportedScreens) {
+ if (screen.type === screenType.SLIDESHOW) {
+ return closePhotoInfopoints;
+ }
+ return closeAllInfopoints;
+ }
+
+ return { closeInfopoints };
+};
diff --git a/web/src/components/infopoint/hooks/useMobileInfopointAutoClosing.ts b/web/src/components/infopoint/hooks/useMobileInfopointAutoClosing.ts
new file mode 100644
index 00000000..7c99c1f0
--- /dev/null
+++ b/web/src/components/infopoint/hooks/useMobileInfopointAutoClosing.ts
@@ -0,0 +1,64 @@
+import { useEffect, Dispatch, SetStateAction } from "react";
+import { useMediaDevice } from "context/media-device-provider/media-device-provider";
+
+import { InfopointStatusMap } from "../useTooltipInfopoint";
+
+type UseMobileInfopointAutoClosingProps = {
+ setInfopointStatusMap: Dispatch>;
+ isMapParsingDone: boolean;
+};
+
+/**
+ * On small (mobile) screens, all infopoints (even the 'isAlwaysVisible = true' ones) are by default closed first.
+ */
+export const useMobileInfopointAutoClosing = ({
+ setInfopointStatusMap,
+ isMapParsingDone,
+}: UseMobileInfopointAutoClosingProps) => {
+ const { isSm } = useMediaDevice();
+
+ useEffect(() => {
+ // Handle larger screens
+ if (!isSm) {
+ setInfopointStatusMap((prevMap) => {
+ if (!prevMap) {
+ return prevMap;
+ }
+
+ const entries = Object.entries(prevMap);
+ const nextMap = entries.reduce(
+ (acc, [key, infopointStatus]) => ({
+ ...acc,
+ [key]: {
+ ...infopointStatus,
+ isOpen: infopointStatus.isAlwaysVisible,
+ },
+ }),
+ {} as InfopointStatusMap
+ );
+
+ return nextMap;
+ });
+
+ return;
+ }
+
+ // Handle mobile screens
+ setInfopointStatusMap((prevMap) => {
+ if (!prevMap) {
+ return prevMap;
+ }
+
+ const entries = Object.entries(prevMap);
+ const nextMapWithClosedInfopoints = entries.reduce(
+ (acc, [key, infopointStatus]) => ({
+ ...acc,
+ [key]: { ...infopointStatus, isOpen: false },
+ }),
+ {} as InfopointStatusMap
+ );
+
+ return nextMapWithClosedInfopoints;
+ });
+ }, [isSm, isMapParsingDone, setInfopointStatusMap]);
+};
diff --git a/web/src/components/infopoint/parseScreenMaps.ts b/web/src/components/infopoint/parseScreenMaps.ts
deleted file mode 100644
index 2c91d5c0..00000000
--- a/web/src/components/infopoint/parseScreenMaps.ts
+++ /dev/null
@@ -1,107 +0,0 @@
-import {
- ImageScreen,
- SlideshowScreen,
- ImageChangeScreen,
- GameQuizScreen,
-} from "models";
-
-export type InfopointStatusObject = {
- isOpen: boolean;
- isAlwaysVisible: boolean;
-};
-
-export const parseSlideshowScreenMap = (viewScreen: SlideshowScreen) => {
- const infopointsMap = viewScreen.images?.reduce(
- (acc, currImage, currImageIndex) => {
- const reducedImage = currImage.infopoints.reduce(
- (innerAcc, currInfopoint, currInfopointIndex) => {
- return {
- ...innerAcc,
- [`${currImageIndex}-${currInfopointIndex}`]: {
- isOpen: currInfopoint.alwaysVisible,
- isAlwaysVisible: currInfopoint.alwaysVisible,
- },
- };
- },
- {}
- );
-
- return { ...acc, ...reducedImage };
- },
- {}
- );
-
- return infopointsMap as Record;
-};
-
-export const parseGameQuizScreenMap = (viewScreen: GameQuizScreen) => {
- const infopointsMap = viewScreen.answers?.reduce(
- (acc, currAnswer, currAnswerIndex) => {
- const reducedAnswer = currAnswer.infopoints?.reduce(
- (innerAcc, currInfopoint, currInfopointIndex) => {
- return {
- ...innerAcc,
- [`${currAnswerIndex}-${currInfopointIndex}`]: {
- isOpen: currInfopoint.alwaysVisible,
- isAlwaysVisible: currInfopoint.alwaysVisible,
- },
- };
- },
- {}
- );
-
- return { ...acc, ...reducedAnswer };
- },
- {}
- );
-
- return infopointsMap as Record;
-};
-
-export const parseImageScreenMap = (viewScreen: ImageScreen) => {
- const infopointsMap = viewScreen?.infopoints?.reduce(
- (acc, currInfopoint, currInfopointIndex) => {
- return {
- ...acc,
- [`${currInfopointIndex}`]: {
- isOpen: currInfopoint.alwaysVisible,
- isAlwaysVisible: currInfopoint.alwaysVisible,
- },
- };
- },
- {}
- );
-
- return infopointsMap as Record;
-};
-
-export const parseImageChangeScreenMap = (viewScreen: ImageChangeScreen) => {
- const infopointsMapFirst = viewScreen?.image1Infopoints?.reduce(
- (acc, currInfopoint, currInfopointIndex) => {
- return {
- ...acc,
- [`0-${currInfopointIndex}`]: {
- isOpen: currInfopoint.alwaysVisible,
- isAlwaysVisible: currInfopoint.alwaysVisible,
- },
- };
- },
- {}
- );
-
- const infopointsMapSecond = viewScreen?.image2Infopoints?.reduce(
- (acc, currInfopoint, currInfopointIndex) => {
- return {
- ...acc,
- [`1-${currInfopointIndex}`]: {
- isOpen: currInfopoint.alwaysVisible,
- isAlwaysVisible: currInfopoint.alwaysVisible,
- },
- };
- },
- {}
- );
-
- const infopointsMap = { ...infopointsMapFirst, ...infopointsMapSecond };
- return infopointsMap as Record;
-};
diff --git a/web/src/components/infopoint/screen-to-map-parsers/game-quiz-parser.ts b/web/src/components/infopoint/screen-to-map-parsers/game-quiz-parser.ts
new file mode 100644
index 00000000..b0b29899
--- /dev/null
+++ b/web/src/components/infopoint/screen-to-map-parsers/game-quiz-parser.ts
@@ -0,0 +1,26 @@
+import { GameQuizScreen } from "models";
+import { InfopointStatusMap } from "../useTooltipInfopoint";
+
+export const parseGameQuizScreenMap = (viewScreen: GameQuizScreen) => {
+ const infopointsMap = viewScreen.answers?.reduce(
+ (acc, currAnswer, currAnswerIndex) => {
+ const reducedAnswer = currAnswer.infopoints?.reduce(
+ (innerAcc, currInfopoint, currInfopointIndex) => {
+ return {
+ ...innerAcc,
+ [`${currAnswerIndex}-${currInfopointIndex}`]: {
+ isOpen: currInfopoint.alwaysVisible,
+ isAlwaysVisible: currInfopoint.alwaysVisible,
+ },
+ };
+ },
+ {}
+ );
+
+ return { ...acc, ...reducedAnswer };
+ },
+ {}
+ );
+
+ return infopointsMap as InfopointStatusMap;
+};
diff --git a/web/src/components/infopoint/screen-to-map-parsers/image-change-parser.ts b/web/src/components/infopoint/screen-to-map-parsers/image-change-parser.ts
new file mode 100644
index 00000000..d8c6d0ee
--- /dev/null
+++ b/web/src/components/infopoint/screen-to-map-parsers/image-change-parser.ts
@@ -0,0 +1,33 @@
+import { ImageChangeScreen } from "models";
+import { InfopointStatusMap } from "../useTooltipInfopoint";
+
+export const parseImageChangeScreenMap = (viewScreen: ImageChangeScreen) => {
+ const infopointsMapFirst = viewScreen?.image1Infopoints?.reduce(
+ (acc, currInfopoint, currInfopointIndex) => {
+ return {
+ ...acc,
+ [`0-${currInfopointIndex}`]: {
+ isOpen: currInfopoint.alwaysVisible,
+ isAlwaysVisible: currInfopoint.alwaysVisible,
+ },
+ };
+ },
+ {}
+ );
+
+ const infopointsMapSecond = viewScreen?.image2Infopoints?.reduce(
+ (acc, currInfopoint, currInfopointIndex) => {
+ return {
+ ...acc,
+ [`1-${currInfopointIndex}`]: {
+ isOpen: currInfopoint.alwaysVisible,
+ isAlwaysVisible: currInfopoint.alwaysVisible,
+ },
+ };
+ },
+ {}
+ );
+
+ const infopointsMap = { ...infopointsMapFirst, ...infopointsMapSecond };
+ return infopointsMap as InfopointStatusMap;
+};
diff --git a/web/src/components/infopoint/screen-to-map-parsers/image-parser.ts b/web/src/components/infopoint/screen-to-map-parsers/image-parser.ts
new file mode 100644
index 00000000..171054c8
--- /dev/null
+++ b/web/src/components/infopoint/screen-to-map-parsers/image-parser.ts
@@ -0,0 +1,19 @@
+import { ImageScreen } from "models";
+import { InfopointStatusMap } from "../useTooltipInfopoint";
+
+export const parseImageScreenMap = (viewScreen: ImageScreen) => {
+ const infopointsMap = viewScreen?.infopoints?.reduce(
+ (acc, currInfopoint, currInfopointIndex) => {
+ return {
+ ...acc,
+ [`${currInfopointIndex}`]: {
+ isOpen: currInfopoint.alwaysVisible,
+ isAlwaysVisible: currInfopoint.alwaysVisible,
+ },
+ };
+ },
+ {}
+ );
+
+ return infopointsMap as InfopointStatusMap;
+};
diff --git a/web/src/components/infopoint/screen-to-map-parsers/index.ts b/web/src/components/infopoint/screen-to-map-parsers/index.ts
new file mode 100644
index 00000000..c02dbc3b
--- /dev/null
+++ b/web/src/components/infopoint/screen-to-map-parsers/index.ts
@@ -0,0 +1,28 @@
+import { parseGameQuizScreenMap } from "./game-quiz-parser";
+import { parseImageChangeScreenMap } from "./image-change-parser";
+import { parseImageScreenMap } from "./image-parser";
+import { parseSlideshowScreenMap } from "./slideshow-parser";
+
+import {
+ InfopointStatusMap,
+ InfopointSupportedScreens,
+} from "../useTooltipInfopoint";
+
+import { screenType } from "enums/screen-type";
+
+export const parseScreenToInfopointStatusMap = (
+ viewScreen: InfopointSupportedScreens
+): InfopointStatusMap => {
+ switch (viewScreen.type) {
+ case screenType.GAME_OPTIONS:
+ return parseGameQuizScreenMap(viewScreen);
+ case screenType.IMAGE_CHANGE:
+ return parseImageChangeScreenMap(viewScreen);
+ case screenType.IMAGE:
+ return parseImageScreenMap(viewScreen);
+ case screenType.SLIDESHOW:
+ return parseSlideshowScreenMap(viewScreen);
+ default:
+ throw new Error("Unsupported view screen type for infopoint map parser.");
+ }
+};
diff --git a/web/src/components/infopoint/screen-to-map-parsers/slideshow-parser.ts b/web/src/components/infopoint/screen-to-map-parsers/slideshow-parser.ts
new file mode 100644
index 00000000..129499be
--- /dev/null
+++ b/web/src/components/infopoint/screen-to-map-parsers/slideshow-parser.ts
@@ -0,0 +1,26 @@
+import { SlideshowScreen } from "models";
+import { InfopointStatusMap } from "../useTooltipInfopoint";
+
+export const parseSlideshowScreenMap = (viewScreen: SlideshowScreen) => {
+ const infopointsMap = viewScreen.images?.reduce(
+ (acc, currImage, currImageIndex) => {
+ const reducedImage = currImage.infopoints.reduce(
+ (innerAcc, currInfopoint, currInfopointIndex) => {
+ return {
+ ...innerAcc,
+ [`${currImageIndex}-${currInfopointIndex}`]: {
+ isOpen: currInfopoint.alwaysVisible,
+ isAlwaysVisible: currInfopoint.alwaysVisible,
+ },
+ };
+ },
+ {}
+ );
+
+ return { ...acc, ...reducedImage };
+ },
+ {}
+ );
+
+ return infopointsMap as InfopointStatusMap;
+};
diff --git a/web/src/components/infopoint/useTooltipInfopoint.tsx b/web/src/components/infopoint/useTooltipInfopoint.tsx
index cc85aa17..3162c8c5 100644
--- a/web/src/components/infopoint/useTooltipInfopoint.tsx
+++ b/web/src/components/infopoint/useTooltipInfopoint.tsx
@@ -1,20 +1,14 @@
-import { useState, useCallback, useEffect } from "react";
+import { useState } from "react";
-// Components
-import ScreenAnchorInfopoint from "./ScreenAnchorInfopoint";
-import TooltipInfoPoint from "./TooltipInfopoint";
+import { useMobileInfopointAutoClosing } from "./hooks/useMobileInfopointAutoClosing";
+import { useInfopointClosing } from "./hooks/useInfopointClosing";
-import { useMediaQuery } from "@mui/material";
-import { breakpoints } from "hooks/media-query-hook/breakpoints";
+// Components
+import AnchorInfopoint from "./components/anchor-infopoint";
+import TooltipInfoPoint from "./components/tooltip-infopoint/TooltipInfopoint";
// Utils
-import {
- parseImageScreenMap,
- parseSlideshowScreenMap,
- parseImageChangeScreenMap,
- parseGameQuizScreenMap,
- InfopointStatusObject,
-} from "./parseScreenMaps";
+import { parseScreenToInfopointStatusMap } from "./screen-to-map-parsers";
// Models
import {
@@ -23,16 +17,22 @@ import {
ImageChangeScreen,
GameQuizScreen,
} from "models";
-import { screenType } from "enums/screen-type";
// - - - - -
-type InfopointSupportedScreens =
+export type InfopointSupportedScreens =
| ImageScreen
| SlideshowScreen
| ImageChangeScreen
| GameQuizScreen;
+export type InfopointStatusObject = {
+ isOpen: boolean;
+ isAlwaysVisible: boolean;
+};
+
+export type InfopointStatusMap = Record;
+
/**
* 1. Import this hook into screen where you want to use Infopoints
* 2. Implement map parser for your type of screen, currently implemented parsers are highly usable
@@ -43,13 +43,13 @@ type InfopointSupportedScreens =
* the infopoint is currently opened or closed
* - alwaysVisible in czech "stale zobrazen", is a checkbox in screen administration
* - alwaysVisible infopoint is at the beginning open, thats the only difference from another infopoint
- * 3. Use ScreenAnchorInfopoint component in your screen
+ * 3. Use AnchorInfopoint component in your screen
* - requires top and left props as absolute offset positioning
* - requires id and content prop, which is used as data-tooltip-id, data-tooltip-content
* - id of SquareInfopoint must be the same as the id prop of TooltipInfopoint
* - content of SquareInfopoint will be through id linked and used in TooltipInfopoint
* 4. Apply TooltipInfopoint component
- * - requires id props which must be the same as the supplied id in ScreenAnchorInfopoint
+ * - requires id props which must be the same as the supplied id in AnchorInfopoint
* - requires information whether this infopoint is marked as alwaysVisible from administration
* - requires infopointOpenStatusMap and setter of this map.. created in first step and returned by this hook
* - requires primary and optionally secondary key, they will be combined into one mapKey
@@ -58,135 +58,36 @@ type InfopointSupportedScreens =
* EXAMPLES: SLIDESHOW screen or IMAGE screen
*/
const useTooltipInfopoint = (viewScreen: InfopointSupportedScreens) => {
- const isSm = useMediaQuery(breakpoints.down("sm"));
-
const [isMapParsingDone, setIsMapParsingDone] = useState(false);
// Object containing information (isOpen, isAlwaysVisible) for each infopoint present in supported screen
// isAlwaysVisible is the mark for each infopoint, set in the slideshow administration
- const [infopointOpenStatusMap, setInfopointOpenStatusMap] = useState(() => {
- let parsedInfopointStatusMap: Record | null =
- null;
-
- if (viewScreen.type === screenType.GAME_OPTIONS) {
- parsedInfopointStatusMap = parseGameQuizScreenMap(viewScreen);
- } else if (viewScreen.type === screenType.SLIDESHOW) {
- parsedInfopointStatusMap = parseSlideshowScreenMap(viewScreen);
- } else if (viewScreen.type === screenType.IMAGE_CHANGE) {
- parsedInfopointStatusMap = parseImageChangeScreenMap(viewScreen);
- } else {
- // TODO - improve structure
- parsedInfopointStatusMap = parseImageScreenMap(viewScreen);
- }
-
- setIsMapParsingDone(true);
- return parsedInfopointStatusMap;
- });
-
- // - - -
-
- // On small (mobile) screens, all infopoint, even the alwaysVisible, are by default first closed and then could be opened
- useEffect(() => {
- if (!isSm) {
- setInfopointOpenStatusMap((prevMap) => {
- if (!prevMap) {
- return prevMap;
- }
- const entries = Object.entries(prevMap);
- const nextMap = entries.reduce(
- (acc, [key, infopointStatus]) => ({
- ...acc,
- [key]: {
- ...infopointStatus,
- isOpen: infopointStatus.isAlwaysVisible,
- },
- }),
- {} as Record
- );
-
- return nextMap;
- });
-
- return;
- }
-
- setInfopointOpenStatusMap((prevMap) => {
- if (!prevMap) {
- return prevMap;
- }
- const entries = Object.entries(prevMap);
- const nextMapWithClosedInfopoints = entries.reduce(
- (acc, [key, infopointStatus]) => ({
- ...acc,
- [key]: { ...infopointStatus, isOpen: false },
- }),
- {} as Record
- );
-
- return nextMapWithClosedInfopoints;
+ const [infopointStatusMap, setInfopointStatusMap] =
+ useState(() => {
+ const parsedInfopointMap = parseScreenToInfopointStatusMap(viewScreen);
+ setIsMapParsingDone(true);
+ return parsedInfopointMap;
});
- }, [isSm, isMapParsingDone]);
// - - -
- // Iterates through whole map and close all of the infopoints
- const closeAllInfopoints = useCallback(() => {
- if (!infopointOpenStatusMap) {
- return;
- }
- const entries = Object.entries(infopointOpenStatusMap);
- const closedEntries = entries.map(([key, infopoint]) => {
- return [key, { ...infopoint, isOpen: false }];
- });
-
- const newMap = Object.fromEntries(closedEntries);
- setInfopointOpenStatusMap(newMap);
- }, [infopointOpenStatusMap]);
-
- // Iterates through whole map, but close only infopoints of current photo
- const closePhotoInfopoints = useCallback(
- (photoIndex: number) => {
- if (!infopointOpenStatusMap) {
- return;
- }
- const entries = Object.entries(infopointOpenStatusMap);
- const closedEntries = entries.map(([key, infopoint]) => {
- const parsedPhotoKey = parseInt(key.charAt(0));
- if (!isNaN(parsedPhotoKey) && parsedPhotoKey === photoIndex) {
- return [key, { ...infopoint, isOpen: false }];
- }
- return [key, { ...infopoint }];
- });
-
- const newMap = Object.fromEntries(closedEntries);
- setInfopointOpenStatusMap(newMap);
- },
- [infopointOpenStatusMap]
- );
+ useMobileInfopointAutoClosing({
+ setInfopointStatusMap,
+ isMapParsingDone,
+ });
// - - -
- // Function which will close infopoints
- // Called e.g on ESC press + clicking on the image as outside any infopoint
- // Typescript method overloading
- function closeInfopoints(screen: GameQuizScreen): () => void;
- function closeInfopoints(screen: ImageScreen): () => void;
- function closeInfopoints(screen: ImageChangeScreen): () => void;
- function closeInfopoints(
- screen: SlideshowScreen
- ): (photoIndex: number) => void;
- function closeInfopoints(screen: InfopointSupportedScreens) {
- if (screen.type === screenType.SLIDESHOW) {
- return closePhotoInfopoints;
- }
- return closeAllInfopoints;
- }
+ const { closeInfopoints } = useInfopointClosing({
+ infopointStatusMap: infopointStatusMap,
+ setInfopointStatusMap: setInfopointStatusMap,
+ });
return {
- infopointOpenStatusMap,
- setInfopointOpenStatusMap,
+ infopointStatusMap,
+ setInfopointStatusMap,
closeInfopoints,
- ScreenAnchorInfopoint,
+ AnchorInfopoint,
TooltipInfoPoint,
};
};
diff --git a/web/src/components/tooltip/tooltip.tsx b/web/src/components/tooltip/BasicTooltip.tsx
similarity index 57%
rename from web/src/components/tooltip/tooltip.tsx
rename to web/src/components/tooltip/BasicTooltip.tsx
index 5485e1c3..32931b68 100644
--- a/web/src/components/tooltip/tooltip.tsx
+++ b/web/src/components/tooltip/BasicTooltip.tsx
@@ -1,13 +1,7 @@
-import { Tooltip as ReactTooltip, PlacesType } from "react-tooltip";
+import { Tooltip as ReactTooltip } from "react-tooltip";
import { useExpoDesignData } from "hooks/view-hooks/expo-design-data-hook";
-
-type TooltipProps = {
- id: string;
- content: string;
- variant?: "light" | "dark";
- place?: PlacesType;
- className?: string;
-};
+import { BasicTooltipProps } from "./tooltip-props";
+import { useMediaDevice } from "context/media-device-provider/media-device-provider";
/**
* Basic tooltip which listens to theme changes if variant is not overridden by prop
@@ -18,8 +12,15 @@ export const BasicTooltip = ({
variant,
place,
className,
-}: TooltipProps) => {
+ style,
+}: BasicTooltipProps) => {
const { isLightMode } = useExpoDesignData();
+ const { isMobile, isMobileLandscape } = useMediaDevice();
+
+ if (isMobile || isMobileLandscape) {
+ return null;
+ }
+
return (
);
};
diff --git a/web/src/components/tooltip/tooltip-props.ts b/web/src/components/tooltip/tooltip-props.ts
new file mode 100644
index 00000000..a7fdab54
--- /dev/null
+++ b/web/src/components/tooltip/tooltip-props.ts
@@ -0,0 +1,11 @@
+import { CSSProperties } from "react";
+import { PlacesType } from "react-tooltip";
+
+export type BasicTooltipProps = {
+ id: string;
+ content: string;
+ variant?: "light" | "dark";
+ place?: PlacesType;
+ className?: string;
+ style?: CSSProperties;
+};
diff --git a/web/src/constants/screen.ts b/web/src/constants/screen.ts
index 06f96abb..6c2b205a 100644
--- a/web/src/constants/screen.ts
+++ b/web/src/constants/screen.ts
@@ -1,7 +1,15 @@
export const defaultScreenTimeInSeconds = 20;
export const defaultScreenTimeInMs = defaultScreenTimeInSeconds * 1000;
-export const OVERLAY_UNACTIVE_TIMEOUT = 3000; // in miliseconds
+export const OVERLAY_UNACTIVE_TIMEOUT = 3000; // in milliseconds
export const ZOOM_SCREEN_DEFAULT_STAY_IN_DETAIL_TIME = 3; // in seconds
export const ZOOM_SCREEN_DEFAULT_SEQ_DELAY_TIME = 2; // in seconds
+
+export const GAME_SCREEN_DEFAULT_RESULT_TIME = 4; // in seconds
+
+export const GAME_FIND_DEFAULT_NUMBER_OF_PINS = 1;
+
+export const GAME_DRAW_DEFAULT_COLOR = "#000000";
+export const GAME_DRAW_DEFAULT_THICKNESS = 5;
+export const GAME_DRAW_DEFAULT_IS_ERASING = false;
diff --git a/web/src/containers/expo-administration/expo-editor/screen-game-draw/Images.tsx b/web/src/containers/expo-administration/expo-editor/screen-game-draw/Images.tsx
index b021ee3d..07f3c042 100644
--- a/web/src/containers/expo-administration/expo-editor/screen-game-draw/Images.tsx
+++ b/web/src/containers/expo-administration/expo-editor/screen-game-draw/Images.tsx
@@ -1,7 +1,9 @@
+import { useCallback } from "react";
import { useDispatch } from "react-redux";
import { useTranslation } from "react-i18next";
// Components
+import Button from "react-md/lib/Buttons/Button";
import Checkbox from "react-md/lib/SelectionControls/Checkbox";
import ImageBox from "components/editors/ImageBox";
import HelpIcon from "components/help-icon";
@@ -13,6 +15,10 @@ import { AppDispatch } from "store/store";
// Actions and utils
import { getFileById } from "actions/file-actions-typed";
import { updateScreenData } from "actions/expoActions";
+import {
+ GAME_DRAW_DEFAULT_COLOR,
+ GAME_DRAW_DEFAULT_THICKNESS,
+} from "constants/screen";
// - -
@@ -37,6 +43,15 @@ const Images = ({ activeScreen }: ImagesProps) => {
dispatch(updateScreenData({ image2: img.id }));
};
+ const resetInitialDrawingSettings = useCallback(() => {
+ dispatch(
+ updateScreenData({
+ initialColor: GAME_DRAW_DEFAULT_COLOR,
+ initialThickness: GAME_DRAW_DEFAULT_THICKNESS,
+ })
+ );
+ }, [dispatch]);
+
return (
@@ -80,12 +95,13 @@ const Images = ({ activeScreen }: ImagesProps) => {
/>
+
dispatch(updateScreenData({ showDrawing: value }))
}
@@ -95,6 +111,64 @@ const Images = ({ activeScreen }: ImagesProps) => {
id="editor-game-draw-show-drawing"
/>
+
+ {/* Initial settings (color and thickness) */}
+
+
{t("initialDrawingSettingsTitle")}
+
+
+
+
{t("initialDrawingColorLabel")}
+
{
+ const newInitialColor = e.target.value;
+ dispatch(updateScreenData({ initialColor: newInitialColor }));
+ }}
+ />
+
+ ({activeScreen.initialColor ?? GAME_DRAW_DEFAULT_COLOR})
+
+
+
+
+
{t("initialDrawingThicknessLabel")}
+
{
+ const newInitialThickness = parseInt(e.target.value);
+ dispatch(
+ updateScreenData({
+ initialThickness: newInitialThickness,
+ })
+ );
+ }}
+ />
+
+ ({activeScreen.initialThickness ?? GAME_DRAW_DEFAULT_THICKNESS})
+
+
+
+
+
+
+
+
);
diff --git a/web/src/containers/expo-administration/expo-editor/screen-game-find/Images.tsx b/web/src/containers/expo-administration/expo-editor/screen-game-find/Images.tsx
index b6505c1d..ecf7b861 100644
--- a/web/src/containers/expo-administration/expo-editor/screen-game-find/Images.tsx
+++ b/web/src/containers/expo-administration/expo-editor/screen-game-find/Images.tsx
@@ -1,11 +1,16 @@
import { useDispatch } from "react-redux";
import { useTranslation } from "react-i18next";
+import { useNumberOfPinsListener } from "./useNumberOfPinsListener";
+
// Components
import Checkbox from "react-md/lib/SelectionControls/Checkbox";
import ImageBox from "components/editors/ImageBox";
import HelpIcon from "components/help-icon";
+import { NumberOfPinsField } from "./NumberOfPinsField";
+import { PinTextField } from "./PinTextField";
+
// Models
import { GameFindScreen, File as IndihuFile } from "models";
import { AppDispatch } from "store/store";
@@ -13,6 +18,7 @@ import { AppDispatch } from "store/store";
// Actions and utils
import { getFileById } from "actions/file-actions-typed";
import { updateScreenData } from "actions/expoActions";
+import { GAME_FIND_DEFAULT_NUMBER_OF_PINS } from "constants/screen";
// - -
@@ -26,6 +32,9 @@ const Images = ({ activeScreen }: ImagesProps) => {
keyPrefix: "descFields.gameFindScreen",
});
+ const { numberOfPins = GAME_FIND_DEFAULT_NUMBER_OF_PINS, pinsTexts } =
+ activeScreen;
+
const image1 = dispatch(getFileById(activeScreen.image1));
const image2 = dispatch(getFileById(activeScreen.image2));
@@ -37,6 +46,9 @@ const Images = ({ activeScreen }: ImagesProps) => {
dispatch(updateScreenData({ image2: img.id }));
};
+ //
+ useNumberOfPinsListener(activeScreen);
+
return (
@@ -80,12 +92,13 @@ const Images = ({ activeScreen }: ImagesProps) => {
/>
+
dispatch(updateScreenData({ showTip: value }))
}
@@ -95,6 +108,39 @@ const Images = ({ activeScreen }: ImagesProps) => {
id="editor-game-find-show-tip"
/>
+
+ {/* Pins */}
+
+
+
+
+
+
+ {pinsTexts?.map((pinText: string, index: number) => {
+ if (index >= numberOfPins) {
+ return null;
+ }
+
+ const onPinTextUpdate = (newPinText: string) =>
+ dispatch(
+ updateScreenData({
+ pinsTexts: pinsTexts.map((pinText, idx) =>
+ index === idx ? newPinText : pinText
+ ),
+ })
+ );
+
+ return (
+
+ );
+ })}
+
+
);
diff --git a/web/src/containers/expo-administration/expo-editor/screen-game-find/NumberOfPinsField.tsx b/web/src/containers/expo-administration/expo-editor/screen-game-find/NumberOfPinsField.tsx
new file mode 100644
index 00000000..69f9890c
--- /dev/null
+++ b/web/src/containers/expo-administration/expo-editor/screen-game-find/NumberOfPinsField.tsx
@@ -0,0 +1,73 @@
+import { useState } from "react";
+import { useDispatch } from "react-redux";
+import { useTranslation } from "react-i18next";
+
+// Components
+import TextField from "react-md/lib/TextFields";
+import HelpIcon from "components/help-icon";
+
+// Models
+import { AppDispatch } from "store/store";
+
+// Actions and utils
+import { updateScreenData } from "actions/expoActions";
+
+// - - - -
+
+type NumberOfPinsFieldProps = {
+ numberOfPinsValue: number;
+};
+
+export const NumberOfPinsField = ({
+ numberOfPinsValue,
+}: NumberOfPinsFieldProps) => {
+ const { t } = useTranslation("expo-editor", {
+ keyPrefix: "descFields.gameFindScreen",
+ });
+
+ const dispatch = useDispatch();
+ const [error, setError] = useState(null);
+
+ return (
+
+
+
+ {
+ const numberValue = Number(newNumberOfPinsValue);
+ const ok =
+ !numberValue ||
+ isNaN(numberValue) ||
+ numberValue < 1 ||
+ numberValue > 10;
+ setError(ok ? "Zadejte číslo v rozsahu 1 až 10." : null);
+
+ if (!ok) {
+ dispatch(
+ updateScreenData({
+ numberOfPins: numberValue,
+ })
+ );
+ }
+ }}
+ />
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+
+ );
+};
diff --git a/web/src/containers/expo-administration/expo-editor/screen-game-find/PinTextField.tsx b/web/src/containers/expo-administration/expo-editor/screen-game-find/PinTextField.tsx
new file mode 100644
index 00000000..b21795d2
--- /dev/null
+++ b/web/src/containers/expo-administration/expo-editor/screen-game-find/PinTextField.tsx
@@ -0,0 +1,26 @@
+import TextField from "react-md/lib/TextFields";
+
+type PinTextFieldProps = {
+ pinTextValue: string;
+ index: number;
+ onPinTextUpdate: (newPinText: string) => void;
+};
+
+export const PinTextField = ({
+ pinTextValue,
+ index,
+ onPinTextUpdate,
+}: PinTextFieldProps) => {
+ return (
+
+ {
+ onPinTextUpdate(newPinTextValue);
+ }}
+ />
+
+ );
+};
diff --git a/web/src/containers/expo-administration/expo-editor/screen-game-find/useNumberOfPinsListener.ts b/web/src/containers/expo-administration/expo-editor/screen-game-find/useNumberOfPinsListener.ts
new file mode 100644
index 00000000..35012f85
--- /dev/null
+++ b/web/src/containers/expo-administration/expo-editor/screen-game-find/useNumberOfPinsListener.ts
@@ -0,0 +1,67 @@
+import { useEffect } from "react";
+import { useDispatch } from "react-redux";
+
+// Models
+import { GameFindScreen } from "models";
+import { AppDispatch } from "store/store";
+
+// Actions and utils
+import { updateScreenData } from "actions/expoActions";
+import { GAME_FIND_DEFAULT_NUMBER_OF_PINS } from "constants/screen";
+
+// - - - -
+
+export const extendPinsTextsArray = (
+ numberOfPins: number,
+ pinTexts: string[]
+): string[] => {
+ if (numberOfPins <= pinTexts.length) {
+ return pinTexts;
+ }
+
+ const extendedArray = pinTexts.slice(); // Create a copy of the input array
+
+ while (extendedArray.length < numberOfPins) {
+ extendedArray.push("");
+ }
+
+ return extendedArray;
+};
+
+// - - - -
+
+export const useNumberOfPinsListener = (activeScreen: GameFindScreen) => {
+ const dispatch = useDispatch();
+
+ const { numberOfPins = GAME_FIND_DEFAULT_NUMBER_OF_PINS, pinsTexts } =
+ activeScreen;
+
+ useEffect(() => {
+ // Pins Texts initialization for this type of screen
+ if (
+ numberOfPins === GAME_FIND_DEFAULT_NUMBER_OF_PINS &&
+ pinsTexts === undefined
+ ) {
+ dispatch(
+ updateScreenData({
+ pinsTexts: new Array(GAME_FIND_DEFAULT_NUMBER_OF_PINS).fill(""),
+ })
+ );
+
+ return;
+ }
+
+ if (pinsTexts === undefined) {
+ return;
+ }
+
+ // Other use case.. not initialization but change of number in field
+ const extendedArr = extendPinsTextsArray(numberOfPins, pinsTexts);
+ dispatch(
+ updateScreenData({
+ pinsTexts: extendedArr,
+ })
+ );
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [numberOfPins]);
+};
diff --git a/web/src/containers/expo-administration/expo-editor/screen-game-move/Images.tsx b/web/src/containers/expo-administration/expo-editor/screen-game-move/Images.tsx
index 61fa8bca..c8c67207 100644
--- a/web/src/containers/expo-administration/expo-editor/screen-game-move/Images.tsx
+++ b/web/src/containers/expo-administration/expo-editor/screen-game-move/Images.tsx
@@ -8,6 +8,8 @@ import TextField from "react-md/lib/TextFields";
import ImageBox from "components/editors/ImageBox";
import HelpIcon from "components/help-icon";
+import ObjectImagePreview from "./ObjectImagePreview";
+
// Models
import { GameMoveScreen, File as IndihuFile } from "models";
import { AppDispatch } from "store/store";
@@ -89,6 +91,7 @@ const Images = ({ activeScreen }: ImagesProps) => {
/>
+
{t("object")}
image
@@ -139,6 +142,7 @@ const Images = ({ activeScreen }: ImagesProps) => {
+
{object && (
{
alt=""
/>
)}
+
+ {image1 && object && (
+
+ )}
);
diff --git a/web/src/containers/expo-administration/expo-editor/screen-game-move/ObjectImagePreview.tsx b/web/src/containers/expo-administration/expo-editor/screen-game-move/ObjectImagePreview.tsx
new file mode 100644
index 00000000..47dce988
--- /dev/null
+++ b/web/src/containers/expo-administration/expo-editor/screen-game-move/ObjectImagePreview.tsx
@@ -0,0 +1,142 @@
+import { useDispatch } from "react-redux";
+import { useTranslation } from "react-i18next";
+import { animated } from "react-spring";
+
+import useResizeObserver from "hooks/use-resize-observer";
+import { useElementMove } from "hooks/spring-hooks/use-element-move";
+import { useElementResize } from "hooks/spring-hooks/use-element-resize";
+
+// Models
+import { AppDispatch } from "store/store";
+import { GameMoveScreen } from "models";
+
+// Actions and utils
+import { updateScreenData } from "actions/expoActions";
+import { calculateObjectFit } from "utils/object-fit";
+
+// Assets
+import expandImg from "../../../../assets/img/expand.png";
+
+// - - - -
+
+type ObjectImagePreviewProps = {
+ activeScreen: GameMoveScreen;
+ image1Src: string;
+ objectImgSrc: string;
+};
+
+const ObjectImagePreview = ({
+ activeScreen,
+ image1Src,
+ objectImgSrc,
+}: ObjectImagePreviewProps) => {
+ const dispatch = useDispatch();
+ const { t } = useTranslation("expo-editor", {
+ keyPrefix: "descFields.gameMoveScreen",
+ });
+
+ const image1OrigData = activeScreen.image1OrigData ?? { width: 0, height: 0 };
+ const objectOrigData = activeScreen.objectOrigData ?? { width: 0, height: 0 };
+
+ const [containerRef, containerSize] = useResizeObserver();
+ const [objectRef, objectSize] = useResizeObserver();
+
+ const {
+ width: containedImg1Width,
+ height: containedImg1Height,
+ left: fromLeft,
+ top: fromTop,
+ } = calculateObjectFit({
+ type: "contain",
+ parent: containerSize,
+ child: image1OrigData,
+ });
+
+ const { moveSpring, bindMoveDrag } = useElementMove({
+ containerSize: containerSize,
+ dragMovingObjectSize: objectSize,
+ initialPosition: activeScreen.objectPositionProps?.containerPosition,
+ additionalCallback: (left, top) => {
+ dispatch(
+ updateScreenData({
+ objectPositionProps: {
+ containerPosition: { left: left, top: top },
+ containedImgPosition: {
+ left: left - fromLeft,
+ top: top - fromTop,
+ },
+ },
+ })
+ );
+ },
+ });
+
+ const { resizeSpring, bindResizeDrag } = useElementResize({
+ containerSize: containerSize,
+ dragResizingImgOrigData: objectOrigData,
+ initialSize: activeScreen.objectSizeProps?.inContainerSize,
+ additionalCallback: (width, height) => {
+ dispatch(
+ updateScreenData({
+ objectSizeProps: {
+ inContainerSize: { width: width, height: height },
+ inContainedImgFractionSize: {
+ width: width / containedImg1Width,
+ height: height / containedImg1Height,
+ },
+ },
+ })
+ );
+ },
+ });
+
+ return (
+
+
+ {t("screenPreviewText")}
+
+
+
+
+ );
+};
+
+export default ObjectImagePreview;
diff --git a/web/src/containers/expo-administration/expo-editor/screen-game-options/answers/AnswerItem.tsx b/web/src/containers/expo-administration/expo-editor/screen-game-options/answers/AnswerItem.tsx
index d179a6a4..83e44e62 100644
--- a/web/src/containers/expo-administration/expo-editor/screen-game-options/answers/AnswerItem.tsx
+++ b/web/src/containers/expo-administration/expo-editor/screen-game-options/answers/AnswerItem.tsx
@@ -260,7 +260,7 @@ const AnswerItem = ({
();
+
const { section, screen } = useSectionScreenParams();
const {
@@ -126,7 +126,11 @@ export const NewViewScreen = ({
musicRef.loop = true;
musicRef.volume = expoVolumes.musicVolume.actualVolume / 100;
if (shouldIncrement) {
- musicRef.play();
+ musicRef.play().catch((error) => {
+ if (error instanceof Error && error.name === "NotAllowedError") {
+ dispatch(setViewProgress({ shouldIncrement: false }));
+ }
+ });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [musicSrc, musicRef]);
@@ -158,7 +162,11 @@ export const NewViewScreen = ({
if (shouldIncrement) {
audioRef.volume = expoVolumes.speechVolume.actualVolume / 100;
- audioRef.play().catch((_error) => noop);
+ audioRef.play().catch((error) => {
+ if (error instanceof Error && error.name === "NotAllowedError") {
+ dispatch(setViewProgress({ shouldIncrement: false }));
+ }
+ });
}
return () => {
@@ -184,7 +192,11 @@ export const NewViewScreen = ({
useEffect(() => {
if (musicRef) {
if (shouldIncrement && !isMusicDisabled) {
- musicRef.play();
+ musicRef.play().catch((error) => {
+ if (error instanceof Error && error.name === "NotAllowedError") {
+ dispatch(setViewProgress({ shouldIncrement: false }));
+ }
+ });
}
if (!shouldIncrement && !isMusicDisabled) {
musicRef.pause();
@@ -193,7 +205,11 @@ export const NewViewScreen = ({
if (audioRef) {
if (shouldIncrement) {
- audioRef.play().catch((_error) => noop);
+ audioRef.play().catch((error) => {
+ if (error instanceof Error && error.name === "NotAllowedError") {
+ dispatch(setViewProgress({ shouldIncrement: false }));
+ }
+ });
} else {
audioRef.pause();
}
diff --git a/web/src/containers/expositions/ExpoCard.tsx b/web/src/containers/expositions/ExpoCard.tsx
index a4bbe01a..76c26f0f 100644
--- a/web/src/containers/expositions/ExpoCard.tsx
+++ b/web/src/containers/expositions/ExpoCard.tsx
@@ -148,7 +148,7 @@ const ExpoCard = ({
>
+
}
color="primary"
style={{ width: "38px", height: "31px", border: "2px solid white" }}
onClick={onGameReset}
- />
-
{/* Done button */}
-
+
}
color="primary"
style={{ width: "38px", height: "31px", border: "2px solid white" }}
onClick={onGameFinish}
- />
-
diff --git a/web/src/containers/views/games/GameInfoPanel.tsx b/web/src/containers/views/games/GameInfoPanel.tsx
index bd5d26f3..7dbe9eb4 100644
--- a/web/src/containers/views/games/GameInfoPanel.tsx
+++ b/web/src/containers/views/games/GameInfoPanel.tsx
@@ -1,4 +1,3 @@
-import { useTranslation } from "react-i18next";
import { useExpoDesignData } from "hooks/view-hooks/expo-design-data-hook";
import { Icon } from "components/icon/icon";
import { RefCallback } from "context/tutorial-provider/use-tutorial";
@@ -8,40 +7,35 @@ import cx from "classnames";
type GameInfoPanelProps = {
gameScreen: { title?: string; task?: string };
isGameFinished?: boolean;
- text?: string; // if not present, screen title with custom game task wont be visible
bindTutorial: { ref: RefCallback };
+ solutionText: string;
};
export const GameInfoPanel = ({
gameScreen,
isGameFinished,
- text,
bindTutorial,
+ solutionText,
}: GameInfoPanelProps) => {
- const { t } = useTranslation("view-screen");
const { bgFgTheming } = useExpoDesignData();
const { title, task } = gameScreen;
return (
- {text && (
-
-
-
-
- {isGameFinished ? t("game.solution") : title}
-
-
- {task && (
-
{task}
- )}
+
+
+
+
+ {isGameFinished ? solutionText : title}
+
- )}
+ {task &&
{task} }
+
);
};
diff --git a/web/src/containers/views/games/game-draw/configureContext.ts b/web/src/containers/views/games/game-draw/configureContext.ts
index fd82efe2..0a77d2a9 100644
--- a/web/src/containers/views/games/game-draw/configureContext.ts
+++ b/web/src/containers/views/games/game-draw/configureContext.ts
@@ -1,8 +1,8 @@
import {
- DEFAULT_COLOR,
- DEFAULT_THICKNESS,
- DEFAULT_IS_ERASING,
-} from "./game-draw";
+ GAME_DRAW_DEFAULT_COLOR,
+ GAME_DRAW_DEFAULT_THICKNESS,
+ GAME_DRAW_DEFAULT_IS_ERASING,
+} from "constants/screen";
export const configureContext = (
ctx: CanvasRenderingContext2D | null,
@@ -10,13 +10,15 @@ export const configureContext = (
thickness?: number,
isErasing?: boolean
) => {
- if (ctx === null) return;
+ if (ctx === null) {
+ return;
+ }
- const erasing = isErasing ?? DEFAULT_IS_ERASING;
+ const erasing = isErasing ?? GAME_DRAW_DEFAULT_IS_ERASING;
- ctx.fillStyle = color ?? DEFAULT_COLOR;
- ctx.strokeStyle = color ?? DEFAULT_COLOR;
- ctx.lineWidth = thickness ?? DEFAULT_THICKNESS;
+ ctx.fillStyle = color ?? GAME_DRAW_DEFAULT_COLOR;
+ ctx.strokeStyle = color ?? GAME_DRAW_DEFAULT_COLOR;
+ ctx.lineWidth = thickness ?? GAME_DRAW_DEFAULT_THICKNESS;
ctx.globalCompositeOperation = erasing ? "destination-out" : "source-over";
ctx.lineCap = "round";
return ctx;
diff --git a/web/src/containers/views/games/game-draw/game-draw.tsx b/web/src/containers/views/games/game-draw/game-draw.tsx
index 339ac87f..a9fcd414 100644
--- a/web/src/containers/views/games/game-draw/game-draw.tsx
+++ b/web/src/containers/views/games/game-draw/game-draw.tsx
@@ -1,39 +1,47 @@
-import { useCallback, useState, MouseEvent, useRef, useEffect } from "react";
-import { animated, useTransition } from "react-spring";
import ReactDOM from "react-dom";
-import cx from "classnames";
+import { useCallback, useState, useRef } from "react";
+import { animated, useTransition } from "react-spring";
import { useTranslation } from "react-i18next";
-import { createSelector } from "reselect";
import { useSelector } from "react-redux";
-import { Icon } from "components/icon/icon";
+import { createSelector } from "reselect";
-import { Position, ScreenProps } from "models";
-import { AppState } from "store/store";
-import { GameDrawScreen } from "models";
+// Custom hooks
+import { useTutorial } from "context/tutorial-provider/use-tutorial";
+import { useGameAutoNavigationOnResultTimeElapsed } from "../useGameAutoNavigationOnResultTimeElapsed";
+import { useBoolean } from "hooks/boolean-hook";
+import { useGameDraw } from "./useGameDraw";
-import classes from "./game-draw.module.scss";
+// Components
import { GameInfoPanel } from "../GameInfoPanel";
-import { Button } from "components/button/button";
-import { useBoolean } from "hooks/boolean-hook";
-import { Popper } from "components/popper/popper";
import { GameActionsPanel } from "../GameActionsPanel";
-import { BasicTooltip } from "components/tooltip/tooltip";
-import { useTutorial } from "context/tutorial-provider/use-tutorial";
-import { configureContext } from "./configureContext";
-// - -
+import { Popper } from "components/popper/popper";
+import { Button } from "components/button/button";
+import { Icon } from "components/icon/icon";
+
+// Models
+import { GameDrawScreen, ScreenProps } from "models";
+import { AppState } from "store/store";
-export const DEFAULT_COLOR = "#000000";
-export const DEFAULT_THICKNESS = 5;
-export const DEFAULT_IS_ERASING = false;
+// Utils
+import cx from "classnames";
+import classes from "./game-draw.module.scss";
+import { GAME_SCREEN_DEFAULT_RESULT_TIME } from "constants/screen";
+import {
+ GAME_DRAW_DEFAULT_COLOR,
+ GAME_DRAW_DEFAULT_THICKNESS,
+ GAME_DRAW_DEFAULT_IS_ERASING,
+} from "constants/screen";
-// - -
+// - - - -
const stateSelector = createSelector(
({ expo }: AppState) => expo.viewScreen as GameDrawScreen,
(viewScreen) => ({ viewScreen })
);
+// - - - -
+
export const GameDraw = ({
screenPreloadedFiles,
infoPanelRef,
@@ -43,112 +51,56 @@ export const GameDraw = ({
const { t } = useTranslation("view-screen");
const { viewScreen } = useSelector(stateSelector);
- const canvasRef = useRef
(null);
- const [ctx, setCtx] = useState(null);
+ const {
+ resultTime = GAME_SCREEN_DEFAULT_RESULT_TIME,
+ showDrawing = false,
+ initialColor = GAME_DRAW_DEFAULT_COLOR,
+ initialThickness = GAME_DRAW_DEFAULT_THICKNESS,
+ } = viewScreen;
- const [isDrawing, setIsDrawing] = useState(false);
- const [mousePosition, setMousePosition] = useState(null); // setting always, even when the pen or erase is not down
+ const { image1: assignmentImgSrc, image2: resultingImgSrc } =
+ screenPreloadedFiles;
- const [color, setColor] = useState(DEFAULT_COLOR);
+ // - - States - -
+
+ const [color, setColor] = useState(initialColor);
+
+ const [thickness, setThickness] = useState(initialThickness);
+
+ const [isErasing, { toggle: toggleTool }] = useBoolean(
+ GAME_DRAW_DEFAULT_IS_ERASING
+ );
- const [thickness, setThickness] = useState(DEFAULT_THICKNESS);
const [thicknessAnchor, setThicknessAnchor] =
useState(null);
+
const [
isThicknessPopoverOpen,
{ toggle: toggleThicknessPopover, setFalse: closeThicknessPopover },
] = useBoolean(false);
- const [isErasing, { toggle: toggleTool }] = useBoolean(DEFAULT_IS_ERASING);
-
- const [isGameFinished, setIsGameFinished] = useState(false);
-
- // - -
-
- useEffect(() => {
- if (!canvasRef.current) return; // should not happen
-
- canvasRef.current.width = window.innerWidth;
- canvasRef.current.height = window.innerHeight;
-
- const context = canvasRef.current.getContext("2d");
- setCtx(context);
- }, []);
-
- const resizeCanvas = useCallback(() => {
- if (!canvasRef.current) return;
-
- canvasRef.current.width = window.innerWidth;
- canvasRef.current.height = window.innerHeight;
- }, []);
+ const canvasRef = useRef(null);
- useEffect(() => {
- window.addEventListener("resize", resizeCanvas);
- return () => window.removeEventListener("resize", resizeCanvas);
- }, [resizeCanvas]);
+ const [isGameFinished, setIsGameFinished] = useState(false);
- // - -
+ // - - Draw functionality - -
- useEffect(() => {
- if (!ctx) return;
-
- configureContext(ctx, color, thickness, isErasing);
- }, [
+ const { startDrawing, stopDrawing, draw, clearCanvas } = useGameDraw({
+ canvasRef,
+ isGameFinished,
color,
thickness,
isErasing,
- ctx,
- canvasRef.current?.width,
- canvasRef.current?.height,
- ]);
-
- // - -
-
- const startDrawing = useCallback((e: MouseEvent) => {
- setMousePosition({ left: e.clientX, top: e.clientY });
- setIsDrawing(true);
- }, []);
-
- const stopDrawing = useCallback(() => {
- setIsDrawing(false);
- }, []);
-
- const draw = useCallback(
- (e: MouseEvent) => {
- if (!isDrawing || isGameFinished || !ctx || !mousePosition) {
- return;
- }
-
- ctx.beginPath();
- ctx.moveTo(mousePosition.left, mousePosition.top);
- ctx.lineTo(e.clientX, e.clientY);
- ctx.stroke();
-
- setMousePosition({ left: e.clientX, top: e.clientY });
- },
- [isDrawing, isGameFinished, mousePosition, ctx]
- );
-
- // - -
-
- const clearCanvas = useCallback(() => {
- if (!canvasRef.current) return;
- if (!ctx) return;
-
- ctx.clearRect(
- 0,
- 0,
- canvasRef.current.width ?? 0,
- canvasRef.current.height ?? 0
- );
- }, [ctx]);
+ });
- // - -
+ // - - - -
const onGameFinish = useCallback(() => {
setIsGameFinished(true);
- clearCanvas();
- }, [clearCanvas]);
+ if (!showDrawing) {
+ clearCanvas();
+ }
+ }, [clearCanvas, showDrawing]);
const onGameReset = useCallback(() => {
setIsGameFinished(false);
@@ -166,41 +118,34 @@ export const GameDraw = ({
// - - Tutorial stuff - -
- const { bind, TutorialTooltip, escapeTutorial } = useTutorial(
- "gameDraw",
- !isMobileOverlay
- );
+ const { bind, TutorialTooltip } = useTutorial("gameDraw", {
+ shouldOpen: !isMobileOverlay,
+ closeOnEsc: true,
+ });
- const onKeydownAction = useCallback(
- (event) => {
- if (event.key === "Escape") {
- escapeTutorial();
- }
- },
- [escapeTutorial]
- );
+ // - -
- useEffect(() => {
- document.addEventListener("keydown", onKeydownAction);
- return () => document.removeEventListener("keydown", onKeydownAction);
+ useGameAutoNavigationOnResultTimeElapsed({
+ gameResultTime: resultTime * 1000,
+ isGameFinished: isGameFinished,
});
return (
-
- {transition(({ opacity }, finished) =>
- !finished ? (
+
+ {transition(({ opacity }, isGameFinished) =>
+ !isGameFinished ? (
) : (
)
)}
@@ -239,8 +184,8 @@ export const GameDraw = ({
,
infoPanelRef.current
)}
@@ -253,48 +198,41 @@ export const GameDraw = ({
onGameFinish={onGameFinish}
onGameReset={onGameReset}
gameActions={[
-
+
}
- />
-
,
-
+
setThicknessAnchor(ref)}
color="expoTheme"
onClick={toggleThicknessPopover}
iconBefore={ }
- />
-
,
-
-
+
+
setColor(e.target.value)}
/>
-
,
]}
/>,
diff --git a/web/src/containers/views/games/game-draw/useGameDraw.ts b/web/src/containers/views/games/game-draw/useGameDraw.ts
new file mode 100644
index 00000000..14327029
--- /dev/null
+++ b/web/src/containers/views/games/game-draw/useGameDraw.ts
@@ -0,0 +1,135 @@
+import {
+ useState,
+ useEffect,
+ useCallback,
+ MutableRefObject,
+ MouseEvent,
+} from "react";
+import { Position } from "models";
+import { configureContext } from "./configureContext";
+
+type UseGameDrawProps = {
+ canvasRef: MutableRefObject;
+ isGameFinished: boolean;
+ color: string;
+ thickness: number;
+ isErasing: boolean;
+};
+
+// NOTE: canvasRef should never be null, because useEffect runs after all components are painted to DOM
+export const useGameDraw = ({
+ canvasRef,
+ isGameFinished,
+ color,
+ thickness,
+ isErasing,
+}: UseGameDrawProps) => {
+ const [isDrawing, setIsDrawing] = useState(false);
+ const [mousePosition, setMousePosition] = useState(null); // setting always, even when the pen or erase is not down
+
+ const [ctx, setCtx] = useState(null);
+
+ // - - - -
+
+ useEffect(() => {
+ if (!canvasRef.current) {
+ return; // should not happen
+ }
+
+ canvasRef.current.width = window.innerWidth;
+ canvasRef.current.height = window.innerHeight;
+
+ const context = canvasRef.current.getContext("2d");
+ setCtx(context);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ // - - - -
+
+ const resizeCanvas = useCallback(() => {
+ if (!canvasRef.current) {
+ return; // should not happen
+ }
+
+ canvasRef.current.width = window.innerWidth;
+ canvasRef.current.height = window.innerHeight;
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ useEffect(() => {
+ window.addEventListener("resize", resizeCanvas);
+ return () => window.removeEventListener("resize", resizeCanvas);
+ }, [resizeCanvas]);
+
+ // - - - -
+
+ useEffect(() => {
+ if (!ctx) {
+ return;
+ }
+
+ configureContext(ctx, color, thickness, isErasing);
+ }, [
+ ctx,
+ color,
+ thickness,
+ isErasing,
+ canvasRef.current?.width,
+ canvasRef.current?.height,
+ ]);
+
+ // - - - -
+
+ const startDrawing = useCallback((e: MouseEvent) => {
+ setMousePosition({ left: e.clientX, top: e.clientY });
+ setIsDrawing(true);
+ }, []);
+
+ const stopDrawing = useCallback(() => {
+ setIsDrawing(false);
+ }, []);
+
+ const draw = useCallback(
+ (e: MouseEvent) => {
+ if (!ctx) {
+ return;
+ }
+
+ if (!isDrawing || isGameFinished) {
+ return;
+ }
+
+ if (!mousePosition) {
+ return;
+ }
+
+ ctx.beginPath();
+ ctx.moveTo(mousePosition.left, mousePosition.top);
+ ctx.lineTo(e.clientX, e.clientY);
+ ctx.stroke();
+
+ setMousePosition({ left: e.clientX, top: e.clientY });
+ },
+ [isDrawing, isGameFinished, mousePosition, ctx]
+ );
+
+ const clearCanvas = useCallback(() => {
+ if (!canvasRef.current) {
+ return; // should not happen
+ }
+
+ if (!ctx) {
+ return;
+ }
+
+ ctx.clearRect(
+ 0,
+ 0,
+ canvasRef.current.width ?? 0,
+ canvasRef.current.height ?? 0
+ );
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [ctx]);
+
+ return { startDrawing, stopDrawing, draw, clearCanvas };
+};
diff --git a/web/src/containers/views/games/game-erase/game-erase.module.scss b/web/src/containers/views/games/game-erase/game-erase.module.scss
index 78b950ad..f4dcfa0e 100644
--- a/web/src/containers/views/games/game-erase/game-erase.module.scss
+++ b/web/src/containers/views/games/game-erase/game-erase.module.scss
@@ -29,3 +29,12 @@
.eraserWipetowel {
cursor: url("../../../../assets/img/erasers/wipe_towel.png") 10 20, auto;
}
+
+// - - - -
+
+.erase-container {
+ user-select: none; /* Disables text selection */
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+}
diff --git a/web/src/containers/views/games/game-erase/game-erase.tsx b/web/src/containers/views/games/game-erase/game-erase.tsx
index 5588e525..76e01ffb 100644
--- a/web/src/containers/views/games/game-erase/game-erase.tsx
+++ b/web/src/containers/views/games/game-erase/game-erase.tsx
@@ -25,6 +25,8 @@ import classes from "./game-erase.module.scss";
import { calculateObjectFit } from "utils/object-fit";
import { useExpoDesignData } from "hooks/view-hooks/expo-design-data-hook";
import { useTutorial } from "context/tutorial-provider/use-tutorial";
+import { useGameAutoNavigationOnResultTimeElapsed } from "../useGameAutoNavigationOnResultTimeElapsed";
+import { GAME_SCREEN_DEFAULT_RESULT_TIME } from "constants/screen";
const stateSelector = createSelector(
({ expo }: AppState) => expo.viewScreen as GameWipeScreen,
@@ -41,9 +43,11 @@ export const GameErase = ({
actionsPanelRef,
isMobileOverlay,
}: ScreenProps) => {
- const { t } = useTranslation("view-screen");
const { viewScreen } = useSelector(stateSelector);
const { expoDesignData, palette } = useExpoDesignData();
+ const { t } = useTranslation("view-screen");
+
+ const { resultTime = GAME_SCREEN_DEFAULT_RESULT_TIME } = viewScreen;
const canvasRef = useRef(null);
const [setContainerRef, containerSize] = useResizeObserver();
@@ -198,27 +202,26 @@ export const GameErase = ({
// - -
- const { bind, TutorialTooltip, escapeTutorial } = useTutorial(
- "gameWipe",
- !isMobileOverlay
- );
+ const { bind, TutorialTooltip } = useTutorial("gameWipe", {
+ shouldOpen: !isMobileOverlay,
+ closeOnEsc: true,
+ });
- const onKeydownAction = useCallback(
- (event) => {
- if (event.key === "Escape") {
- escapeTutorial();
- }
- },
- [escapeTutorial]
- );
+ // - -
- useEffect(() => {
- document.addEventListener("keydown", onKeydownAction);
- return () => document.removeEventListener("keydown", onKeydownAction);
+ useGameAutoNavigationOnResultTimeElapsed({
+ gameResultTime: resultTime * 1000,
+ isGameFinished: isGameFinished,
});
return (
-
+
,
infoPanelRef.current
)}
diff --git a/web/src/containers/views/games/game-find/game-find.tsx b/web/src/containers/views/games/game-find/game-find.tsx
index 300f5f4a..7ca66dd8 100644
--- a/web/src/containers/views/games/game-find/game-find.tsx
+++ b/web/src/containers/views/games/game-find/game-find.tsx
@@ -1,140 +1,220 @@
import ReactDOM from "react-dom";
-import { useTranslation } from "react-i18next";
-import { MouseEvent, useCallback, useEffect, useState } from "react";
-import { animated, useTransition } from "react-spring";
-import { ScreenProps } from "models";
-import cx from "classnames";
+import { useState, useMemo, useCallback, MouseEvent } from "react";
+import { useTransition, animated } from "react-spring";
import { useSelector } from "react-redux";
import { createSelector } from "reselect";
+import { useTranslation } from "react-i18next";
-import { AppState } from "store/store";
-import { GameFindScreen } from "models";
+import { useTutorial } from "context/tutorial-provider/use-tutorial";
+import { useGameAutoNavigationOnResultTimeElapsed } from "../useGameAutoNavigationOnResultTimeElapsed";
+import { useCornerInfoBox } from "hooks/spring-hooks/use-corner-info-box";
-import pinIcon from "assets/img/pin.png";
-import classes from "./game-find.module.scss";
+// Components
import { GameInfoPanel } from "../GameInfoPanel";
import { GameActionsPanel } from "../GameActionsPanel";
-import { useTutorial } from "context/tutorial-provider/use-tutorial";
+import { BasicTooltip } from "components/tooltip/BasicTooltip";
+
+// Models
+import { ScreenProps, Position } from "models";
+import { GameFindScreen } from "models";
+import { AppState } from "store/store";
+
+// Utils
+import cx from "classnames";
+import classes from "./game-find.module.scss";
+import {
+ GAME_FIND_DEFAULT_NUMBER_OF_PINS,
+ GAME_SCREEN_DEFAULT_RESULT_TIME,
+} from "constants/screen";
+
+// Assets
+import pinIcon from "assets/img/pin.png";
+
+// - - - -
const stateSelector = createSelector(
({ expo }: AppState) => expo.viewScreen as GameFindScreen,
(viewScreen) => ({ viewScreen })
);
+// - - - -
+
export const GameFind = ({
screenPreloadedFiles,
infoPanelRef,
actionsPanelRef,
isMobileOverlay,
}: ScreenProps) => {
- const { viewScreen } = useSelector(stateSelector);
- const [finished, setFinished] = useState(false);
- const [pin, setPin] = useState<{ x: number; y: number }>();
const { t } = useTranslation("view-screen");
+ const { viewScreen } = useSelector(stateSelector);
+
+ const {
+ resultTime = GAME_SCREEN_DEFAULT_RESULT_TIME,
+ showTip = false,
+ numberOfPins = GAME_FIND_DEFAULT_NUMBER_OF_PINS,
+ pinsTexts,
+ } = viewScreen;
+
+ // NOTE: pinsTexts - can store more than numberOfPins texts
+ const slicedPinsTexts = useMemo(
+ () => pinsTexts?.slice(0, numberOfPins),
+ [numberOfPins, pinsTexts]
+ );
+
+ const { image1: assignmentImgSrc, image2: resultingImgSrc } =
+ screenPreloadedFiles;
+
+ // - - - -
- const onFinish = useCallback(() => {
- setFinished(true);
+ const [isGameFinished, setIsGameFinished] = useState(false);
+
+ const [currentPinIndex, setCurrentPinIndex] = useState
(0);
+
+ const [pinPositions, setPinPositions] = useState<(Position | undefined)[]>(
+ new Array(numberOfPins).fill(undefined)
+ );
+
+ const areAllPinPositionsFilled = useMemo(
+ () => pinPositions.every((pos) => pos !== undefined),
+ [pinPositions]
+ );
+
+ const onGameFinish = useCallback(() => {
+ setIsGameFinished(true);
}, []);
- const onReset = useCallback(() => {
- setFinished(false);
- setPin(undefined);
+ const onGameReset = useCallback(() => {
+ setPinPositions((prev) => prev.map((_position) => undefined));
+ setCurrentPinIndex(0);
+ setIsGameFinished(false);
}, []);
const pinImage = useCallback(
(e: MouseEvent) => {
- if (pin) {
+ if (areAllPinPositionsFilled || currentPinIndex === pinPositions.length) {
return;
}
- setPin({ x: e.clientX, y: e.clientY });
+ setPinPositions((prevPositions) =>
+ prevPositions.map((pos, posIdx) =>
+ posIdx === currentPinIndex ? { left: e.clientX, top: e.clientY } : pos
+ )
+ );
+
+ setCurrentPinIndex((prev) => prev + 1);
},
- [pin]
+ [areAllPinPositionsFilled, currentPinIndex, pinPositions.length]
+ );
+
+ // When game is not finished, display always and when game is finished, it depends on showTip prop
+ const shouldDisplayPin = useMemo(
+ () => !isGameFinished || (isGameFinished && showTip),
+ [isGameFinished, showTip]
);
- const imageTransition = useTransition(finished, {
+ // - - Tutorial - -
+
+ const { bind, TutorialTooltip } = useTutorial("gameFind", {
+ shouldOpen: !isMobileOverlay,
+ closeOnEsc: true,
+ });
+
+ // - - - -
+
+ useGameAutoNavigationOnResultTimeElapsed({
+ gameResultTime: resultTime * 1000,
+ isGameFinished: isGameFinished,
+ });
+
+ // - - Transitions - -
+
+ const imageTransition = useTransition(isGameFinished, {
initial: { opacity: 1 },
from: { opacity: 0 },
enter: { opacity: 1 },
leave: { opacity: 0 },
});
- const pinTransition = useTransition(pin, {
+ const pinPositionsTransition = useTransition(pinPositions, {
from: { x: 0 },
enter: { x: 1 },
leave: { x: 1 },
});
- //
-
- const { bind, TutorialTooltip, escapeTutorial } = useTutorial(
- "gameFind",
- !isMobileOverlay
- );
-
- const onKeydownAction = useCallback(
- (event) => {
- if (event.key === "Escape") {
- escapeTutorial();
- }
- },
- [escapeTutorial]
- );
-
- useEffect(() => {
- document.addEventListener("keydown", onKeydownAction);
- return () => document.removeEventListener("keydown", onKeydownAction);
+ // NOTE: return value of this hook is transition as well
+ const CornerPinInfoBox = useCornerInfoBox({
+ items: slicedPinsTexts,
+ currIndex: currentPinIndex,
+ textExtractor: (pinText) => pinText,
+ position: "left",
});
return (
- {imageTransition(({ opacity }, finished) =>
- !finished ? (
+ {imageTransition(({ opacity }, isGameFinished) =>
+ !isGameFinished ? (
) : (
)
)}
- {pinTransition(
- ({ x }, pin) =>
- pin && (
-
+ {pinPositionsTransition(
+ ({ x }, pinPos, _trState, trIndex) =>
+ pinPos &&
+ shouldDisplayPin && (
+ <>
+
+
+
+ >
)
)}
+ {CornerPinInfoBox}
+
{infoPanelRef.current &&
ReactDOM.createPortal(
,
infoPanelRef.current
)}
@@ -143,9 +223,9 @@ export const GameFind = ({
ReactDOM.createPortal(
,
actionsPanelRef.current
)}
diff --git a/web/src/containers/views/games/game-move/game-move.tsx b/web/src/containers/views/games/game-move/game-move.tsx
index a597094d..0dd76f83 100644
--- a/web/src/containers/views/games/game-move/game-move.tsx
+++ b/web/src/containers/views/games/game-move/game-move.tsx
@@ -1,13 +1,14 @@
import ReactDOM from "react-dom";
-import { useCallback, useEffect, useState } from "react";
-
+import { useState, useMemo, useCallback } from "react";
+import { animated, useTransition } from "react-spring";
import { useSelector } from "react-redux";
import { createSelector } from "reselect";
+
import { useTranslation } from "react-i18next";
import { useTutorial } from "context/tutorial-provider/use-tutorial";
-import { animated, useSpring, useTransition } from "react-spring";
-import { useDrag } from "@use-gesture/react";
+import { useGameAutoNavigationOnResultTimeElapsed } from "../useGameAutoNavigationOnResultTimeElapsed";
import useResizeObserver from "hooks/use-resize-observer";
+import { useElementMove } from "../../../../hooks/spring-hooks/use-element-move";
// Components
import { GameInfoPanel } from "../GameInfoPanel";
@@ -17,14 +18,18 @@ import { GameActionsPanel } from "../GameActionsPanel";
import { AppState } from "store/store";
import { ScreenProps, GameMoveScreen } from "models";
-// - - -
+// Utils
+import { calculateObjectInitialPosition, calculateObjectSize } from "./utils";
+import { GAME_SCREEN_DEFAULT_RESULT_TIME } from "constants/screen";
+
+// - - - - - -
const stateSelector = createSelector(
({ expo }: AppState) => expo.viewScreen as GameMoveScreen,
(viewScreen) => ({ viewScreen })
);
-// - - -
+// - - - - - -
export const GameMove = ({
screenPreloadedFiles,
@@ -35,53 +40,58 @@ export const GameMove = ({
const { t } = useTranslation("view-screen");
const { viewScreen } = useSelector(stateSelector);
- const [containerRef, { width: containerWidth, height: containerHeight }] =
- useResizeObserver();
- const [dragRef, { width: dragWidth, height: dragHeight }] =
- useResizeObserver(); // dragTarget as a container with the object img
+ const { resultTime = GAME_SCREEN_DEFAULT_RESULT_TIME } = viewScreen;
- // - -
+ const {
+ image1: assignmentImgSrc,
+ image2: resultingImgSrc,
+ object: objectImgSrc,
+ } = screenPreloadedFiles;
- const [isGameFinished, setIsGameFinished] = useState
(false);
+ // - - Move functionality - -
+
+ const [containerRef, containerSize] = useResizeObserver();
+
+ const [objectDragRef, objectDragSize] = useResizeObserver();
+
+ const { objInitialLeft, objInitialTop } = useMemo(
+ () => calculateObjectInitialPosition(viewScreen, containerSize),
+ [containerSize, viewScreen]
+ );
+
+ const { moveSpring, moveSpringApi, bindMoveDrag } = useElementMove({
+ containerSize: containerSize,
+ dragMovingObjectSize: objectDragSize,
+ initialPosition: { left: objInitialLeft, top: objInitialTop },
+ });
- // Initialize the position for drag object image, it will be reset back to [0, 0] whenever the width or height of the container changes
- const [{ dragLeft, dragTop }, dragApi] = useSpring(
- () => ({
- dragLeft: 0,
- dragTop: 0,
- }),
- [containerWidth, containerHeight]
+ // - - Size calculation of object, based on administration settings - -
+ // once at mount assigned through CSS and then used by `objectDragSize`
+
+ const { objectWidth, objectHeight } = useMemo(
+ () => calculateObjectSize(viewScreen, containerSize),
+ [containerSize, viewScreen]
);
+ // - - Tutorial - -
+
+ const { bind: bindTutorial, TutorialTooltip } = useTutorial("gameMove", {
+ shouldOpen: !isMobileOverlay,
+ closeOnEsc: true,
+ });
+
+ // - - - -
+
+ const [isGameFinished, setIsGameFinished] = useState(false);
+
const onGameFinish = useCallback(() => {
setIsGameFinished(true);
}, []);
const onGameReset = useCallback(() => {
setIsGameFinished(false);
- dragApi.start({ dragLeft: 0, dragTop: 0 });
- }, [dragApi]);
-
- // - -
-
- const bind = useDrag(
- ({ down, offset: [x, y] }) => {
- if (!down) {
- return;
- }
-
- dragApi.start({ dragLeft: x, dragTop: y, immediate: true });
- },
- {
- from: () => [dragLeft.get(), dragTop.get()],
- bounds: {
- left: 0,
- top: 0,
- right: containerWidth - dragWidth,
- bottom: containerHeight - dragHeight,
- },
- }
- );
+ moveSpringApi.start({ left: objInitialLeft, top: objInitialTop });
+ }, [moveSpringApi, objInitialLeft, objInitialTop]);
const transition = useTransition(isGameFinished, {
initial: { opacity: 1 },
@@ -90,64 +100,49 @@ export const GameMove = ({
leave: { opacity: 0 },
});
- // - -
-
- const {
- bind: bindTutorial,
- TutorialTooltip,
- escapeTutorial,
- } = useTutorial("gameMove", !isMobileOverlay);
-
- const onKeydownAction = useCallback(
- (event) => {
- if (event.key === "Escape") {
- escapeTutorial();
- }
- },
- [escapeTutorial]
- );
+ // - - - -
- useEffect(() => {
- document.addEventListener("keydown", onKeydownAction);
- return () => document.removeEventListener("keydown", onKeydownAction);
+ useGameAutoNavigationOnResultTimeElapsed({
+ gameResultTime: resultTime * 1000,
+ isGameFinished: isGameFinished,
});
return (
{transition(({ opacity }, isGameFinished) =>
isGameFinished ? (
- // Image2 is result image
) : (
<>
- {/* Image1 is background image (zadanie) */}
- {/* Object */}
@@ -161,8 +156,8 @@ export const GameMove = ({
,
infoPanelRef.current
)}
diff --git a/web/src/containers/views/games/game-move/utils.ts b/web/src/containers/views/games/game-move/utils.ts
new file mode 100644
index 00000000..52cca2ae
--- /dev/null
+++ b/web/src/containers/views/games/game-move/utils.ts
@@ -0,0 +1,76 @@
+import { GameMoveScreen, Size } from "models";
+import { calculateObjectFit } from "utils/object-fit";
+
+export const calculateObjectInitialPosition = (
+ viewScreen: GameMoveScreen,
+ containerSize: Size
+) => {
+ const assignmentImgOrigData = viewScreen.image1OrigData ?? {
+ width: 0,
+ height: 0,
+ };
+
+ // Object position from administration against the contained image there
+ const objectPosition = viewScreen.objectPositionProps
+ ?.containedImgPosition ?? {
+ left: 0,
+ top: 0,
+ };
+
+ const {
+ width: assignmentImgWidth,
+ height: assignmentImgHeight,
+ left: assignmentImgLeftEdge,
+ top: assignmentImgTopEdge,
+ } = calculateObjectFit({
+ type: "contain",
+ parent: containerSize,
+ child: assignmentImgOrigData,
+ });
+
+ // E.g. wFraction = 0.25 means that the object's left-top corner is located 25% left against contained img there
+ const wFraction = objectPosition.left / assignmentImgOrigData.width;
+ const hFraction = objectPosition.top / assignmentImgOrigData.height;
+
+ const objInitialLeft = assignmentImgLeftEdge + wFraction * assignmentImgWidth;
+ const objInitialTop = assignmentImgTopEdge + hFraction * assignmentImgHeight;
+
+ return { objInitialLeft, objInitialTop };
+};
+
+export const calculateObjectSize = (
+ viewScreen: GameMoveScreen,
+ containerSize: Size
+) => {
+ const assignmentImgOrigData = viewScreen.image1OrigData ?? {
+ width: 0,
+ height: 0,
+ };
+
+ const { width: assignmentImgWidth, height: assignmentImgHeight } =
+ calculateObjectFit({
+ type: "contain",
+ parent: containerSize,
+ child: assignmentImgOrigData,
+ });
+
+ const objectImgOrigData = viewScreen.objectOrigData ?? {
+ width: 0,
+ height: 0,
+ };
+
+ const inContainedImgFractionSize =
+ viewScreen.objectSizeProps?.inContainedImgFractionSize;
+
+ if (inContainedImgFractionSize === undefined) {
+ return {
+ objectWidth: objectImgOrigData.width,
+ objectHeight: objectImgOrigData.height,
+ };
+ }
+
+ const objectWidth = inContainedImgFractionSize.width * assignmentImgWidth;
+ const objectHeight = inContainedImgFractionSize.height * assignmentImgHeight;
+
+ return { objectWidth, objectHeight };
+};
diff --git a/web/src/containers/views/games/game-quiz/ImageTextAnswer.tsx b/web/src/containers/views/games/game-quiz/ImageTextAnswer.tsx
index 08a22487..5ba2bc9c 100644
--- a/web/src/containers/views/games/game-quiz/ImageTextAnswer.tsx
+++ b/web/src/containers/views/games/game-quiz/ImageTextAnswer.tsx
@@ -1,9 +1,9 @@
import { Dispatch, SetStateAction, useMemo, Fragment } from "react";
-import useElementSize from "hooks/element-size-hook";
+import useResizeObserver from "hooks/use-resize-observer";
import { Checkbox, Radio } from "@mui/material";
-import ScreenAnchorInfopoint from "components/infopoint/ScreenAnchorInfopoint";
-import TooltipInfoPoint from "components/infopoint/TooltipInfopoint";
+import AnchorInfopoint from "components/infopoint/components/anchor-infopoint";
+import TooltipInfoPoint from "components/infopoint/components/tooltip-infopoint/TooltipInfopoint";
import {
GameQuizAnswer,
@@ -11,7 +11,7 @@ import {
GameQuizType,
Size,
} from "models";
-import { InfopointStatusObject } from "components/infopoint/parseScreenMaps";
+import { InfopointStatusObject } from "components/infopoint/useTooltipInfopoint";
import cx from "classnames";
import { calculateObjectFit } from "utils/object-fit";
@@ -32,8 +32,8 @@ type ImageTextAnswerProps = {
answersTextDisplayType: GameQuizAnswerDisplayType;
// Infopoint stuff
- infopointOpenStatusMap: Record;
- setInfopointOpenStatusMap: Dispatch<
+ infopointStatusMap: Record;
+ setInfopointStatusMap: Dispatch<
SetStateAction>
>;
};
@@ -48,13 +48,13 @@ const ImageTextAnswer = ({
setMarkedAnswers,
quizType,
answersTextDisplayType,
- infopointOpenStatusMap,
- setInfopointOpenStatusMap,
+ infopointStatusMap,
+ setInfopointStatusMap,
}: ImageTextAnswerProps) => {
const { isSm, isMobileLandscape } = useMediaDevice();
const [imageContainerRef, imageContainerSize] =
- useElementSize();
+ useResizeObserver();
const answerImageOrigData = useMemo(
() => answer.imageOrigData ?? { width: 0, height: 0 },
@@ -159,7 +159,7 @@ const ImageTextAnswer = ({
-
diff --git a/web/src/containers/views/games/game-quiz/game-quiz.tsx b/web/src/containers/views/games/game-quiz/game-quiz.tsx
index 3082fd01..920a9113 100644
--- a/web/src/containers/views/games/game-quiz/game-quiz.tsx
+++ b/web/src/containers/views/games/game-quiz/game-quiz.tsx
@@ -3,7 +3,6 @@ import { useCallback, useState, useMemo, useEffect } from "react";
import { createSelector } from "reselect";
import { useSelector } from "react-redux";
-import { useTranslation } from "react-i18next";
import { useMediaDevice } from "context/media-device-provider/media-device-provider";
import useTooltipInfopoint from "components/infopoint/useTooltipInfopoint";
@@ -23,6 +22,7 @@ import {
import cx from "classnames";
import { useTutorial } from "context/tutorial-provider/use-tutorial";
+import { useTranslation } from "react-i18next";
// - -
@@ -43,8 +43,8 @@ export const GameQuiz = ({
const { isSm, isMobileLandscape } = useMediaDevice();
const {
- infopointOpenStatusMap,
- setInfopointOpenStatusMap,
+ infopointStatusMap,
+ setInfopointStatusMap,
closeInfopoints: closeAllInfopoints,
} = useTooltipInfopoint(viewScreen);
@@ -76,19 +76,18 @@ export const GameQuiz = ({
// - -
- const { bind, TutorialTooltip, escapeTutorial } = useTutorial(
- "gameOptions",
- !isMobileOverlay
- );
+ const { bind, TutorialTooltip } = useTutorial("gameOptions", {
+ shouldOpen: !isMobileOverlay,
+ closeOnEsc: true,
+ });
const onKeydownAction = useCallback(
(event: KeyboardEvent) => {
if (event.key === "Escape") {
closeAllInfopoints(viewScreen)();
- escapeTutorial();
}
},
- [closeAllInfopoints, escapeTutorial, viewScreen]
+ [closeAllInfopoints, viewScreen]
);
useEffect(() => {
@@ -138,8 +137,8 @@ export const GameQuiz = ({
quizType={quizType}
answersTextDisplayType={answersTextDisplayType}
setMarkedAnswers={setMarkedAnswers}
- infopointOpenStatusMap={infopointOpenStatusMap}
- setInfopointOpenStatusMap={setInfopointOpenStatusMap}
+ infopointStatusMap={infopointStatusMap}
+ setInfopointStatusMap={setInfopointStatusMap}
/>
);
})}
@@ -150,8 +149,8 @@ export const GameQuiz = ({
,
infoPanelRef.current
)}
diff --git a/web/src/containers/views/games/game-sizing/game-sizing.tsx b/web/src/containers/views/games/game-sizing/game-sizing.tsx
index 091fdb1f..dcb2c00e 100644
--- a/web/src/containers/views/games/game-sizing/game-sizing.tsx
+++ b/web/src/containers/views/games/game-sizing/game-sizing.tsx
@@ -1,141 +1,148 @@
import ReactDOM from "react-dom";
-import { useCallback, useEffect, useMemo, useState } from "react";
-import { animated, useSpring, useTransition } from "react-spring";
-import { createSelector } from "reselect";
-import { useDrag } from "@use-gesture/react";
+import { useState, useCallback } from "react";
+import { animated, useTransition } from "react-spring";
import { useSelector } from "react-redux";
+import { createSelector } from "reselect";
-import { ScreenProps } from "models";
-import { GameSizingScreen } from "models";
-import { AppState } from "store/store";
import { useTranslation } from "react-i18next";
-import useElementSize from "hooks/element-size-hook";
+import { useTutorial } from "context/tutorial-provider/use-tutorial";
+import { useGameAutoNavigationOnResultTimeElapsed } from "../useGameAutoNavigationOnResultTimeElapsed";
+import useResizeObserver from "hooks/use-resize-observer";
+import { useElementResize } from "../../../../hooks/spring-hooks/use-element-resize";
-import expand from "../../../../assets/img/expand.png";
+// Components
import { GameInfoPanel } from "../GameInfoPanel";
import { GameActionsPanel } from "../GameActionsPanel";
-import { useTutorial } from "context/tutorial-provider/use-tutorial";
+
+// Models
+import { ScreenProps } from "models";
+import { GameSizingScreen } from "models";
+import { AppState } from "store/store";
+
+// Assets
+import expandImg from "../../../../assets/img/expand.png";
+import { GAME_SCREEN_DEFAULT_RESULT_TIME } from "constants/screen";
+
+// - - - - - -
const stateSelector = createSelector(
({ expo }: AppState) => expo.viewScreen as GameSizingScreen,
(viewScreen) => ({ viewScreen })
);
+// - - - - - -
+
export const GameSizing = ({
screenPreloadedFiles,
infoPanelRef,
actionsPanelRef,
isMobileOverlay,
}: ScreenProps) => {
- const { viewScreen } = useSelector(stateSelector);
- const [finished, setFinished] = useState(false);
- const [ref, containerSize] = useElementSize();
const { t } = useTranslation("view-screen");
+ const { viewScreen } = useSelector(stateSelector);
- const onFinish = useCallback(() => {
- setFinished(true);
- }, []);
+ const { resultTime = GAME_SCREEN_DEFAULT_RESULT_TIME } = viewScreen;
- const onReset = useCallback(() => {
- setFinished(false);
- }, []);
+ const {
+ image1: referenceImgSrc,
+ image2: comparisonImgSrc,
+ image3: resultingImgSrc,
+ } = screenPreloadedFiles;
- const { height: originalHeight = 0, width: originalWidth = 0 } =
- viewScreen.image2OrigData ?? {};
+ // - - Resizing functionality - -
- const [{ width, height }, api] = useSpring(() => ({
- width: originalWidth,
- height: originalHeight,
- }));
+ const {
+ image1OrigData: referenceImgOrigData,
+ image2OrigData: comparisonImgOrigData,
+ } = viewScreen;
- const ratio = useMemo(
- () => originalWidth / originalHeight,
- [originalHeight, originalWidth]
- );
+ const [rightContainerRef, rightContainerSize] = useResizeObserver();
+ const [leftContainerRef, leftContainerSize] = useResizeObserver();
- const bind = useDrag(
- ({ down, offset: [x, y], lastOffset: [xp, yp] }) => {
- if (!down) {
- return;
- }
-
- // we double the increments since the container is centered (grows on both sides)
- const width = 2 * x - xp;
- const height = 2 * y - yp;
-
- const widthBased = width > height * ratio;
-
- api.start({
- width: widthBased ? width : height * ratio,
- height: widthBased ? width / ratio : height,
- immediate: true,
- });
- },
- {
- from: () => [width.get(), height.get()],
- bounds: (state) => {
- const [xp = 0, yp = 0] = state?.lastOffset ?? [];
-
- const maxWidth = (containerSize.width - 100 + xp) / 2;
- const maxHeight = (containerSize.height - 100 + yp) / 2;
- const widthBased = containerSize.width < containerSize.height * ratio;
-
- return {
- left: (50 + xp) / 2,
- top: (50 + yp) / 2,
- right: widthBased ? maxWidth : maxHeight * ratio,
- bottom: widthBased ? maxWidth / ratio : maxHeight,
- };
- },
- }
- );
+ const {
+ resizeSpring: comparisonImgResizeSpring,
+ bindResizeDrag: comparisongImgBindResizeDrag,
+ } = useElementResize({
+ containerSize: rightContainerSize,
+ dragResizingImgOrigData: comparisonImgOrigData ?? { width: 0, height: 0 },
+ });
+
+ const {
+ resizeSpring: referenceImgResizeSpring,
+ bindResizeDrag: referenceImgBindResizeDrag,
+ } = useElementResize({
+ containerSize: leftContainerSize,
+ dragResizingImgOrigData: referenceImgOrigData ?? { width: 0, height: 0 },
+ });
+
+ // - - Tutorial - -
- const transition = useTransition(finished, {
+ const { bind: bindTutorial, TutorialTooltip } = useTutorial("gameSizing", {
+ shouldOpen: !isMobileOverlay,
+ closeOnEsc: true,
+ });
+
+ // - - - -
+
+ const [isGameFinished, setIsGameFinished] = useState(false);
+
+ const onGameFinish = useCallback(() => {
+ setIsGameFinished(true);
+ }, []);
+
+ const onGameReset = useCallback(() => {
+ setIsGameFinished(false);
+ }, []);
+
+ const transition = useTransition(isGameFinished, {
initial: { opacity: 1 },
from: { opacity: 0 },
enter: { opacity: 1 },
leave: { opacity: 0 },
});
- // - -
-
- const {
- bind: bindTutorial,
- TutorialTooltip,
- escapeTutorial,
- } = useTutorial("gameSizing", !isMobileOverlay);
-
- const onKeydownAction = useCallback(
- (event) => {
- if (event.key === "Escape") {
- escapeTutorial();
- }
- },
- [escapeTutorial]
- );
+ // - - - -
- useEffect(() => {
- document.addEventListener("keydown", onKeydownAction);
- return () => document.removeEventListener("keydown", onKeydownAction);
+ useGameAutoNavigationOnResultTimeElapsed({
+ gameResultTime: resultTime * 1000,
+ isGameFinished: isGameFinished,
});
return (
-
-
+ {/* Left container */}
+
+
+
+
+
+
+ {/* Right container */}
- {transition(({ opacity }, finished) =>
- finished ? (
+ {transition(({ opacity }, isGameFinished) =>
+ isGameFinished ? (
) : (
@@ -144,15 +151,18 @@ export const GameSizing = ({
style={{ opacity }}
>
)
@@ -163,9 +173,9 @@ export const GameSizing = ({
ReactDOM.createPortal(
,
infoPanelRef.current
)}
@@ -174,9 +184,9 @@ export const GameSizing = ({
ReactDOM.createPortal(
,
actionsPanelRef.current
)}
diff --git a/web/src/containers/views/games/useGameAutoNavigationOnResultTimeElapsed.ts b/web/src/containers/views/games/useGameAutoNavigationOnResultTimeElapsed.ts
new file mode 100644
index 00000000..1829c50c
--- /dev/null
+++ b/web/src/containers/views/games/useGameAutoNavigationOnResultTimeElapsed.ts
@@ -0,0 +1,26 @@
+import { useCallback } from "react";
+import { useCountdown } from "hooks/countdown-hook";
+import { useExpoNavigation } from "hooks/view-hooks/expo-navigation-hook";
+
+type UseGameAutoNavigationOnResultTimeElapsedProps = {
+ gameResultTime: number; // in milliseconds
+ isGameFinished: boolean;
+};
+
+export const useGameAutoNavigationOnResultTimeElapsed = ({
+ gameResultTime,
+ isGameFinished,
+}: UseGameAutoNavigationOnResultTimeElapsedProps) => {
+ const { navigateForward } = useExpoNavigation();
+
+ // NOTE: it is important that the deps array is empty
+ const onCountdownFinish = useCallback(() => {
+ navigateForward();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ useCountdown(gameResultTime, {
+ isPaused: !isGameFinished,
+ onFinish: onCountdownFinish,
+ });
+};
diff --git a/web/src/containers/views/view-finish/rating-panel/SendButton.tsx b/web/src/containers/views/view-finish/rating-panel/SendButton.tsx
index 4b829ac0..ed9dc7ce 100644
--- a/web/src/containers/views/view-finish/rating-panel/SendButton.tsx
+++ b/web/src/containers/views/view-finish/rating-panel/SendButton.tsx
@@ -34,7 +34,7 @@ const SendButton = ({ disabled, onClick, isSubmitting }: SendButtonProps) => {
>
{t("sendActionButtonLabel")}
{!isSubmitting && (
-
+
)}
{isSubmitting && (
{
const { image1, image2 } = screenPreloadedFiles;
// Hook up with reference to screen container div, provide its current width and height
- const [screenContainerRef, screenContainerSize] = useElementSize();
+ const [screenContainerRef, screenContainerSize] = useResizeObserver();
const [imageBeforeEl, setImageBeforeEl] = useState(
null
@@ -94,10 +94,10 @@ export const ViewImageChange = ({ screenPreloadedFiles }: ScreenProps) => {
// - - -
const {
- infopointOpenStatusMap,
- setInfopointOpenStatusMap,
+ infopointStatusMap,
+ setInfopointStatusMap,
closeInfopoints,
- ScreenAnchorInfopoint,
+ AnchorInfopoint,
TooltipInfoPoint,
} = useTooltipInfopoint(viewScreen);
@@ -145,10 +145,11 @@ export const ViewImageChange = ({ screenPreloadedFiles }: ScreenProps) => {
bind: bindTutorial,
TutorialTooltip,
isTutorialOpen,
- } = useTutorial(
- "screenChange",
- animation !== "GRADUAL_TRANSITION" && animation !== "FADE_IN_OUT_TWO_IMAGES"
- );
+ } = useTutorial("screenChange", {
+ shouldOpen:
+ animation !== "GRADUAL_TRANSITION" &&
+ animation !== "FADE_IN_OUT_TWO_IMAGES",
+ });
// - - -
@@ -509,7 +510,7 @@ export const ViewImageChange = ({ screenPreloadedFiles }: ScreenProps) => {
// Render the anchor infopoints
return (
- {
{
return (
- {
{
/* Wrapper is the whole of this screen */
/* Container is
without tooltip */
- const [wrapperRef, parentSize] = useElementSize();
+ const [wrapperRef, parentSize] = useResizeObserver();
const containerRef = useRef
(null);
const [containedImgEl, setContainedImgEl] = useState(
null
@@ -94,10 +94,10 @@ export const ViewImage = ({ screenPreloadedFiles }: ScreenProps) => {
// - - - -
const {
- infopointOpenStatusMap,
- setInfopointOpenStatusMap,
+ infopointStatusMap,
+ setInfopointStatusMap,
closeInfopoints,
- ScreenAnchorInfopoint,
+ AnchorInfopoint,
TooltipInfoPoint,
} = useTooltipInfopoint(viewScreen);
@@ -186,7 +186,7 @@ export const ViewImage = ({ screenPreloadedFiles }: ScreenProps) => {
return (
- {
key={`infopoint-tooltip-${infopointIndex}`}
id={`infopoint-tooltip-${infopointIndex}`}
infopoint={infopoint}
- infopointOpenStatusMap={infopointOpenStatusMap}
- setInfopointOpenStatusMap={setInfopointOpenStatusMap}
+ infopointStatusMap={infopointStatusMap}
+ setInfopointStatusMap={setInfopointStatusMap}
primaryKey={infopointIndex.toString()}
/>
diff --git a/web/src/containers/views/view-parallax/view-parallax.tsx b/web/src/containers/views/view-parallax/view-parallax.tsx
index 2c9fc157..20c01343 100644
--- a/web/src/containers/views/view-parallax/view-parallax.tsx
+++ b/web/src/containers/views/view-parallax/view-parallax.tsx
@@ -3,7 +3,7 @@ import { useSelector } from "react-redux";
import { createSelector } from "reselect";
import { animated, easings, useSpring } from "react-spring";
-import useElementSize from "hooks/element-size-hook";
+import useResizeObserver from "hooks/use-resize-observer";
// Models
import { ScreenProps, ParallaxScreeen } from "models";
@@ -56,7 +56,7 @@ export const ViewParallax = ({ screenPreloadedFiles }: ScreenProps) => {
const [
viewContainerRef,
{ width: viewContainerWidth, height: viewContainerHeight },
- ] = useElementSize();
+ ] = useResizeObserver();
const totalDistance = useMemo(
() =>
diff --git a/web/src/containers/views/view-photogallery/Lightbox.tsx b/web/src/containers/views/view-photogallery/Lightbox.tsx
index a80ab59c..b638bba6 100644
--- a/web/src/containers/views/view-photogallery/Lightbox.tsx
+++ b/web/src/containers/views/view-photogallery/Lightbox.tsx
@@ -5,13 +5,12 @@ import { useDialogRef } from "context/dialog-ref-provider/dialog-ref-provider";
import { DialogRefType } from "context/dialog-ref-provider/dialog-ref-types";
import DialogPortal from "context/dialog-ref-provider/DialogPortal";
-import useElementSize from "hooks/element-size-hook";
+import useResizeObserver from "hooks/use-resize-observer";
import { TransformWrapper, TransformComponent } from "react-zoom-pan-pinch";
import { Button } from "components/button/button";
import { Icon } from "components/icon/icon";
-import { BasicTooltip } from "components/tooltip/tooltip";
import { InformationDialog } from "components/dialogs/information-dialog/information-dialog";
@@ -33,7 +32,7 @@ const LightBox = ({
closeLightBox,
overlayOpacityAnimation,
}: LightBoxProps) => {
- const [imgContainerRef, imgContainerSize] = useElementSize();
+ const [imgContainerRef, imgContainerSize] = useResizeObserver();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [containedImageSize, setContainedImageSize] = useState({
width: 0,
@@ -138,85 +137,98 @@ const Toolbar = ({
style={{ opacity: overlayOpacityAnimation.opacity }}
>
-
-
+
+
-
-
-
+
+
-
-
-
+
+
-
{currPhotoObj.photoDescription && (
-
-
diff --git a/web/src/containers/views/view-screen-overlay/ActionsPanel/ActionsPanel.tsx b/web/src/containers/views/view-screen-overlay/ActionsPanel/ActionsPanel.tsx
index 053f3c51..912f53c1 100644
--- a/web/src/containers/views/view-screen-overlay/ActionsPanel/ActionsPanel.tsx
+++ b/web/src/containers/views/view-screen-overlay/ActionsPanel/ActionsPanel.tsx
@@ -9,7 +9,7 @@ import { GlassMagnifierDialog } from "components/dialogs/glass-magnifier-dialog/
import { SettingsDialog } from "components/dialogs/settings-dialog/settings-dialog";
import { ChaptersDialog } from "components/dialogs/chapters-dialog/chapters-dialog";
-import useElementSize from "hooks/element-size-hook";
+import useResizeObserver from "hooks/use-resize-observer";
import { useSectionScreenParams } from "hooks/view-hooks/section-screen-hook";
// Components
@@ -108,7 +108,7 @@ const ActionsPanel = ({
const sectionScreen = useSectionScreenParams();
const { section, screen } = sectionScreen;
- const [actionsBoxRef, actionsBoxSize] = useElementSize();
+ const [actionsBoxRef, actionsBoxSize] = useResizeObserver();
const {
openNewTopDialog,
diff --git a/web/src/containers/views/view-screen-overlay/ActionsPanel/AudioButton.tsx b/web/src/containers/views/view-screen-overlay/ActionsPanel/AudioButton.tsx
index 2d91a9cb..13c94b1f 100644
--- a/web/src/containers/views/view-screen-overlay/ActionsPanel/AudioButton.tsx
+++ b/web/src/containers/views/view-screen-overlay/ActionsPanel/AudioButton.tsx
@@ -3,7 +3,6 @@ import { useTranslation } from "react-i18next";
import { Button } from "components/button/button";
import { Icon } from "components/icon/icon";
-import { BasicTooltip } from "components/tooltip/tooltip";
import { RefCallback } from "context/tutorial-provider/use-tutorial";
@@ -25,28 +24,24 @@ const AudioButton = ({
const { openNewTopDialog } = useDialogRef();
return (
- <>
-
+ openNewTopDialog(DialogRefType.AudioDialog)}
+ tooltip={{
+ id: "overlay-audio-button-tooltip",
+ content: t("audioButtonTooltip"),
+ }}
>
- openNewTopDialog(DialogRefType.AudioDialog)}
- >
-
-
-
-
-
- >
+
+
+
);
};
diff --git a/web/src/containers/views/view-screen-overlay/ActionsPanel/EditorButton.tsx b/web/src/containers/views/view-screen-overlay/ActionsPanel/EditorButton.tsx
index f20c70da..125bce6d 100644
--- a/web/src/containers/views/view-screen-overlay/ActionsPanel/EditorButton.tsx
+++ b/web/src/containers/views/view-screen-overlay/ActionsPanel/EditorButton.tsx
@@ -2,7 +2,6 @@ import { useTranslation } from "react-i18next";
import { Button } from "components/button/button";
import { Icon } from "components/icon/icon";
-import { BasicTooltip } from "components/tooltip/tooltip";
import { RefCallback } from "context/tutorial-provider/use-tutorial";
@@ -24,25 +23,24 @@ const EditorButton = ({
const { t } = useTranslation("view-screen", { keyPrefix: "overlay" });
return (
- <>
-
+
-
-
-
-
-
-
- >
+
+
+
);
};
diff --git a/web/src/containers/views/view-screen-overlay/ActionsPanel/GlassMagnifierButton/GlassMagnifierButton.tsx b/web/src/containers/views/view-screen-overlay/ActionsPanel/GlassMagnifierButton/GlassMagnifierButton.tsx
index bdf8acab..1935f983 100644
--- a/web/src/containers/views/view-screen-overlay/ActionsPanel/GlassMagnifierButton/GlassMagnifierButton.tsx
+++ b/web/src/containers/views/view-screen-overlay/ActionsPanel/GlassMagnifierButton/GlassMagnifierButton.tsx
@@ -24,35 +24,32 @@ const GlassMagnifierButton = ({
const { isLightMode } = useExpoDesignData();
return (
- <>
-
-
- {
- setIsGlassMagnifierEnabled((prev) => !prev);
- }}
- >
-
-
-
-
-
}
- />
+
+
+ {
+ setIsGlassMagnifierEnabled((prev) => !prev);
+ }}
+ >
+
+
- >
+
+
}
+ />
+
);
};
diff --git a/web/src/containers/views/view-screen-overlay/ActionsPanel/PlayButton.tsx b/web/src/containers/views/view-screen-overlay/ActionsPanel/PlayButton.tsx
index 063e4239..996edfd5 100644
--- a/web/src/containers/views/view-screen-overlay/ActionsPanel/PlayButton.tsx
+++ b/web/src/containers/views/view-screen-overlay/ActionsPanel/PlayButton.tsx
@@ -1,6 +1,5 @@
import { Button } from "components/button/button";
import { Icon } from "components/icon/icon";
-import { BasicTooltip } from "components/tooltip/tooltip";
import { RefCallback } from "context/tutorial-provider/use-tutorial";
@@ -27,27 +26,26 @@ const PlayButton = ({
const { t } = useTranslation("view-screen", { keyPrefix: "overlay" });
return (
- <>
-
+
-
-
-
-
-
-
- >
+
+
+
);
};
diff --git a/web/src/containers/views/view-screen-overlay/ActionsPanel/ReactivateTutorialButton.tsx b/web/src/containers/views/view-screen-overlay/ActionsPanel/ReactivateTutorialButton.tsx
index 74384af0..558b2fb4 100644
--- a/web/src/containers/views/view-screen-overlay/ActionsPanel/ReactivateTutorialButton.tsx
+++ b/web/src/containers/views/view-screen-overlay/ActionsPanel/ReactivateTutorialButton.tsx
@@ -2,7 +2,6 @@ import { useTranslation } from "react-i18next";
import { Button } from "components/button/button";
import { Icon } from "components/icon/icon";
-import { BasicTooltip } from "components/tooltip/tooltip";
import cx from "classnames";
import { RefCallback } from "context/tutorial-provider/use-tutorial";
@@ -23,25 +22,24 @@ const ReactivateTutorialButton = ({
const { t } = useTranslation("view-screen", { keyPrefix: "overlay" });
return (
- <>
-
+
-
-
-
-
-
-
- >
+
+
+
);
};
diff --git a/web/src/containers/views/view-screen-overlay/ActionsPanel/SettingsButton.tsx b/web/src/containers/views/view-screen-overlay/ActionsPanel/SettingsButton.tsx
index 1ee0161a..1c74786d 100644
--- a/web/src/containers/views/view-screen-overlay/ActionsPanel/SettingsButton.tsx
+++ b/web/src/containers/views/view-screen-overlay/ActionsPanel/SettingsButton.tsx
@@ -4,7 +4,6 @@ import { DialogRefType } from "context/dialog-ref-provider/dialog-ref-types";
import { Button } from "components/button/button";
import { Icon } from "components/icon/icon";
-import { BasicTooltip } from "components/tooltip/tooltip";
import { RefCallback } from "context/tutorial-provider/use-tutorial";
@@ -25,28 +24,24 @@ const SettingsButton = ({
const { t } = useTranslation("view-screen", { keyPrefix: "overlay" });
return (
- <>
-
+ openNewTopDialog(DialogRefType.SettingsDialog)}
+ tooltip={{
+ id: "overlay-settings-button-tooltip",
+ content: t("settingsButtonTooltip"),
+ }}
>
- openNewTopDialog(DialogRefType.SettingsDialog)}
- >
-
-
-
-
-
- >
+
+
+
);
};
diff --git a/web/src/containers/views/view-screen-overlay/view-screen-overlay.tsx b/web/src/containers/views/view-screen-overlay/view-screen-overlay.tsx
index 3be51530..25cb18ba 100644
--- a/web/src/containers/views/view-screen-overlay/view-screen-overlay.tsx
+++ b/web/src/containers/views/view-screen-overlay/view-screen-overlay.tsx
@@ -127,10 +127,10 @@ export const ViewScreenOverlay = ({
getTutorialEnhanceClassnameByStepkeys,
reactivateTutorial,
isTutorialCompleted,
- } = useTutorial(
- isMobileOverlay ? "mobile-overlay" : "overlay",
- isOverlayActive === true && (isMobileOverlay ? true : !amIGameScreen)
- );
+ } = useTutorial(isMobileOverlay ? "mobile-overlay" : "overlay", {
+ shouldOpen:
+ isOverlayActive === true && (isMobileOverlay ? true : !amIGameScreen),
+ });
const isAnyTutorialOpen = useIsAnyTutorialOpened();
diff --git a/web/src/containers/views/view-signpost/ReferenceItem.tsx b/web/src/containers/views/view-signpost/ReferenceItem.tsx
index 5100f719..0a6b652d 100644
--- a/web/src/containers/views/view-signpost/ReferenceItem.tsx
+++ b/web/src/containers/views/view-signpost/ReferenceItem.tsx
@@ -73,7 +73,7 @@ const ReferenceItem = ({
@@ -86,7 +86,7 @@ const ReferenceItem = ({
)}
diff --git a/web/src/containers/views/view-slideshow/view-slideshow.tsx b/web/src/containers/views/view-slideshow/view-slideshow.tsx
index 0bda693a..58035945 100644
--- a/web/src/containers/views/view-slideshow/view-slideshow.tsx
+++ b/web/src/containers/views/view-slideshow/view-slideshow.tsx
@@ -4,7 +4,7 @@ import { createSelector } from "reselect";
import { animated, useTransition } from "react-spring";
import { useCountdown } from "hooks/countdown-hook";
-import useElementSize from "hooks/element-size-hook";
+import useResizeObserver from "hooks/use-resize-observer";
import useTooltipInfopoint from "components/infopoint/useTooltipInfopoint";
import { useGlassMagnifier } from "hooks/view-hooks/glass-magnifier-hook/useGlassMagnifier";
@@ -43,7 +43,7 @@ export const ViewSlideshow = ({ screenPreloadedFiles }: ScreenProps) => {
// - - -
// Container here is parent, { width, height } in pixels of the whole available screen container
- const [containerRef, containerSize, containerEl] = useElementSize();
+ const [containerRef, containerSize, containerEl] = useResizeObserver();
const [photoImgEl, setPhotoImgEl] = useState
(null);
const { GlassMagnifier } = useGlassMagnifier(containerEl, photoImgEl);
@@ -82,10 +82,10 @@ export const ViewSlideshow = ({ screenPreloadedFiles }: ScreenProps) => {
// - - - -
const {
- infopointOpenStatusMap,
- setInfopointOpenStatusMap,
+ infopointStatusMap,
+ setInfopointStatusMap,
closeInfopoints,
- ScreenAnchorInfopoint,
+ AnchorInfopoint,
TooltipInfoPoint,
} = useTooltipInfopoint(viewScreen);
@@ -137,11 +137,11 @@ export const ViewSlideshow = ({ screenPreloadedFiles }: ScreenProps) => {
}, [photosTimes, dispatch]);
// One countdown for each photo in slideshow.. after finished, setIndex to next photo
- const { reset } = useCountdown(photosTimesArr[photoIndex], {
- paused: !viewProgress.shouldIncrement,
+ const { resetCountdown } = useCountdown(photosTimesArr[photoIndex], {
+ isPaused: !viewProgress.shouldIncrement,
onFinish: () => {
setPhotoIndex((prev) => (prev + 1) % (viewScreen.images?.length ?? 1));
- reset();
+ resetCountdown();
},
});
@@ -334,7 +334,7 @@ export const ViewSlideshow = ({ screenPreloadedFiles }: ScreenProps) => {
- {
key={`infopoint-tooltip-${photoIndex}-${infopointIndex}`}
id={`infopoint-tooltip-${photoIndex}-${infopointIndex}`}
infopoint={infopoint}
- infopointOpenStatusMap={infopointOpenStatusMap}
- setInfopointOpenStatusMap={setInfopointOpenStatusMap}
+ infopointStatusMap={infopointStatusMap}
+ setInfopointStatusMap={setInfopointStatusMap}
primaryKey={photoIndex.toString()}
secondaryKey={infopointIndex.toString()}
canBeOpen={!isAnimationRunning}
diff --git a/web/src/containers/views/view-start/view-start.tsx b/web/src/containers/views/view-start/view-start.tsx
index 56c320d8..5ff86b62 100644
--- a/web/src/containers/views/view-start/view-start.tsx
+++ b/web/src/containers/views/view-start/view-start.tsx
@@ -8,7 +8,6 @@ import { useDialogRef } from "context/dialog-ref-provider/dialog-ref-provider";
import { DialogRefType } from "context/dialog-ref-provider/dialog-ref-types";
import DialogPortal from "context/dialog-ref-provider/DialogPortal";
-import useElementSize from "hooks/element-size-hook";
import useResizeObserver from "hooks/use-resize-observer";
import { useExpoNavigation } from "hooks/view-hooks/expo-navigation-hook";
import { useViewStartAnimation } from "./view-start-animation-hook";
@@ -74,12 +73,17 @@ export const ViewStart = ({ screenPreloadedFiles }: ScreenProps) => {
const [isInfoPanelOpen, setIsInfoPanelOpen] = useState(false);
const [isDetailPanelOpen, setIsDetailPanelOpen] = useState(false);
- const [screenContainerRef, screenContainerSize] = useElementSize();
+ const [screenContainerRef, screenContainerSize] = useResizeObserver();
const [infoPanelRef, infoPanelSize] = useResizeObserver({
ignoreUpdate: true,
});
- const [isLandscapeRecommendationOpen, setIsLandscapeRecommendationOpen] =
+ const [
+ isLandscapeRecommendationSnackbarOpen,
+ setIsLandscapeRecommendationSnackbarOpen,
+ ] = useState(isMobile ? true : false);
+
+ const [isAudioWarningSnackbarOpen, setIsAudioWarningSnackbarOpen] =
useState(isMobile ? true : false);
// - -
@@ -324,14 +328,31 @@ export const ViewStart = ({ screenPreloadedFiles }: ScreenProps) => {
{/* Mobile snackbar for recommendation to turn the mobile into landscape mode */}
+ setIsLandscapeRecommendationSnackbarOpen(false)}
+ >
+ {t("landscapeModeRecommendationSnackbarText")}
+
+
+
+ {/* Mobile snackbar for warning that after every screen change, the expo needs to be stopped,
+ because without user interaction, the audio tracks can not start playing */}
+
setIsLandscapeRecommendationOpen(false)}
+ onClose={() => setIsAudioWarningSnackbarOpen(false)}
>
- {t("landscapeModeRecommendation")}
+ {t("audioWarningSnackbarText")}
>
diff --git a/web/src/containers/views/view-zoom/view-zoom.tsx b/web/src/containers/views/view-zoom/view-zoom.tsx
index 53f60a4f..b7bc10ed 100644
--- a/web/src/containers/views/view-zoom/view-zoom.tsx
+++ b/web/src/containers/views/view-zoom/view-zoom.tsx
@@ -4,7 +4,7 @@ import { createSelector } from "reselect";
import { animated, useSpring, useTransition, easings } from "react-spring";
import { useExpoDesignData } from "hooks/view-hooks/expo-design-data-hook";
-import useElementSize from "hooks/element-size-hook";
+import useResizeObserver from "hooks/use-resize-observer";
import { useZoomPhase, calculateSequenceParameters } from "./useZoomPhase";
// Models
@@ -58,7 +58,7 @@ export const ViewZoom = ({ screenPreloadedFiles }: ScreenProps) => {
// - -
- const [containerRef, containerSize] = useElementSize();
+ const [containerRef, containerSize] = useResizeObserver();
const { width: containedImgWidth, height: containedImgHeight } = useMemo(
() =>
diff --git a/web/src/context/tutorial-provider/use-tutorial.tsx b/web/src/context/tutorial-provider/use-tutorial.tsx
index 3c6d462c..8c291d0d 100644
--- a/web/src/context/tutorial-provider/use-tutorial.tsx
+++ b/web/src/context/tutorial-provider/use-tutorial.tsx
@@ -17,7 +17,17 @@ export type RefCallback = (ref: HTMLElement | null) => void;
// - - - - - - - -
-export const useTutorial = (tutorialKey: TutorialKey, shouldOpen = true) => {
+type UseTutorialOptions = {
+ shouldOpen?: boolean;
+ closeOnEsc?: boolean;
+};
+
+export const useTutorial = (
+ tutorialKey: TutorialKey,
+ options?: UseTutorialOptions
+) => {
+ const { shouldOpen = true, closeOnEsc = false } = options ?? {};
+
// Whole object will all tutorials and their info + helper functions to manipulate with them
const {
store,
@@ -224,6 +234,23 @@ export const useTutorial = (tutorialKey: TutorialKey, shouldOpen = true) => {
[currStepObj?.stepKey, isTutorialOpen]
);
+ // - -
+ // Register escape tutorial on Esc button press
+
+ const onKeydownAction = useCallback(
+ (event: KeyboardEvent) => {
+ if (event.key === "Escape" && closeOnEsc) {
+ escapeTutorial();
+ }
+ },
+ [closeOnEsc, escapeTutorial]
+ );
+
+ useEffect(() => {
+ document.addEventListener("keydown", onKeydownAction);
+ return () => document.removeEventListener("keydown", onKeydownAction);
+ }, [onKeydownAction]);
+
// - -
// Returned object
diff --git a/web/src/hooks/countdown-hook.ts b/web/src/hooks/countdown-hook.ts
index 5b5bf9db..fc5d0627 100644
--- a/web/src/hooks/countdown-hook.ts
+++ b/web/src/hooks/countdown-hook.ts
@@ -2,38 +2,45 @@ import { useCallback, useEffect, useMemo, useState } from "react";
type Options = {
onFinish?: () => void;
- paused?: boolean;
+ isPaused?: boolean;
tick?: number;
};
+/**
+ * NOTE: onFinish function should be wrapped within useCallback
+ */
export const useCountdown = (time: number, options?: Options) => {
- const { onFinish, paused = false, tick = 250 } = options ?? {};
+ const { onFinish, isPaused = false, tick = 250 } = options ?? {};
- const [elapsed, setElapsed] = useState(0);
+ const [elapsedTime, setElapsedTime] = useState(0);
- const finished = useMemo(() => elapsed >= time, [elapsed, time]);
+ const isFinished = useMemo(() => elapsedTime >= time, [elapsedTime, time]);
- useEffect(() => setElapsed(0), [time]);
+ const resetCountdown = useCallback(() => setElapsedTime(0), []);
+
+ // Restart countdown on input time change
+ useEffect(() => setElapsedTime(0), [time]);
useEffect(() => {
- if (paused) {
+ if (isPaused) {
return;
}
- const interval = setInterval(() => setElapsed((prev) => prev + tick), tick);
+ const interval = setInterval(
+ () => setElapsedTime((prev) => prev + tick),
+ tick
+ );
return () => clearInterval(interval);
- }, [paused, tick]);
-
- const reset = useCallback(() => setElapsed(0), []);
+ }, [isPaused, tick]);
useEffect(() => {
- if (!finished) {
+ if (!isFinished) {
return;
}
onFinish?.();
- }, [finished, onFinish]);
+ }, [isFinished, onFinish]);
- return { reset, elapsed, finished };
+ return { resetCountdown, elapsedTime, isFinished };
};
diff --git a/web/src/hooks/element-size-hook.ts b/web/src/hooks/element-size-hook.ts
deleted file mode 100644
index cc485141..00000000
--- a/web/src/hooks/element-size-hook.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import { useState, useEffect, useCallback } from "react";
-import { Size } from "models";
-
-const useElementSize = (): [
- (node: T | null) => void,
- Size,
- T | null
-] => {
- const [ref, setRef] = useState(null);
- const [size, setSize] = useState({
- width: 0,
- height: 0,
- });
-
- // - -
-
- useEffect(() => {
- handleSize();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [ref?.offsetHeight, ref?.offsetWidth]);
-
- // - -
-
- const handleSize = useCallback(() => {
- setSize({
- width: ref?.offsetWidth || 0,
- height: ref?.offsetHeight || 0,
- });
-
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [ref?.offsetHeight, ref?.offsetWidth]);
-
- useEffect(() => {
- window.addEventListener("resize", handleSize);
-
- return () => window.removeEventListener("resize", handleSize);
- }, [handleSize]);
-
- return [setRef, size, ref];
-};
-
-export default useElementSize;
diff --git a/web/src/hooks/spring-hooks/use-corner-info-box.tsx b/web/src/hooks/spring-hooks/use-corner-info-box.tsx
new file mode 100644
index 00000000..a7db810f
--- /dev/null
+++ b/web/src/hooks/spring-hooks/use-corner-info-box.tsx
@@ -0,0 +1,54 @@
+import { useMemo, CSSProperties } from "react";
+import { useTransition, animated } from "react-spring";
+import cx from "classnames";
+
+type UseCornerInfoBoxProps = {
+ items: T[] | undefined;
+ currIndex: number;
+ textExtractor: (item: T) => string;
+ position: "left" | "right"; // upper-left or upper-right
+ className?: string;
+ style?: CSSProperties;
+};
+
+export const useCornerInfoBox = ({
+ items,
+ currIndex,
+ textExtractor,
+ position,
+ className,
+ style,
+}: UseCornerInfoBoxProps) => {
+ const currItem = useMemo(() => items?.[currIndex], [currIndex, items]);
+
+ const infoBoxStyle = useMemo(() => {
+ if (position === "left") {
+ return { left: 20, top: 20 };
+ }
+
+ return { right: 20, top: 20 };
+ }, [position]);
+
+ const itemsInfoTransition = useTransition(currItem, {
+ from: { opacity: 0, translateX: position === "right" ? 15 : -15 },
+ enter: { opacity: 1, translateX: 0, delay: 250 },
+ leave: { opacity: 0, translateX: position === "right" ? 15 : -15 },
+ });
+
+ const Element = itemsInfoTransition(
+ ({ opacity, translateX }, item) =>
+ item && (
+
+ {textExtractor(item)}
+
+ )
+ );
+
+ return Element;
+};
diff --git a/web/src/hooks/spring-hooks/use-element-move.ts b/web/src/hooks/spring-hooks/use-element-move.ts
new file mode 100644
index 00000000..a30918cd
--- /dev/null
+++ b/web/src/hooks/spring-hooks/use-element-move.ts
@@ -0,0 +1,73 @@
+import { CSSProperties } from "react";
+import { useSpring } from "react-spring";
+import { useDrag } from "@use-gesture/react";
+
+import { Position, Size } from "models";
+
+type UseElementMoveProps = {
+ containerSize: Size;
+ dragMovingObjectSize: Size;
+ initialPosition?: Position;
+ additionalCallback?: (left: number, top: number) => void;
+};
+
+/**
+ * This hook enables an element (e.g. image) to be moved within a defined container boundary.
+ * Moving object should be positioned 'absolute', while its container should be positioned 'relative'.
+ * NOTE: Add `draggable={false}` html property to the element being moved
+ *
+ * @param containerSize size of the container which acts as a boundary when moving our element
+ * @param dragMovingObjectSize size of the object which is being moved
+ * @param initialPosition position of moved object inside container boundary, defaults [0, 0]
+ * @param additionalCallback function which is called when new left and top position is being assigned
+ */
+export const useElementMove = ({
+ containerSize,
+ dragMovingObjectSize,
+ initialPosition,
+ additionalCallback,
+}: UseElementMoveProps) => {
+ // Initial position of image being dragged
+ // It will be also reset back to [0, 0] whenever the container size changes (because of the deps array)
+ const [moveSpring, moveSpringApi] = useSpring(
+ () => ({
+ left: initialPosition?.left ?? 0,
+ top: initialPosition?.top ?? 0,
+ }),
+ [containerSize.width, containerSize.height]
+ );
+
+ const bindMoveDrag = useDrag(
+ ({ down, offset: [x, y] }) => {
+ if (!down) {
+ return;
+ }
+
+ moveSpringApi.start({ left: x, top: y, immediate: true });
+ additionalCallback?.(x, y);
+ },
+ {
+ from: () => [moveSpring.left.get(), moveSpring.top.get()],
+ bounds: {
+ left: 0,
+ top: 0,
+ right: containerSize.width - dragMovingObjectSize.width,
+ bottom: containerSize.height - dragMovingObjectSize.height,
+ },
+ }
+ );
+
+ return { moveSpring, moveSpringApi, bindMoveDrag };
+};
+
+export const moveContainerStyle: CSSProperties = {
+ position: "relative",
+};
+
+// NOTE: also add 'hover:cursor-move'
+export const dragMovingObjectStyle: CSSProperties = {
+ position: "absolute",
+ touchAction: "none",
+ WebkitUserSelect: "none",
+ WebkitTouchCallout: "none",
+};
diff --git a/web/src/hooks/spring-hooks/use-element-resize.ts b/web/src/hooks/spring-hooks/use-element-resize.ts
new file mode 100644
index 00000000..73440146
--- /dev/null
+++ b/web/src/hooks/spring-hooks/use-element-resize.ts
@@ -0,0 +1,108 @@
+import { useMemo, CSSProperties } from "react";
+import { useSpring } from "react-spring";
+import { useDrag } from "@use-gesture/react";
+
+import { ImageOrigData, Size } from "models";
+
+type UseElementResizeProps = {
+ containerSize: Size;
+ dragResizingImgOrigData: ImageOrigData;
+ initialSize?: Size;
+ additionalCallback?: (width: number, height: number) => void;
+};
+
+/**
+ * This hook enables an element (e.g. image) to be resized within a defined container boundary.
+ * Object being resized should be positioned 'absolute', while its container should be positioned 'relative'.
+ * NOTE: Add `draggable={false}` html property to the element being resized
+ *
+ * @param containerSize size of the container which acts as a boundary when resizing our element
+ * @param dragResizingImgOrigData needed for aspect ratio of the element which is being resized
+ * @param initialSize first size of the object which is being resized inside container boundary
+ * @param additionalCallback function which is called when new width and height size is being assigned
+ */
+export const useElementResize = ({
+ containerSize,
+ dragResizingImgOrigData,
+ initialSize,
+ additionalCallback,
+}: UseElementResizeProps) => {
+ const { width: origImgWidth, height: origImgHeight } =
+ dragResizingImgOrigData;
+
+ const origImgRatio = useMemo(
+ () => origImgWidth / origImgHeight,
+ [origImgHeight, origImgWidth]
+ );
+
+ const [resizeSpring, resizeSpringApi] = useSpring(() => ({
+ width: initialSize?.width ?? origImgWidth,
+ height: initialSize?.height ?? origImgHeight,
+ }));
+
+ const bindResizeDrag = useDrag(
+ (state) => {
+ const { down, offset, lastOffset } = state;
+ const [x, y] = offset;
+ const [xp, yp] = lastOffset;
+
+ if (!down) {
+ return;
+ }
+
+ // we double the increments since the container is centered (grows on both sides)
+ const width = 2 * x - xp;
+ const height = 2 * y - yp;
+
+ const widthBased = width > height * origImgRatio;
+
+ const finalWidth = widthBased ? width : height * origImgRatio;
+ const finalHeight = widthBased ? width / origImgRatio : height;
+
+ resizeSpringApi.start({
+ width: finalWidth,
+ height: finalHeight,
+ immediate: true,
+ });
+
+ additionalCallback?.(finalWidth, finalHeight);
+ },
+ {
+ from: () => [resizeSpring.width.get(), resizeSpring.height.get()],
+ bounds: (state) => {
+ const [xp = 0, yp = 0] = state?.lastOffset ?? [];
+
+ const maxWidth = (containerSize.width - 100 + xp) / 2;
+ const maxHeight = (containerSize.height - 100 + yp) / 2;
+
+ const widthBased =
+ containerSize.width < containerSize.height * origImgRatio;
+
+ return {
+ left: (50 + xp) / 2,
+ top: (50 + yp) / 2,
+ right: widthBased ? maxWidth : maxHeight * origImgRatio,
+ bottom: widthBased ? maxWidth / origImgRatio : maxHeight,
+ };
+ },
+ }
+ );
+
+ return {
+ resizeSpring,
+ resizeSpringApi,
+ bindResizeDrag,
+ };
+};
+
+export const resizeContainerStyle: CSSProperties = {
+ position: "relative",
+};
+
+// NOTE: also add 'hover:cursor-se-resize'
+export const dragResizingObjectStyle: CSSProperties = {
+ position: "absolute",
+ touchAction: "none",
+ // WebkitUserSelect: "none",
+ // WebkitTouchCallout: "none",
+};
diff --git a/web/src/hooks/view-hooks/glass-magnifier-hook/GlassMagnifier.tsx b/web/src/hooks/view-hooks/glass-magnifier-hook/GlassMagnifier.tsx
index aa6e6c19..0476b8e9 100644
--- a/web/src/hooks/view-hooks/glass-magnifier-hook/GlassMagnifier.tsx
+++ b/web/src/hooks/view-hooks/glass-magnifier-hook/GlassMagnifier.tsx
@@ -6,10 +6,10 @@ import { Position } from "models";
// Thats why image container should be positioned relatively and this absolutel
type GlassMagnifierProps = {
+ containedImgSrc: string | undefined;
containedImgSize: { width: number; height: number };
cursorPosition: Position;
targetPosition: Position;
- containedImgSrc: string | undefined;
lensContainerStyle?: React.CSSProperties;
lensStyle?: React.CSSProperties;
};
diff --git a/web/src/hooks/view-hooks/glass-magnifier-hook/calculate-positions.ts b/web/src/hooks/view-hooks/glass-magnifier-hook/calculate-positions.ts
index 99483de8..ef9d209c 100644
--- a/web/src/hooks/view-hooks/glass-magnifier-hook/calculate-positions.ts
+++ b/web/src/hooks/view-hooks/glass-magnifier-hook/calculate-positions.ts
@@ -139,7 +139,7 @@ export const calculatePositions = (
return {
containedImgSize,
- newContainerCursorPosition,
- newTargetPosition,
+ cursorPosition: newContainerCursorPosition,
+ targetPosition: newTargetPosition,
};
};
diff --git a/web/src/hooks/view-hooks/glass-magnifier-hook/glass-magnifier-hook.tsx b/web/src/hooks/view-hooks/glass-magnifier-hook/glass-magnifier-hook.tsx
deleted file mode 100644
index 84da7d59..00000000
--- a/web/src/hooks/view-hooks/glass-magnifier-hook/glass-magnifier-hook.tsx
+++ /dev/null
@@ -1,147 +0,0 @@
-import { useState, useMemo, useEffect, useCallback } from "react";
-import { calculatePositions } from "./calculate-positions";
-import { useGlassMagnifierConfig } from "context/glass-magnifier-config-provider/glass-magnifier-config-provider";
-import GlassMagnifier from "./GlassMagnifier";
-
-import { Position } from "models";
-
-export const useGlassMagnifier = (
- imageContainerEl: HTMLDivElement | null,
- containedImageEl: HTMLImageElement | null
-) => {
- const { isGlassMagnifierEnabled, glassMagnifierPxSize } =
- useGlassMagnifierConfig();
-
- // Cursor position (left and top from image container)
- // Cursor is in the middle of the glass magnifier
- const [cursorPosition, setCursorPosition] = useState({
- left: 0,
- top: 0,
- });
-
- // Position (left and top) in the natural dimensions of contained img
- const [targetPosition, setTargetPosition] = useState({
- left: 0,
- top: 0,
- });
-
- const [containedImgSize, setContainedImgSize] = useState({
- width: 0,
- height: 0,
- });
-
- const containedImgSrc = useMemo(
- () => containedImageEl?.src,
- [containedImageEl]
- );
-
- // - -
-
- // ImageContainerOnMouseMoveHandler
- const imgContainerMouseMoveHandler = useCallback(
- (event: globalThis.MouseEvent | globalThis.TouchEvent) => {
- if (!isGlassMagnifierEnabled) {
- return;
- }
- if (!imageContainerEl || !containedImageEl || !containedImgSrc) {
- return;
- }
-
- const {
- containedImgSize,
- newContainerCursorPosition,
- newTargetPosition,
- } = calculatePositions(
- imageContainerEl,
- containedImageEl,
- event,
- glassMagnifierPxSize
- );
-
- setContainedImgSize(containedImgSize);
- setCursorPosition(newContainerCursorPosition);
- setTargetPosition(newTargetPosition);
-
- return;
- },
- [
- containedImageEl,
- containedImgSrc,
- glassMagnifierPxSize,
- imageContainerEl,
- isGlassMagnifierEnabled,
- ]
- );
-
- // Apply the required styles for the imageContainer
- useEffect(() => {
- if (!imageContainerEl) {
- return;
- }
-
- imageContainerEl.style.position = "relative";
- imageContainerEl.style.overflow = "hidden";
- imageContainerEl.style.cursor = isGlassMagnifierEnabled ? "none" : "auto";
- imageContainerEl.style.touchAction = isGlassMagnifierEnabled
- ? "none" // disable default browser touch actions such as scrolling, zooming, ...
- : "auto";
- }, [imageContainerEl, isGlassMagnifierEnabled]);
-
- // Add the onMouseMove and touchStart handler for the imageContainer
- useEffect(() => {
- if (!imageContainerEl) {
- return;
- }
-
- imageContainerEl.addEventListener(
- "mousemove",
- imgContainerMouseMoveHandler
- );
-
- imageContainerEl.addEventListener(
- "touchmove",
- imgContainerMouseMoveHandler
- );
-
- return () => {
- imageContainerEl.removeEventListener(
- "mousemove",
- imgContainerMouseMoveHandler
- );
-
- imageContainerEl.removeEventListener(
- "touchmove",
- imgContainerMouseMoveHandler
- );
- };
- }, [imageContainerEl, imgContainerMouseMoveHandler]);
-
- // - -
-
- // Glass Magnifier component with current cursor positions
- // Adding this component into viewScreen will render "lens" at the current cursor position
- type CalculatedGlassMagnifierProps = {
- lensContainerStyle?: React.CSSProperties;
- lensStyle?: React.CSSProperties;
- };
-
- const CalculatedGlassMagnifier = ({
- lensContainerStyle,
- lensStyle,
- }: CalculatedGlassMagnifierProps) => {
- return (
-
- );
- };
-
- return {
- GlassMagnifier: CalculatedGlassMagnifier,
- };
-};
diff --git a/web/src/hooks/view-hooks/glass-magnifier-hook/useGlassMagnifier.tsx b/web/src/hooks/view-hooks/glass-magnifier-hook/useGlassMagnifier.tsx
index 14ef891d..9ad99d17 100644
--- a/web/src/hooks/view-hooks/glass-magnifier-hook/useGlassMagnifier.tsx
+++ b/web/src/hooks/view-hooks/glass-magnifier-hook/useGlassMagnifier.tsx
@@ -1,8 +1,16 @@
import { useState, useMemo, useEffect, useCallback } from "react";
-import { calculatePositions } from "./calculate-positions";
import { useGlassMagnifierConfig } from "context/glass-magnifier-config-provider/glass-magnifier-config-provider";
+
import GlassMagnifier from "./GlassMagnifier";
-import { Position } from "models";
+
+import { Position, Size } from "models";
+import { calculatePositions } from "./calculate-positions";
+
+type PositionsInfoObj = {
+ containedImgSize: Size;
+ cursorPosition: Position;
+ targetPosition: Position;
+};
export const useGlassMagnifier = (
imageContainerEl: HTMLDivElement | null,
@@ -11,56 +19,45 @@ export const useGlassMagnifier = (
const { isGlassMagnifierEnabled, glassMagnifierPxSize } =
useGlassMagnifierConfig();
- // Cursor position (left and top from image container)
- // Cursor is in the middle of the glass magnifier
- const [cursorPosition, setCursorPosition] = useState({
- left: 0,
- top: 0,
- });
-
- // Position (left and top) in the natural dimensions of contained img
- const [targetPosition, setTargetPosition] = useState({
- left: 0,
- top: 0,
- });
-
- const [containedImgSize, setContainedImgSize] = useState({
- width: 0,
- height: 0,
- });
-
const containedImgSrc = useMemo(
() => containedImageEl?.src,
[containedImageEl]
);
- // - -
- // ImageContainerOnMouseMoveHandler
+ const [positionsInfo, setPositionsInfo] = useState({
+ // Size of the contained image
+ containedImgSize: { width: 0, height: 0 },
+
+ // Current position of the cursor relative to the image container (e.g. whole screen)
+ // Cursor is located in the middle of the glass magnifier and is not visible when glass magnifier is enabled
+ cursorPosition: { left: 0, top: 0 },
+
+ // Represents current position of the left-top corner of the lens -> relative to the natural dimensions of contained img
+ targetPosition: { left: 0, top: 0 },
+ });
+
+ // - - - - - - - - - - - -
+ // Event Handlers
+ // - - - - - - - - - - - -
+
const imageContainerOnMouseMoveHandler = useCallback(
(event: globalThis.MouseEvent | globalThis.TouchEvent) => {
if (!isGlassMagnifierEnabled) {
return;
}
+
if (!imageContainerEl || !containedImageEl || !containedImgSrc) {
return;
}
- const {
- containedImgSize,
- newContainerCursorPosition,
- newTargetPosition,
- } = calculatePositions(
+ const newPositionsInfo = calculatePositions(
imageContainerEl,
containedImageEl,
event,
glassMagnifierPxSize
);
- setContainedImgSize(containedImgSize);
- setCursorPosition(newContainerCursorPosition);
- setTargetPosition(newTargetPosition);
-
- return;
+ setPositionsInfo(newPositionsInfo);
},
[
containedImageEl,
@@ -71,31 +68,23 @@ export const useGlassMagnifier = (
]
);
- // Disable default browser behavior, like saving image when long touch and other..
- // const imageContainerElOnTouchStart = useCallback(
- // (e: TouchEvent) => {
- // if (isGlassMagnifierEnabled) {
- // e.preventDefault();
- // }
- // },
- // [isGlassMagnifierEnabled]
- // );
-
- // Apply the required styles for the imageContainer
- useEffect(() => {
- if (!imageContainerEl) {
- return;
- }
+ // Handler responsible for disabling the default browser behavior, e.g. saving image pop-up when long touch
+ /*
+ const imageContainerOnTouchStart = useCallback(
+ (e: TouchEvent) => {
+ if (isGlassMagnifierEnabled) {
+ e.preventDefault();
+ }
+ },
+ [isGlassMagnifierEnabled]
+ );
+ */
- imageContainerEl.style.position = "relative";
- imageContainerEl.style.overflow = "hidden";
- imageContainerEl.style.cursor = isGlassMagnifierEnabled ? "none" : "auto";
- imageContainerEl.style.touchAction = isGlassMagnifierEnabled
- ? "none" // disable default browser touch actions such as scrolling, zooming, ...
- : "auto";
- }, [imageContainerEl, isGlassMagnifierEnabled]);
+ // - - - - - - - - - - - -
+ // Effects
+ // - - - - - - - - - - - -
- // Add the onMouseMove handler for the imageContainer
+ // Effect responsible for registration/unregistration of event handlers for imageContainer element
useEffect(() => {
if (!imageContainerEl) {
return;
@@ -105,37 +94,53 @@ export const useGlassMagnifier = (
imageContainerOnMouseMoveHandler
);
- // imageContainerEl.addEventListener(
- // "touchstart",
- // imageContainerElOnTouchStart
- // );
-
imageContainerEl.addEventListener(
"touchmove",
imageContainerOnMouseMoveHandler
);
+ // imageContainerEl.addEventListener(
+ // "touchstart",
+ // imageContainerOnTouchStart
+ // );
+
return () => {
imageContainerEl.removeEventListener(
"mousemove",
imageContainerOnMouseMoveHandler
);
- // imageContainerEl.removeEventListener(
- // "touchstart",
- // imageContainerElOnTouchStart
- // );
-
imageContainerEl.removeEventListener(
"touchmove",
imageContainerOnMouseMoveHandler
);
+
+ // imageContainerEl.removeEventListener(
+ // "touchstart",
+ // imageContainerOnTouchStart
+ // );
};
}, [imageContainerEl, imageContainerOnMouseMoveHandler]);
- // - -
- // Glass Magnifier component with current cursor positions
- // Adding this component into viewScreen will render "lens" at the current cursor position
+ // Effect responsible for applying all necessary styles for the imageContainer element
+ useEffect(() => {
+ if (!imageContainerEl) {
+ return;
+ }
+
+ imageContainerEl.style.position = "relative";
+ imageContainerEl.style.overflow = "hidden";
+ imageContainerEl.style.cursor = isGlassMagnifierEnabled ? "none" : "auto";
+ imageContainerEl.style.touchAction = isGlassMagnifierEnabled
+ ? "none" // disable default browser touch actions such as scrolling, zooming, ...
+ : "auto";
+ }, [imageContainerEl, isGlassMagnifierEnabled]);
+
+ // - - - - - - - - - - - -
+ // Prepared GlassMagnifier component
+ // - adding this component into viewScreen will render "glass magnifier lens" at the current cursor position
+ // - - - - - - - - - - - -
+
type CalculatedGlassMagnifierProps = {
lensContainerStyle?: React.CSSProperties;
lensStyle?: React.CSSProperties;
@@ -147,10 +152,10 @@ export const useGlassMagnifier = (
}: CalculatedGlassMagnifierProps) => {
return (
diff --git a/web/src/models/infopoint.ts b/web/src/models/infopoint.ts
index fb75efb8..0a073103 100644
--- a/web/src/models/infopoint.ts
+++ b/web/src/models/infopoint.ts
@@ -6,19 +6,19 @@ export type InfopointShape = "SQUARE" | "CIRCLE" | "ICON";
export type Infopoint = {
header?: string; // optional header
- bodyContentType: InfopointBodyType;
+ bodyContentType?: InfopointBodyType;
text?: string; // present only if bodyContentType is TEXT
imageFile?: File; // present only if bodyContentType is IMAGE
videoFile?: File; // present only if bodyContentType is VIDEO
alwaysVisible: boolean;
- isUrlIncluded: boolean;
+ isUrlIncluded?: boolean;
url?: string; // present only if isUrlIncluded is set to true
urlName?: string; // present only if isUrlIncluded is set to true
isScreenIdIncluded: boolean;
screenIdReference?: string; // id of the screen, present only if isScreenIdIncluded is set to true
screenNameReference?: string; // present only if isScreenIdIncluded is set to true
- shape: InfopointShape;
- pxSize: number; // number representing size of infopoint in pixels
+ shape?: InfopointShape;
+ pxSize?: number; // number representing size of infopoint in pixels
color?: string; // present only if shape !== "ICON", hexa string (3B or 4B.. depends if alpha is not 100%)
iconFile?: File; // present only if shape === "ICON"
// Rest
diff --git a/web/src/models/screen.ts b/web/src/models/screen.ts
index 1a1d345b..a2651946 100644
--- a/web/src/models/screen.ts
+++ b/web/src/models/screen.ts
@@ -8,6 +8,7 @@ import { Document } from "./document";
import { Infopoint } from "./infopoint";
import { ScreenPreloadedFiles } from "context/file-preloader/file-preloader-provider";
import { ActiveExpo } from "./exposition";
+import { Position, Size } from "models";
// NEW
import {
@@ -397,12 +398,14 @@ export type GameFindScreen = {
image2?: string;
image1OrigData?: ImageOrigData;
image2OrigData?: ImageOrigData;
- showTip: boolean;
+ showTip?: boolean;
aloneScreen: boolean;
music?: string;
muteChapterMusic: boolean;
screenCompleted: boolean;
resultTime?: number;
+ numberOfPins?: number;
+ pinsTexts?: string[];
};
export type GameDrawScreen = {
@@ -414,12 +417,14 @@ export type GameDrawScreen = {
image2?: string;
image1OrigData?: ImageOrigData;
image2OrigData?: ImageOrigData;
- showDrawing: boolean;
+ showDrawing?: boolean;
aloneScreen: boolean;
music?: string;
muteChapterMusic: boolean;
screenCompleted: boolean;
resultTime?: number;
+ initialColor?: string;
+ initialThickness?: number;
};
export type GameWipeScreen = {
@@ -468,6 +473,14 @@ export type GameMoveScreen = {
image1OrigData?: ImageOrigData;
image2OrigData?: ImageOrigData;
objectOrigData?: ImageOrigData;
+ objectPositionProps?: {
+ containerPosition: Position;
+ containedImgPosition: Position;
+ };
+ objectSizeProps?: {
+ inContainerSize: Size;
+ inContainedImgFractionSize: Size;
+ };
aloneScreen: boolean;
music?: string;
muteChapterMusic: boolean;