From 037adc12a344a28bcfcf027d028a027edda81841 Mon Sep 17 00:00:00 2001 From: nflnkr Date: Thu, 2 May 2024 21:51:05 +0300 Subject: [PATCH 01/26] add side widget button color param --- src/widgets/side/QuizSideButton.tsx | 4 +++- src/widgets/side/SideWidget.tsx | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/widgets/side/QuizSideButton.tsx b/src/widgets/side/QuizSideButton.tsx index ad3ea43..f08bf92 100644 --- a/src/widgets/side/QuizSideButton.tsx +++ b/src/widgets/side/QuizSideButton.tsx @@ -10,9 +10,10 @@ const PADDING = 10; interface Props { quizId: string; position: "left" | "right"; + buttonColor?: string; } -export default function QuizSideButton({ quizId, position }: Props) { +export default function QuizSideButton({ quizId, position, buttonColor }: Props) { const [isQuizShown, setIsQuizShown] = useState(false); return createPortal( @@ -53,6 +54,7 @@ export default function QuizSideButton({ quizId, position }: Props) { position: "fixed", height: "70px", width: `calc(min(calc(100% - ${PADDING * 2}px), 600px))`, + color: buttonColor, }, position === "left" && { bottom: PADDING, diff --git a/src/widgets/side/SideWidget.tsx b/src/widgets/side/SideWidget.tsx index f42f47b..1d8192e 100644 --- a/src/widgets/side/SideWidget.tsx +++ b/src/widgets/side/SideWidget.tsx @@ -7,7 +7,9 @@ export class SideWidget { root: Root | undefined; element = document.createElement("div"); - constructor({ quizId, position }: ComponentPropsWithoutRef) { + constructor({ quizId, position, buttonColor }: ComponentPropsWithoutRef & { + buttonColor?: string; + }) { this.element.style.setProperty("display", "none"); document.body.appendChild(this.element); @@ -17,6 +19,7 @@ export class SideWidget { ); } From d46163a4ce8824d6f5bf081bae9b7fe4ee167326 Mon Sep 17 00:00:00 2001 From: nflnkr Date: Fri, 3 May 2024 21:42:28 +0300 Subject: [PATCH 02/26] add component and path for widget development --- src/WidgetDev.tsx | 64 +++++++++++++++++++++++++++++++++++++++++++++++ src/main.tsx | 27 +++++++++++++++++--- 2 files changed, 87 insertions(+), 4 deletions(-) create mode 100644 src/WidgetDev.tsx diff --git a/src/WidgetDev.tsx b/src/WidgetDev.tsx new file mode 100644 index 0000000..9159934 --- /dev/null +++ b/src/WidgetDev.tsx @@ -0,0 +1,64 @@ +import lightTheme from "@/utils/themes/light"; +import { Box, ThemeProvider, Typography } from "@mui/material"; +import { useEffect, useRef } from "react"; +import { SideWidget } from "./widgets"; + + +const widgetProps: ConstructorParameters[0] = { + quizId: "3c49550d-8c77-4788-bc2d-42586a261514", + position: "right", +}; + +export default function WidgetDev() { + const widgetRef = useRef(null); + + useEffect(() => { + if (!widgetRef.current) { + widgetRef.current = new SideWidget(widgetProps); + } else { + widgetRef.current.render(widgetProps); + } + }); + + return ( + + + + + + + + + + + + + + + + + + + + ); +} + +const lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qu"; + +function Lorem() { + + return ( + + {lorem} + + ); +} diff --git a/src/main.tsx b/src/main.tsx index f018768..81eb653 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,10 +1,10 @@ -// import "https://markknol.github.io/console-log-viewer/console-log-viewer.js"; import { createRoot } from "react-dom/client"; import { RouterProvider, createBrowserRouter } from "react-router-dom"; import App from "./App"; +import { StrictMode, lazy } from "react"; -const router = createBrowserRouter([ +const routes = [ { path: "/", children: [ @@ -18,8 +18,27 @@ const router = createBrowserRouter([ }, ] } -]); +]; + +if (import.meta.env.DEV) { + const WidgetDev = lazy(() => import("./WidgetDev")); + + routes[0].children.push({ + path: "widgetdev", + element: ( + + + + ) + }); +} + +const router = createBrowserRouter(routes); const root = createRoot(document.getElementById("root")!); -root.render(); +root.render( + + + +); From 82b6d3708021b81a95ccd7ea5ce639ecd12bb469 Mon Sep 17 00:00:00 2001 From: nflnkr Date: Fri, 3 May 2024 21:46:14 +0300 Subject: [PATCH 03/26] add side widget class render method --- src/widgets/side/SideWidget.tsx | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/widgets/side/SideWidget.tsx b/src/widgets/side/SideWidget.tsx index 1d8192e..2c7c4d3 100644 --- a/src/widgets/side/SideWidget.tsx +++ b/src/widgets/side/SideWidget.tsx @@ -3,25 +3,23 @@ import QuizSideButton from "./QuizSideButton"; import { ComponentPropsWithoutRef } from "react"; +type Props = ComponentPropsWithoutRef; + export class SideWidget { root: Root | undefined; element = document.createElement("div"); - constructor({ quizId, position, buttonColor }: ComponentPropsWithoutRef & { - buttonColor?: string; - }) { + constructor(props: Props) { this.element.style.setProperty("display", "none"); document.body.appendChild(this.element); this.root = createRoot(this.element); - this.root.render( - - ); + this.render(props); + } + + render(props: Props) { + this.root?.render(); } destroy() { From 79674fedbb6745b1e9a306d0cabfc0411585e0a3 Mon Sep 17 00:00:00 2001 From: nflnkr Date: Fri, 3 May 2024 22:30:38 +0300 Subject: [PATCH 04/26] use QuizDialog in side widget add QuizDialog close button --- src/widgets/QuizDialog.tsx | 61 +++++++++++++++--- src/widgets/side/QuizSideButton.tsx | 99 +++++++++++++---------------- 2 files changed, 96 insertions(+), 64 deletions(-) diff --git a/src/widgets/QuizDialog.tsx b/src/widgets/QuizDialog.tsx index 44cf2eb..e2c5517 100644 --- a/src/widgets/QuizDialog.tsx +++ b/src/widgets/QuizDialog.tsx @@ -1,28 +1,52 @@ import QuizAnswerer from "@/components/QuizAnswerer"; -import { Dialog } from "@mui/material"; +import CloseIcon from '@mui/icons-material/Close'; +import { Dialog, IconButton, Slide, SlideProps, SxProps, Theme } from "@mui/material"; +import { forwardRef } from "react"; +const SlideTransition = forwardRef((props, ref) => { + return ( + + ); +}); + interface Props { open?: boolean; quizId: string; + paperSx?: SxProps; + hideBackdrop?: boolean; + disableScrollLock?: boolean; onClose?: () => void; } -export default function QuizDialog({ open = true, quizId, onClose }: Props) { +export default function QuizDialog({ + open = true, + quizId, + paperSx = [], + hideBackdrop, + disableScrollLock, + onClose +}: Props) { return ( + + + ); } diff --git a/src/widgets/side/QuizSideButton.tsx b/src/widgets/side/QuizSideButton.tsx index f08bf92..c8d7d4c 100644 --- a/src/widgets/side/QuizSideButton.tsx +++ b/src/widgets/side/QuizSideButton.tsx @@ -1,8 +1,8 @@ -import { QuizAnswerer } from "@/index"; import lightTheme from "@/utils/themes/light"; -import { Box, Button, Grow, ThemeProvider } from "@mui/material"; +import { Button, ThemeProvider } from "@mui/material"; import { useState } from "react"; import { createPortal } from "react-dom"; +import QuizDialog from "../QuizDialog"; const PADDING = 10; @@ -10,65 +10,54 @@ const PADDING = 10; interface Props { quizId: string; position: "left" | "right"; - buttonColor?: string; + buttonBackgroundColor?: string; } -export default function QuizSideButton({ quizId, position, buttonColor }: Props) { +export default function QuizSideButton({ quizId, position, buttonBackgroundColor }: Props) { const [isQuizShown, setIsQuizShown] = useState(false); return createPortal( - {isQuizShown ? ( - - - - - - ) : ( - - )} + setIsQuizShown(false)} + hideBackdrop + disableScrollLock + paperSx={{ + position: "absolute", + bottom: PADDING, + right: position === "right" ? PADDING : undefined, + left: position === "left" ? PADDING : undefined, + height: `calc(min(calc(100% - ${PADDING * 2}px), 800px))`, + width: `calc(min(calc(100% - ${PADDING * 2}px), 600px))`, + m: 0, + }} + /> + , document.body ); From ffc8b5e9cb2c4afa6f05cad3203a1997f6c76d16 Mon Sep 17 00:00:00 2001 From: nflnkr Date: Sat, 4 May 2024 15:24:59 +0300 Subject: [PATCH 05/26] minor fix --- src/main.tsx | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/main.tsx b/src/main.tsx index 81eb653..376161d 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,10 +1,10 @@ import { createRoot } from "react-dom/client"; -import { RouterProvider, createBrowserRouter } from "react-router-dom"; +import { RouteObject, RouterProvider, createBrowserRouter } from "react-router-dom"; import App from "./App"; import { StrictMode, lazy } from "react"; -const routes = [ +const routes: RouteObject[] = [ { path: "/", children: [ @@ -23,13 +23,9 @@ const routes = [ if (import.meta.env.DEV) { const WidgetDev = lazy(() => import("./WidgetDev")); - routes[0].children.push({ + routes[0].children?.push({ path: "widgetdev", - element: ( - - - - ) + element: , }); } From b421e6eed327ed212292a8b1146abfa9f05868f3 Mon Sep 17 00:00:00 2001 From: nflnkr Date: Sat, 4 May 2024 15:53:56 +0300 Subject: [PATCH 06/26] quiz is fullscreen when viewport width is small --- src/widgets/side/QuizSideButton.tsx | 32 ++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/src/widgets/side/QuizSideButton.tsx b/src/widgets/side/QuizSideButton.tsx index c8d7d4c..bb39a17 100644 --- a/src/widgets/side/QuizSideButton.tsx +++ b/src/widgets/side/QuizSideButton.tsx @@ -1,5 +1,5 @@ import lightTheme from "@/utils/themes/light"; -import { Button, ThemeProvider } from "@mui/material"; +import { Button, ThemeProvider, useMediaQuery } from "@mui/material"; import { useState } from "react"; import { createPortal } from "react-dom"; import QuizDialog from "../QuizDialog"; @@ -15,6 +15,7 @@ interface Props { export default function QuizSideButton({ quizId, position, buttonBackgroundColor }: Props) { const [isQuizShown, setIsQuizShown] = useState(false); + const isMobile = useMediaQuery("(max-width: 600px)"); return createPortal( @@ -24,15 +25,26 @@ export default function QuizSideButton({ quizId, position, buttonBackgroundColor onClose={() => setIsQuizShown(false)} hideBackdrop disableScrollLock - paperSx={{ - position: "absolute", - bottom: PADDING, - right: position === "right" ? PADDING : undefined, - left: position === "left" ? PADDING : undefined, - height: `calc(min(calc(100% - ${PADDING * 2}px), 800px))`, - width: `calc(min(calc(100% - ${PADDING * 2}px), 600px))`, - m: 0, - }} + paperSx={[ + { + m: 0, + }, + !isMobile && { + position: "absolute", + bottom: PADDING, + right: position === "right" ? PADDING : undefined, + left: position === "left" ? PADDING : undefined, + height: `calc(min(calc(100% - ${PADDING * 2}px), 800px))`, + width: `calc(min(calc(100% - ${PADDING * 2}px), 600px))`, + }, + isMobile && { + position: "relative", + height: "100%", + maxHeight: "100%", + width: "100%", + borderRadius: 0, + }, + ]} /> , From a18dbf00222037e82dff501925cba06a487460f5 Mon Sep 17 00:00:00 2001 From: nflnkr Date: Sat, 4 May 2024 19:47:17 +0300 Subject: [PATCH 11/26] add apology page background color --- lib/components/ViewPublicationPage/ApologyPage.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/components/ViewPublicationPage/ApologyPage.tsx b/lib/components/ViewPublicationPage/ApologyPage.tsx index 1a983f2..f869827 100644 --- a/lib/components/ViewPublicationPage/ApologyPage.tsx +++ b/lib/components/ViewPublicationPage/ApologyPage.tsx @@ -19,6 +19,7 @@ export const ApologyPage = ({ error }: Props) => { alignItems: "center", justifyContent: "center", height: "100%", + backgroundColor: "#F2F3F7", }} > Date: Sat, 4 May 2024 19:47:47 +0300 Subject: [PATCH 12/26] hide side widget button flash animation if quiz is completed --- src/widgets/shared/RunningStripe.tsx | 1 + src/widgets/shared/useQuizCompletionStatus.ts | 17 +++++++++++++++++ src/widgets/side/QuizSideButton.tsx | 4 +++- 3 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 src/widgets/shared/useQuizCompletionStatus.ts diff --git a/src/widgets/shared/RunningStripe.tsx b/src/widgets/shared/RunningStripe.tsx index b1b31df..7b526c4 100644 --- a/src/widgets/shared/RunningStripe.tsx +++ b/src/widgets/shared/RunningStripe.tsx @@ -9,6 +9,7 @@ export default function RunningStripe({ sx = [] }: Props) { return ( { + const sessions = JSON.parse(localStorage.getItem("sessions") || "{}"); + + if ( + typeof sessions[quizId] === "number" + && Date.now() - sessions[quizId] < 86400000 + ) { + return true; + } + + return false; + }, [quizId]); +} diff --git a/src/widgets/side/QuizSideButton.tsx b/src/widgets/side/QuizSideButton.tsx index e110c9e..7f563ec 100644 --- a/src/widgets/side/QuizSideButton.tsx +++ b/src/widgets/side/QuizSideButton.tsx @@ -4,6 +4,7 @@ import { useState } from "react"; import { createPortal } from "react-dom"; import QuizDialog from "../shared/QuizDialog"; import RunningStripe from "../shared/RunningStripe"; +import { useQuizCompletionStatus } from "../shared/useQuizCompletionStatus"; const PADDING = 10; @@ -21,6 +22,7 @@ interface Props { export default function QuizSideButton({ quizId, position, buttonBackgroundColor, dimensions, fullScreen = false }: Props) { const [isQuizShown, setIsQuizShown] = useState(false); const isMobile = useMediaQuery("(max-width: 600px)"); + const isQuizCompleted = useQuizCompletionStatus(quizId); return createPortal( @@ -78,7 +80,7 @@ export default function QuizSideButton({ quizId, position, buttonBackgroundColor }, ]} > - + {!isQuizCompleted && } Пройти квиз , From 8c1b6d97efe02bd0d2b90c96fdbdb02561d46159 Mon Sep 17 00:00:00 2001 From: nflnkr Date: Sat, 4 May 2024 19:58:01 +0300 Subject: [PATCH 13/26] add side widget button text color param --- src/widgets/side/QuizSideButton.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/widgets/side/QuizSideButton.tsx b/src/widgets/side/QuizSideButton.tsx index 7f563ec..e911c20 100644 --- a/src/widgets/side/QuizSideButton.tsx +++ b/src/widgets/side/QuizSideButton.tsx @@ -15,11 +15,12 @@ interface Props { quizId: string; position: "left" | "right"; buttonBackgroundColor?: string; + buttonTextColor?: string; dimensions?: { width: string; height: string; }; fullScreen?: boolean; } -export default function QuizSideButton({ quizId, position, buttonBackgroundColor, dimensions, fullScreen = false }: Props) { +export default function QuizSideButton({ quizId, position, buttonBackgroundColor, buttonTextColor, dimensions, fullScreen = false }: Props) { const [isQuizShown, setIsQuizShown] = useState(false); const isMobile = useMediaQuery("(max-width: 600px)"); const isQuizCompleted = useQuizCompletionStatus(quizId); @@ -68,6 +69,7 @@ export default function QuizSideButton({ quizId, position, buttonBackgroundColor width: "600px", maxWidth: `calc(100% - ${PADDING * 2}px)`, backgroundColor: buttonBackgroundColor, + color: buttonTextColor, overflow: "hidden", }, position === "left" && { From 9f5ec6653374f83ab0a487c2d3798c6102ab0433 Mon Sep 17 00:00:00 2001 From: nflnkr Date: Tue, 7 May 2024 13:09:43 +0300 Subject: [PATCH 14/26] side widget: disable flash on quiz opening --- src/widgets/side/QuizSideButton.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/widgets/side/QuizSideButton.tsx b/src/widgets/side/QuizSideButton.tsx index e911c20..b825c66 100644 --- a/src/widgets/side/QuizSideButton.tsx +++ b/src/widgets/side/QuizSideButton.tsx @@ -24,6 +24,12 @@ export default function QuizSideButton({ quizId, position, buttonBackgroundColor const [isQuizShown, setIsQuizShown] = useState(false); const isMobile = useMediaQuery("(max-width: 600px)"); const isQuizCompleted = useQuizCompletionStatus(quizId); + const [isFlashEnabled, setIsFlashEnabled] = useState(true); + + function openQuiz() { + setIsQuizShown(true); + setIsFlashEnabled(false); + } return createPortal( @@ -37,7 +43,7 @@ export default function QuizSideButton({ quizId, position, buttonBackgroundColor { m: 0, }, - (!isMobile && !fullScreen) && { + !(isMobile || fullScreen) && { position: "absolute", bottom: PADDING, right: position === "right" ? PADDING : undefined, @@ -59,7 +65,7 @@ export default function QuizSideButton({ quizId, position, buttonBackgroundColor , From 0ffa3585857090751a4995b0918ee2566281864e Mon Sep 17 00:00:00 2001 From: nflnkr Date: Tue, 7 May 2024 13:12:42 +0300 Subject: [PATCH 15/26] side widget: add enable button flash param --- src/widgets/side/QuizSideButton.tsx | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/widgets/side/QuizSideButton.tsx b/src/widgets/side/QuizSideButton.tsx index b825c66..6ea447c 100644 --- a/src/widgets/side/QuizSideButton.tsx +++ b/src/widgets/side/QuizSideButton.tsx @@ -18,13 +18,22 @@ interface Props { buttonTextColor?: string; dimensions?: { width: string; height: string; }; fullScreen?: boolean; + buttonFlash?: boolean; } -export default function QuizSideButton({ quizId, position, buttonBackgroundColor, buttonTextColor, dimensions, fullScreen = false }: Props) { +export default function QuizSideButton({ + quizId, + position, + buttonBackgroundColor, + buttonTextColor, + dimensions, + fullScreen = false, + buttonFlash = false, +}: Props) { const [isQuizShown, setIsQuizShown] = useState(false); const isMobile = useMediaQuery("(max-width: 600px)"); const isQuizCompleted = useQuizCompletionStatus(quizId); - const [isFlashEnabled, setIsFlashEnabled] = useState(true); + const [isFlashEnabled, setIsFlashEnabled] = useState(buttonFlash); function openQuiz() { setIsQuizShown(true); From 41e02c82ef598999fac52f929f71bc4c262b2e60 Mon Sep 17 00:00:00 2001 From: nflnkr Date: Wed, 8 May 2024 14:23:15 +0300 Subject: [PATCH 16/26] side widget: add auto open time and hide on mobile params --- src/widgets/shared/useAutoOpenTimer.ts | 18 +++++++ src/widgets/side/QuizSideButton.tsx | 70 +++++++++++++++----------- 2 files changed, 58 insertions(+), 30 deletions(-) create mode 100644 src/widgets/shared/useAutoOpenTimer.ts diff --git a/src/widgets/shared/useAutoOpenTimer.ts b/src/widgets/shared/useAutoOpenTimer.ts new file mode 100644 index 0000000..1eb839e --- /dev/null +++ b/src/widgets/shared/useAutoOpenTimer.ts @@ -0,0 +1,18 @@ +import { useEffect, useState } from "react"; + + +export function useAutoOpenTimer(autoOpenTime: number) { + const [isWidgetHidden, setIsWidgetHidden] = useState(autoOpenTime ? true : false); + + useEffect(function setAutoOpenTimer() { + if (!autoOpenTime) return; + + const timeout = setTimeout(() => setIsWidgetHidden(false), autoOpenTime * 1000); + + return () => { + clearTimeout(timeout); + }; + }, [autoOpenTime]); + + return isWidgetHidden; +} diff --git a/src/widgets/side/QuizSideButton.tsx b/src/widgets/side/QuizSideButton.tsx index 6ea447c..8aa1aea 100644 --- a/src/widgets/side/QuizSideButton.tsx +++ b/src/widgets/side/QuizSideButton.tsx @@ -1,9 +1,10 @@ import lightTheme from "@/utils/themes/light"; -import { Button, ThemeProvider, useMediaQuery } from "@mui/material"; +import { Button, Fade, ThemeProvider, useMediaQuery } from "@mui/material"; import { useState } from "react"; import { createPortal } from "react-dom"; import QuizDialog from "../shared/QuizDialog"; import RunningStripe from "../shared/RunningStripe"; +import { useAutoOpenTimer } from "../shared/useAutoOpenTimer"; import { useQuizCompletionStatus } from "../shared/useQuizCompletionStatus"; @@ -19,6 +20,8 @@ interface Props { dimensions?: { width: string; height: string; }; fullScreen?: boolean; buttonFlash?: boolean; + autoOpenTime?: number; + hideOnMobile?: boolean; } export default function QuizSideButton({ @@ -29,17 +32,22 @@ export default function QuizSideButton({ dimensions, fullScreen = false, buttonFlash = false, + autoOpenTime = 0, + hideOnMobile = false, }: Props) { const [isQuizShown, setIsQuizShown] = useState(false); const isMobile = useMediaQuery("(max-width: 600px)"); const isQuizCompleted = useQuizCompletionStatus(quizId); const [isFlashEnabled, setIsFlashEnabled] = useState(buttonFlash); + const isWidgetHidden = useAutoOpenTimer(autoOpenTime); function openQuiz() { setIsQuizShown(true); setIsFlashEnabled(false); } + if (hideOnMobile && isMobile) return null; + return createPortal( - + + + , document.body ); From 7749b00e56026930925f3cf44b6026606892cfa9 Mon Sep 17 00:00:00 2001 From: nflnkr Date: Wed, 8 May 2024 15:12:40 +0300 Subject: [PATCH 17/26] side widget: add quiz auto open timer --- src/widgets/side/QuizSideButton.tsx | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/widgets/side/QuizSideButton.tsx b/src/widgets/side/QuizSideButton.tsx index 8aa1aea..3e14cee 100644 --- a/src/widgets/side/QuizSideButton.tsx +++ b/src/widgets/side/QuizSideButton.tsx @@ -1,6 +1,6 @@ import lightTheme from "@/utils/themes/light"; import { Button, Fade, ThemeProvider, useMediaQuery } from "@mui/material"; -import { useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { createPortal } from "react-dom"; import QuizDialog from "../shared/QuizDialog"; import RunningStripe from "../shared/RunningStripe"; @@ -20,7 +20,14 @@ interface Props { dimensions?: { width: string; height: string; }; fullScreen?: boolean; buttonFlash?: boolean; + /** + * Скрывать виджет первые X секунд + */ autoOpenTime?: number; + /** + * Открыть квиз через X секунд + */ + autoShowQuizTime?: number; hideOnMobile?: boolean; } @@ -33,6 +40,7 @@ export default function QuizSideButton({ fullScreen = false, buttonFlash = false, autoOpenTime = 0, + autoShowQuizTime = 0, hideOnMobile = false, }: Props) { const [isQuizShown, setIsQuizShown] = useState(false); @@ -40,8 +48,22 @@ export default function QuizSideButton({ const isQuizCompleted = useQuizCompletionStatus(quizId); const [isFlashEnabled, setIsFlashEnabled] = useState(buttonFlash); const isWidgetHidden = useAutoOpenTimer(autoOpenTime); + const preventQuizAutoShowRef = useRef(false); + + useEffect(function setAutoShowQuizTimer() { + if (!autoShowQuizTime) return; + + const timeout = setTimeout(() => { + if (!preventQuizAutoShowRef.current) setIsQuizShown(true); + }, autoShowQuizTime * 1000); + + return () => { + clearTimeout(timeout); + }; + }, [autoShowQuizTime]); function openQuiz() { + preventQuizAutoShowRef.current = true; setIsQuizShown(true); setIsFlashEnabled(false); } From cd972e493b060f58ac73a1ac353e38f25f624423 Mon Sep 17 00:00:00 2001 From: nflnkr Date: Wed, 8 May 2024 20:50:18 +0300 Subject: [PATCH 18/26] add popup quiz features --- src/WidgetDev.tsx | 9 ++-- src/widgets/popup/PopupWidget.tsx | 23 +++++---- src/widgets/popup/QuizPopup.tsx | 78 +++++++++++++++++++++++++++++++ src/widgets/shared/QuizDialog.tsx | 3 +- 4 files changed, 95 insertions(+), 18 deletions(-) create mode 100644 src/widgets/popup/QuizPopup.tsx diff --git a/src/WidgetDev.tsx b/src/WidgetDev.tsx index 9159934..901713c 100644 --- a/src/WidgetDev.tsx +++ b/src/WidgetDev.tsx @@ -1,20 +1,19 @@ import lightTheme from "@/utils/themes/light"; import { Box, ThemeProvider, Typography } from "@mui/material"; import { useEffect, useRef } from "react"; -import { SideWidget } from "./widgets"; +import { PopupWidget as Widget } from "./widgets"; -const widgetProps: ConstructorParameters[0] = { +const widgetProps: ConstructorParameters[0] = { quizId: "3c49550d-8c77-4788-bc2d-42586a261514", - position: "right", }; export default function WidgetDev() { - const widgetRef = useRef(null); + const widgetRef = useRef(null); useEffect(() => { if (!widgetRef.current) { - widgetRef.current = new SideWidget(widgetProps); + widgetRef.current = new Widget(widgetProps); } else { widgetRef.current.render(widgetProps); } diff --git a/src/widgets/popup/PopupWidget.tsx b/src/widgets/popup/PopupWidget.tsx index eb490bd..d238eaa 100644 --- a/src/widgets/popup/PopupWidget.tsx +++ b/src/widgets/popup/PopupWidget.tsx @@ -1,26 +1,25 @@ import { Root, createRoot } from "react-dom/client"; -import QuizDialog from "../shared/QuizDialog"; +import { ComponentPropsWithoutRef } from "react"; +import QuizPopup from "./QuizPopup"; +type Props = ComponentPropsWithoutRef; + export class PopupWidget { root: Root | undefined; - element: HTMLDivElement; + element = document.createElement("div"); - constructor({ quizId }: { - quizId: string; - }) { - this.element = document.createElement("div"); + constructor(props: Props) { this.element.style.setProperty("display", "none"); document.body.appendChild(this.element); this.root = createRoot(this.element); - this.root.render( - this.destroy()} - /> - ); + this.render(props); + } + + render(props: Props) { + this.root?.render(); } destroy() { diff --git a/src/widgets/popup/QuizPopup.tsx b/src/widgets/popup/QuizPopup.tsx new file mode 100644 index 0000000..f824328 --- /dev/null +++ b/src/widgets/popup/QuizPopup.tsx @@ -0,0 +1,78 @@ +import { useEffect, useRef, useState } from "react"; +import QuizDialog from "../shared/QuizDialog"; +import { useQuizCompletionStatus } from "../shared/useQuizCompletionStatus"; +import { useMediaQuery } from "@mui/material"; + + +const WIDGET_DEFAULT_WIDTH = "600px"; +const WIDGET_DEFAULT_HEIGHT = "80%"; + +interface Props { + quizId: string; + dimensions?: { width: string; height: string; }; + /** + * Открыть квиз через X секунд + */ + autoShowQuizTime?: number; + hideOnMobile?: boolean; + openOnLeaveAttempt?: boolean; +} + +export default function QuizPopup({ + quizId, + dimensions, + autoShowQuizTime = 0, + hideOnMobile = false, + openOnLeaveAttempt = false, +}: Props) { + const initialIsQuizShown = (autoShowQuizTime || openOnLeaveAttempt) ? false : true; + + const [isQuizShown, setIsQuizShown] = useState(initialIsQuizShown); + const isQuizCompleted = useQuizCompletionStatus(quizId); + const isMobile = useMediaQuery("(max-width: 600px)"); + const preventOpenOnLeaveAttemptRef = useRef(false); + + useEffect(function setAutoShowQuizTimer() { + if (!autoShowQuizTime || openOnLeaveAttempt) return; + + const timeout = setTimeout(() => { + setIsQuizShown(true); + }, autoShowQuizTime * 1000); + + return () => { + clearTimeout(timeout); + }; + }, [autoShowQuizTime, openOnLeaveAttempt]); + + useEffect(function attachLeaveListener() { + if (!openOnLeaveAttempt) return; + + const handleMouseLeave = () => { + if (!preventOpenOnLeaveAttemptRef.current) { + preventOpenOnLeaveAttemptRef.current = true; + setIsQuizShown(true); + } + }; + + document.addEventListener("mouseleave", handleMouseLeave); + + return () => { + document.removeEventListener("mouseleave", handleMouseLeave); + }; + }, [openOnLeaveAttempt]); + + if (isQuizCompleted) return null; + if (hideOnMobile && isMobile) return null; + + return ( + setIsQuizShown(false)} + paperSx={{ + width: dimensions?.width ?? WIDGET_DEFAULT_WIDTH, + height: dimensions?.height ?? WIDGET_DEFAULT_HEIGHT, + }} + /> + ); +} diff --git a/src/widgets/shared/QuizDialog.tsx b/src/widgets/shared/QuizDialog.tsx index 4429a3a..fa3895c 100644 --- a/src/widgets/shared/QuizDialog.tsx +++ b/src/widgets/shared/QuizDialog.tsx @@ -41,8 +41,9 @@ export default function QuizDialog({ { backgroundColor: "transparent", width: "calc(min(100%, max(70%, 700px)))", - height: "80%", maxWidth: "100%", + height: "80%", + maxHeight: "100%", m: "16px", }, ...(Array.isArray(paperSx) ? paperSx : [paperSx]) From 92d727b2e904f0cda7a4512f7c7a53815a8ed2a9 Mon Sep 17 00:00:00 2001 From: nflnkr Date: Thu, 9 May 2024 14:27:54 +0300 Subject: [PATCH 19/26] add button widget features --- src/WidgetDev.tsx | 4 +- src/widgets/button/ButtonWidget.tsx | 37 ++++----- src/widgets/button/OpenQuizButton.tsx | 107 +++++++++++++++++++++++--- src/widgets/shared/RunningStripe.tsx | 2 +- 4 files changed, 120 insertions(+), 30 deletions(-) diff --git a/src/WidgetDev.tsx b/src/WidgetDev.tsx index 901713c..98f50df 100644 --- a/src/WidgetDev.tsx +++ b/src/WidgetDev.tsx @@ -1,11 +1,12 @@ import lightTheme from "@/utils/themes/light"; import { Box, ThemeProvider, Typography } from "@mui/material"; import { useEffect, useRef } from "react"; -import { PopupWidget as Widget } from "./widgets"; +import { ButtonWidget as Widget } from "./widgets"; const widgetProps: ConstructorParameters[0] = { quizId: "3c49550d-8c77-4788-bc2d-42586a261514", + selector: "#widget-button", }; export default function WidgetDev() { @@ -28,6 +29,7 @@ export default function WidgetDev() { }} > + diff --git a/src/widgets/button/ButtonWidget.tsx b/src/widgets/button/ButtonWidget.tsx index 5482d18..a432749 100644 --- a/src/widgets/button/ButtonWidget.tsx +++ b/src/widgets/button/ButtonWidget.tsx @@ -1,25 +1,28 @@ +import { ComponentPropsWithoutRef } from "react"; import { Root, createRoot } from "react-dom/client"; +import { pollForSelector } from "../shared/pollForSelector"; import OpenQuizButton from "./OpenQuizButton"; import { createPortal } from "react-dom"; -import { pollForSelector } from "../shared/pollForSelector"; +type Props = ComponentPropsWithoutRef; + export class ButtonWidget { root: Root | undefined; - element = document.createElement("div"); - constructor({ quizId, selector, selectorPollingTimeLimit = 60 }: { - quizId: string; + constructor(props: Props & { selector: string; /** * In seconds, null - polling disabled */ selectorPollingTimeLimit?: number | null; }) { + const { selector, selectorPollingTimeLimit = 60 } = props; + const element = document.querySelector(selector); if (element) { this.root = createRoot(element); - this.root.render(); + this.render(props); return; } @@ -31,13 +34,16 @@ export class ButtonWidget { pollForSelector(selector, selectorPollingTimeLimit, (element) => { this.root = createRoot(element); - this.root.render(); + this.render(props); }); } + render(props: Props) { + this.root?.render(); + } + destroy() { if (this.root) this.root.unmount(); - this.element.remove(); } } @@ -45,22 +51,17 @@ export class ButtonWidgetFixed { root: Root | undefined; element = document.createElement("div"); - constructor({ quizId, side }: { - quizId: string; - side: "left" | "right"; - }) { + constructor(props: Props) { this.element.style.setProperty("display", "none"); document.body.appendChild(this.element); this.root = createRoot(this.element); - this.root.render(createPortal( - , - document.body - )); + this.render(props); + } + + render(props: Props) { + this.root?.render(createPortal(, document.body)); } destroy() { diff --git a/src/widgets/button/OpenQuizButton.tsx b/src/widgets/button/OpenQuizButton.tsx index 08b35a8..72cc093 100644 --- a/src/widgets/button/OpenQuizButton.tsx +++ b/src/widgets/button/OpenQuizButton.tsx @@ -1,26 +1,108 @@ import lightTheme from "@/utils/themes/light"; -import { Button, ThemeProvider } from "@mui/material"; -import { useState } from "react"; +import { Button, ThemeProvider, useMediaQuery } from "@mui/material"; +import { useEffect, useRef, useState } from "react"; import QuizDialog from "../shared/QuizDialog"; +import RunningStripe from "../shared/RunningStripe"; +import { useQuizCompletionStatus } from "../shared/useQuizCompletionStatus"; +const WIDGET_DEFAULT_WIDTH = "600px"; +const WIDGET_DEFAULT_HEIGHT = "80%"; + interface Props { - fixedSide?: "left" | "right"; quizId: string; + fixedSide?: "left" | "right"; + dimensions?: { width: string; height: string; }; + /** + * Открыть квиз через X секунд + */ + autoShowQuizTime?: number; + hideOnMobile?: boolean; + openOnLeaveAttempt?: boolean; + buttonFlash?: boolean; + withShadow?: boolean; + rounded?: boolean; + buttonText?: string; + buttonTextColor?: string; + buttonBackgroundColor?: string; } -export default function OpenQuizButton({ quizId, fixedSide }: Props) { - const [isQuizDialogOpen, setIsQuizDialogOpen] = useState(false); +export default function OpenQuizButton({ + quizId, + fixedSide, + autoShowQuizTime = 0, + dimensions, + hideOnMobile, + openOnLeaveAttempt, + buttonFlash = false, + withShadow = false, + rounded = false, + buttonText = "Пройти квиз", + buttonTextColor, + buttonBackgroundColor, +}: Props) { + const isMobile = useMediaQuery("(max-width: 600px)"); + const [isQuizShown, setIsQuizShown] = useState(false); + const isQuizCompleted = useQuizCompletionStatus(quizId); + const [isFlashEnabled, setIsFlashEnabled] = useState(buttonFlash); + const preventQuizAutoShowRef = useRef(false); + const preventOpenOnLeaveAttemptRef = useRef(false); + + useEffect(function setAutoShowQuizTimer() { + if (!autoShowQuizTime || openOnLeaveAttempt) return; + + const timeout = setTimeout(() => { + setIsQuizShown(true); + }, autoShowQuizTime * 1000); + + return () => { + clearTimeout(timeout); + }; + }, [autoShowQuizTime, openOnLeaveAttempt]); + + useEffect(function attachLeaveListener() { + if (!openOnLeaveAttempt) return; + + const handleMouseLeave = () => { + if (!preventOpenOnLeaveAttemptRef.current) { + preventOpenOnLeaveAttemptRef.current = true; + setIsQuizShown(true); + } + }; + + document.addEventListener("mouseleave", handleMouseLeave); + + return () => { + document.removeEventListener("mouseleave", handleMouseLeave); + }; + }, [openOnLeaveAttempt]); + + function openQuiz() { + preventQuizAutoShowRef.current = true; + setIsQuizShown(true); + setIsFlashEnabled(false); + } + + if (hideOnMobile && isMobile) return null; return ( setIsQuizDialogOpen(false)} + onClose={() => setIsQuizShown(false)} + paperSx={{ + width: dimensions?.width ?? WIDGET_DEFAULT_WIDTH, + height: dimensions?.height ?? WIDGET_DEFAULT_HEIGHT, + }} /> ); diff --git a/src/widgets/shared/RunningStripe.tsx b/src/widgets/shared/RunningStripe.tsx index 7b526c4..0faf39a 100644 --- a/src/widgets/shared/RunningStripe.tsx +++ b/src/widgets/shared/RunningStripe.tsx @@ -20,7 +20,7 @@ export default function RunningStripe({ sx = [] }: Props) { transform: "rotate(-60deg)", "@keyframes runningStripe": { "0%": { - left: "-20%", + left: "-150px", opacity: 1, }, "25%, 100%": { From b8313765d37ba48d3403e50ddffd61823566af9d Mon Sep 17 00:00:00 2001 From: nflnkr Date: Thu, 9 May 2024 14:39:36 +0300 Subject: [PATCH 20/26] refactor autoShowQuizTime quiz widget param --- src/widgets/button/OpenQuizButton.tsx | 8 ++++---- src/widgets/popup/QuizPopup.tsx | 10 +++++----- src/widgets/side/QuizSideButton.tsx | 8 ++++---- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/widgets/button/OpenQuizButton.tsx b/src/widgets/button/OpenQuizButton.tsx index 72cc093..aac0186 100644 --- a/src/widgets/button/OpenQuizButton.tsx +++ b/src/widgets/button/OpenQuizButton.tsx @@ -14,9 +14,9 @@ interface Props { fixedSide?: "left" | "right"; dimensions?: { width: string; height: string; }; /** - * Открыть квиз через X секунд + * Открыть квиз через X секунд, 0 - сразу */ - autoShowQuizTime?: number; + autoShowQuizTime?: number | null; hideOnMobile?: boolean; openOnLeaveAttempt?: boolean; buttonFlash?: boolean; @@ -30,7 +30,7 @@ interface Props { export default function OpenQuizButton({ quizId, fixedSide, - autoShowQuizTime = 0, + autoShowQuizTime = null, dimensions, hideOnMobile, openOnLeaveAttempt, @@ -49,7 +49,7 @@ export default function OpenQuizButton({ const preventOpenOnLeaveAttemptRef = useRef(false); useEffect(function setAutoShowQuizTimer() { - if (!autoShowQuizTime || openOnLeaveAttempt) return; + if (autoShowQuizTime === null || openOnLeaveAttempt) return; const timeout = setTimeout(() => { setIsQuizShown(true); diff --git a/src/widgets/popup/QuizPopup.tsx b/src/widgets/popup/QuizPopup.tsx index f824328..068068e 100644 --- a/src/widgets/popup/QuizPopup.tsx +++ b/src/widgets/popup/QuizPopup.tsx @@ -11,9 +11,9 @@ interface Props { quizId: string; dimensions?: { width: string; height: string; }; /** - * Открыть квиз через X секунд + * Открыть квиз через X секунд, 0 - сразу */ - autoShowQuizTime?: number; + autoShowQuizTime?: number | null; hideOnMobile?: boolean; openOnLeaveAttempt?: boolean; } @@ -21,11 +21,11 @@ interface Props { export default function QuizPopup({ quizId, dimensions, - autoShowQuizTime = 0, + autoShowQuizTime = null, hideOnMobile = false, openOnLeaveAttempt = false, }: Props) { - const initialIsQuizShown = (autoShowQuizTime || openOnLeaveAttempt) ? false : true; + const initialIsQuizShown = (autoShowQuizTime !== null || openOnLeaveAttempt) ? false : true; const [isQuizShown, setIsQuizShown] = useState(initialIsQuizShown); const isQuizCompleted = useQuizCompletionStatus(quizId); @@ -33,7 +33,7 @@ export default function QuizPopup({ const preventOpenOnLeaveAttemptRef = useRef(false); useEffect(function setAutoShowQuizTimer() { - if (!autoShowQuizTime || openOnLeaveAttempt) return; + if (autoShowQuizTime === null || openOnLeaveAttempt) return; const timeout = setTimeout(() => { setIsQuizShown(true); diff --git a/src/widgets/side/QuizSideButton.tsx b/src/widgets/side/QuizSideButton.tsx index 3e14cee..6ad6fe2 100644 --- a/src/widgets/side/QuizSideButton.tsx +++ b/src/widgets/side/QuizSideButton.tsx @@ -25,9 +25,9 @@ interface Props { */ autoOpenTime?: number; /** - * Открыть квиз через X секунд + * Открыть квиз через X секунд, 0 - сразу */ - autoShowQuizTime?: number; + autoShowQuizTime?: number | null; hideOnMobile?: boolean; } @@ -40,7 +40,7 @@ export default function QuizSideButton({ fullScreen = false, buttonFlash = false, autoOpenTime = 0, - autoShowQuizTime = 0, + autoShowQuizTime = null, hideOnMobile = false, }: Props) { const [isQuizShown, setIsQuizShown] = useState(false); @@ -51,7 +51,7 @@ export default function QuizSideButton({ const preventQuizAutoShowRef = useRef(false); useEffect(function setAutoShowQuizTimer() { - if (!autoShowQuizTime) return; + if (autoShowQuizTime === null) return; const timeout = setTimeout(() => { if (!preventQuizAutoShowRef.current) setIsQuizShown(true); From ceb1f1e7c29cd6e4f95981d0d2f7be917acbff40 Mon Sep 17 00:00:00 2001 From: nflnkr Date: Thu, 9 May 2024 15:05:12 +0300 Subject: [PATCH 21/26] fix button widget params types --- src/widgets/button/ButtonWidget.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/widgets/button/ButtonWidget.tsx b/src/widgets/button/ButtonWidget.tsx index a432749..68965ea 100644 --- a/src/widgets/button/ButtonWidget.tsx +++ b/src/widgets/button/ButtonWidget.tsx @@ -5,12 +5,12 @@ import OpenQuizButton from "./OpenQuizButton"; import { createPortal } from "react-dom"; -type Props = ComponentPropsWithoutRef; +type ButtonWidgetProps = Omit, "fixedSide">; export class ButtonWidget { root: Root | undefined; - constructor(props: Props & { + constructor(props: ButtonWidgetProps & { selector: string; /** * In seconds, null - polling disabled @@ -38,7 +38,7 @@ export class ButtonWidget { }); } - render(props: Props) { + render(props: ButtonWidgetProps) { this.root?.render(); } @@ -47,11 +47,13 @@ export class ButtonWidget { } } +type ButtonWidgetFixedProps = Omit, "selector">; + export class ButtonWidgetFixed { root: Root | undefined; element = document.createElement("div"); - constructor(props: Props) { + constructor(props: ButtonWidgetFixedProps) { this.element.style.setProperty("display", "none"); document.body.appendChild(this.element); @@ -60,7 +62,7 @@ export class ButtonWidgetFixed { this.render(props); } - render(props: Props) { + render(props: ButtonWidgetFixedProps) { this.root?.render(createPortal(, document.body)); } From ab6f58066e9530223baf393277632c3394fcedbf Mon Sep 17 00:00:00 2001 From: nflnkr Date: Thu, 9 May 2024 18:16:56 +0300 Subject: [PATCH 22/26] fix button widget types & minor --- src/widgets/button/ButtonWidget.tsx | 4 +++- src/widgets/button/OpenQuizButton.tsx | 2 +- src/widgets/side/QuizSideButton.tsx | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/widgets/button/ButtonWidget.tsx b/src/widgets/button/ButtonWidget.tsx index 68965ea..d322a3e 100644 --- a/src/widgets/button/ButtonWidget.tsx +++ b/src/widgets/button/ButtonWidget.tsx @@ -47,7 +47,9 @@ export class ButtonWidget { } } -type ButtonWidgetFixedProps = Omit, "selector">; +type ButtonWidgetFixedProps = Omit, "selector"> & { + fixedSide: "left" | "right"; +}; export class ButtonWidgetFixed { root: Root | undefined; diff --git a/src/widgets/button/OpenQuizButton.tsx b/src/widgets/button/OpenQuizButton.tsx index aac0186..fe28a2d 100644 --- a/src/widgets/button/OpenQuizButton.tsx +++ b/src/widgets/button/OpenQuizButton.tsx @@ -120,8 +120,8 @@ export default function OpenQuizButton({ }, ]} > + {buttonText} {!isQuizCompleted && isFlashEnabled && } - {buttonText} - {!isQuizCompleted && isFlashEnabled && } Пройти квиз + {!isQuizCompleted && isFlashEnabled && } , From 4e0bdb6f3f785ade5f8146c612a2977ca9300a12 Mon Sep 17 00:00:00 2001 From: nflnkr Date: Fri, 10 May 2024 09:59:12 +0300 Subject: [PATCH 23/26] add banner widget features --- src/WidgetDev.tsx | 6 +- src/widgets/banner/BannerWidget.tsx | 15 +- src/widgets/banner/QuizBanner.tsx | 245 +++++++++++++++++++++------- src/widgets/shared/BannerIcon.tsx | 25 +++ 4 files changed, 225 insertions(+), 66 deletions(-) create mode 100644 src/widgets/shared/BannerIcon.tsx diff --git a/src/WidgetDev.tsx b/src/WidgetDev.tsx index 98f50df..567736f 100644 --- a/src/WidgetDev.tsx +++ b/src/WidgetDev.tsx @@ -1,12 +1,14 @@ import lightTheme from "@/utils/themes/light"; import { Box, ThemeProvider, Typography } from "@mui/material"; import { useEffect, useRef } from "react"; -import { ButtonWidget as Widget } from "./widgets"; +import { BannerWidget as Widget } from "./widgets"; const widgetProps: ConstructorParameters[0] = { quizId: "3c49550d-8c77-4788-bc2d-42586a261514", - selector: "#widget-button", + position: "bottomright", + pulsation: true, + rounded: true, }; export default function WidgetDev() { diff --git a/src/widgets/banner/BannerWidget.tsx b/src/widgets/banner/BannerWidget.tsx index 3153d6f..f2e4ed1 100644 --- a/src/widgets/banner/BannerWidget.tsx +++ b/src/widgets/banner/BannerWidget.tsx @@ -3,21 +3,26 @@ import QuizBanner from "./QuizBanner"; import { ComponentPropsWithoutRef } from "react"; +type Props = Omit, "onClose">; + export class BannerWidget { root: Root | undefined; element = document.createElement("div"); - constructor({ quizId, position }: ComponentPropsWithoutRef) { + constructor(props: Props) { this.element.style.setProperty("display", "none"); document.body.appendChild(this.element); this.root = createRoot(this.element); - this.root.render( + this.render(props); + } + + render(props: Props) { + this.root?.render( this.destroy()} + {...props} + onWidgetClose={() => this.destroy()} /> ); } diff --git a/src/widgets/banner/QuizBanner.tsx b/src/widgets/banner/QuizBanner.tsx index 7a81976..93a2a09 100644 --- a/src/widgets/banner/QuizBanner.tsx +++ b/src/widgets/banner/QuizBanner.tsx @@ -1,80 +1,207 @@ import lightTheme from "@/utils/themes/light"; import CloseIcon from '@mui/icons-material/Close'; -import { Box, Button, IconButton, ThemeProvider } from "@mui/material"; -import { useState } from "react"; +import { Box, Button, Fade, IconButton, ThemeProvider, Typography, useMediaQuery } from "@mui/material"; +import { useEffect, useRef, useState } from "react"; import { createPortal } from "react-dom"; import QuizDialog from "../shared/QuizDialog"; +import RunningStripe from "../shared/RunningStripe"; +import { useQuizCompletionStatus } from "../shared/useQuizCompletionStatus"; +import BannerIcon from "../shared/BannerIcon"; const PADDING = 10; interface Props { - position: "topleft" | "topright" | "bottomleft" | "bottomright"; quizId: string; - onClose: () => void; + position: "topleft" | "topright" | "bottomleft" | "bottomright"; + onWidgetClose?: () => void; + appealText?: string; + quizHeaderText?: string; + buttonTextColor?: string; + buttonBackgroundColor?: string; + /** + * Открыть квиз через X секунд, 0 - сразу + */ + autoShowQuizTime?: number | null; + openOnLeaveAttempt?: boolean; + buttonFlash?: boolean; + hideOnMobile?: boolean; + withShadow?: boolean; + rounded?: boolean; + bannerFullWidth?: boolean; + pulsation?: boolean; } -export default function QuizBanner({ quizId, position, onClose }: Props) { - const [isQuizDialogOpen, setIsQuizDialogOpen] = useState(false); +export default function QuizBanner({ + quizId, + position, + onWidgetClose, + appealText = "Пройти тест", + quizHeaderText = "Заголовок квиза", + buttonTextColor, + buttonBackgroundColor, + autoShowQuizTime = null, + openOnLeaveAttempt, + buttonFlash = false, + hideOnMobile, + withShadow = false, + rounded = false, + bannerFullWidth = false, + pulsation = false, +}: Props) { + const isMobile = useMediaQuery("(max-width: 600px)"); + const [isQuizShown, setIsQuizShown] = useState(false); + const [isFlashEnabled, setIsFlashEnabled] = useState(buttonFlash); + const isQuizCompleted = useQuizCompletionStatus(quizId); + const preventQuizAutoShowRef = useRef(false); + const preventOpenOnLeaveAttemptRef = useRef(false); + + useEffect(function setAutoShowQuizTimer() { + if (autoShowQuizTime === null || openOnLeaveAttempt) return; + + const timeout = setTimeout(() => { + setIsQuizShown(true); + }, autoShowQuizTime * 1000); + + return () => { + clearTimeout(timeout); + }; + }, [autoShowQuizTime, openOnLeaveAttempt]); + + useEffect(function attachLeaveListener() { + if (!openOnLeaveAttempt) return; + + const handleMouseLeave = () => { + if (!preventOpenOnLeaveAttemptRef.current) { + preventOpenOnLeaveAttemptRef.current = true; + setIsQuizShown(true); + } + }; + + document.addEventListener("mouseleave", handleMouseLeave); + + return () => { + document.removeEventListener("mouseleave", handleMouseLeave); + }; + }, [openOnLeaveAttempt]); + + function openQuiz() { + preventQuizAutoShowRef.current = true; + setIsQuizShown(true); + setIsFlashEnabled(false); + } + + if (hideOnMobile && isMobile) return null; return createPortal( - - - - - - + + + + + + setIsQuizDialogOpen(false)} + onClose={() => setIsQuizShown(false)} + disableScrollLock /> , document.body diff --git a/src/widgets/shared/BannerIcon.tsx b/src/widgets/shared/BannerIcon.tsx new file mode 100644 index 0000000..fd3b402 --- /dev/null +++ b/src/widgets/shared/BannerIcon.tsx @@ -0,0 +1,25 @@ +import { Box } from "@mui/material"; + + +export default function BannerIcon() { + + return ( + + + + + + + + + ); +} From aebf66eba4d86de9fd7bd1b1efb7ee607ff1d089 Mon Sep 17 00:00:00 2001 From: nflnkr Date: Fri, 10 May 2024 14:51:29 +0300 Subject: [PATCH 24/26] fix banner widget close button --- src/WidgetDev.tsx | 2 +- src/widgets/banner/QuizBanner.tsx | 19 +++++++++++-------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/WidgetDev.tsx b/src/WidgetDev.tsx index 567736f..7da8233 100644 --- a/src/WidgetDev.tsx +++ b/src/WidgetDev.tsx @@ -6,7 +6,7 @@ import { BannerWidget as Widget } from "./widgets"; const widgetProps: ConstructorParameters[0] = { quizId: "3c49550d-8c77-4788-bc2d-42586a261514", - position: "bottomright", + position: "bottomleft", pulsation: true, rounded: true, }; diff --git a/src/widgets/banner/QuizBanner.tsx b/src/widgets/banner/QuizBanner.tsx index 93a2a09..88427ba 100644 --- a/src/widgets/banner/QuizBanner.tsx +++ b/src/widgets/banner/QuizBanner.tsx @@ -1,12 +1,11 @@ import lightTheme from "@/utils/themes/light"; -import CloseIcon from '@mui/icons-material/Close'; import { Box, Button, Fade, IconButton, ThemeProvider, Typography, useMediaQuery } from "@mui/material"; import { useEffect, useRef, useState } from "react"; import { createPortal } from "react-dom"; +import BannerIcon from "../shared/BannerIcon"; import QuizDialog from "../shared/QuizDialog"; import RunningStripe from "../shared/RunningStripe"; import { useQuizCompletionStatus } from "../shared/useQuizCompletionStatus"; -import BannerIcon from "../shared/BannerIcon"; const PADDING = 10; @@ -103,7 +102,7 @@ export default function QuizBanner({ position: "fixed", height: "120px", width: bannerFullWidth ? "100%" : "800px", - maxWidth: "100%", + maxWidth: `calc(100% - ${PADDING * 2}px)`, }, position === "topleft" && { top: bannerFullWidth ? 0 : PADDING, @@ -186,14 +185,18 @@ export default function QuizBanner({ position: "absolute", top: 0, right: 0, - p: 0, - width: "34px", - height: "34px", + p: "8px", + width: "44px", + height: "44px", borderRadius: "4px", - backgroundColor: "#333647", + ":hover": { + backgroundColor: "rgba(0, 0, 0, 0.3)", + }, }} > - + + + From 2375f81b48fec3d2a9c3dea0c610d75e3ed25e6b Mon Sep 17 00:00:00 2001 From: nflnkr Date: Mon, 13 May 2024 16:41:10 +0300 Subject: [PATCH 25/26] rename widget components dimensions prop to dialogDimensions --- src/WidgetDev.tsx | 12 ++++++++---- src/widgets/button/OpenQuizButton.tsx | 8 ++++---- src/widgets/popup/QuizPopup.tsx | 8 ++++---- src/widgets/side/QuizSideButton.tsx | 8 ++++---- 4 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/WidgetDev.tsx b/src/WidgetDev.tsx index 7da8233..29dd6b3 100644 --- a/src/WidgetDev.tsx +++ b/src/WidgetDev.tsx @@ -1,14 +1,12 @@ import lightTheme from "@/utils/themes/light"; import { Box, ThemeProvider, Typography } from "@mui/material"; import { useEffect, useRef } from "react"; -import { BannerWidget as Widget } from "./widgets"; +import { ContainerWidget as Widget } from "./widgets"; const widgetProps: ConstructorParameters[0] = { quizId: "3c49550d-8c77-4788-bc2d-42586a261514", - position: "bottomleft", - pulsation: true, - rounded: true, + selector: "#widget-container", }; export default function WidgetDev() { @@ -32,6 +30,12 @@ export default function WidgetDev() { > + diff --git a/src/widgets/button/OpenQuizButton.tsx b/src/widgets/button/OpenQuizButton.tsx index fe28a2d..f60ba3a 100644 --- a/src/widgets/button/OpenQuizButton.tsx +++ b/src/widgets/button/OpenQuizButton.tsx @@ -12,7 +12,7 @@ const WIDGET_DEFAULT_HEIGHT = "80%"; interface Props { quizId: string; fixedSide?: "left" | "right"; - dimensions?: { width: string; height: string; }; + dialogDimensions?: { width: string; height: string; }; /** * Открыть квиз через X секунд, 0 - сразу */ @@ -31,7 +31,7 @@ export default function OpenQuizButton({ quizId, fixedSide, autoShowQuizTime = null, - dimensions, + dialogDimensions, hideOnMobile, openOnLeaveAttempt, buttonFlash = false, @@ -128,8 +128,8 @@ export default function OpenQuizButton({ quizId={quizId} onClose={() => setIsQuizShown(false)} paperSx={{ - width: dimensions?.width ?? WIDGET_DEFAULT_WIDTH, - height: dimensions?.height ?? WIDGET_DEFAULT_HEIGHT, + width: dialogDimensions?.width ?? WIDGET_DEFAULT_WIDTH, + height: dialogDimensions?.height ?? WIDGET_DEFAULT_HEIGHT, }} /> diff --git a/src/widgets/popup/QuizPopup.tsx b/src/widgets/popup/QuizPopup.tsx index 068068e..681c844 100644 --- a/src/widgets/popup/QuizPopup.tsx +++ b/src/widgets/popup/QuizPopup.tsx @@ -9,7 +9,7 @@ const WIDGET_DEFAULT_HEIGHT = "80%"; interface Props { quizId: string; - dimensions?: { width: string; height: string; }; + dialogDimensions?: { width: string; height: string; }; /** * Открыть квиз через X секунд, 0 - сразу */ @@ -20,7 +20,7 @@ interface Props { export default function QuizPopup({ quizId, - dimensions, + dialogDimensions, autoShowQuizTime = null, hideOnMobile = false, openOnLeaveAttempt = false, @@ -70,8 +70,8 @@ export default function QuizPopup({ quizId={quizId} onClose={() => setIsQuizShown(false)} paperSx={{ - width: dimensions?.width ?? WIDGET_DEFAULT_WIDTH, - height: dimensions?.height ?? WIDGET_DEFAULT_HEIGHT, + width: dialogDimensions?.width ?? WIDGET_DEFAULT_WIDTH, + height: dialogDimensions?.height ?? WIDGET_DEFAULT_HEIGHT, }} /> ); diff --git a/src/widgets/side/QuizSideButton.tsx b/src/widgets/side/QuizSideButton.tsx index 0d145f0..aaa78a4 100644 --- a/src/widgets/side/QuizSideButton.tsx +++ b/src/widgets/side/QuizSideButton.tsx @@ -17,7 +17,7 @@ interface Props { position: "left" | "right"; buttonBackgroundColor?: string; buttonTextColor?: string; - dimensions?: { width: string; height: string; }; + dialogDimensions?: { width: string; height: string; }; fullScreen?: boolean; buttonFlash?: boolean; /** @@ -36,7 +36,7 @@ export default function QuizSideButton({ position, buttonBackgroundColor, buttonTextColor, - dimensions, + dialogDimensions, fullScreen = false, buttonFlash = false, autoOpenTime = 0, @@ -87,9 +87,9 @@ export default function QuizSideButton({ bottom: PADDING, right: position === "right" ? PADDING : undefined, left: position === "left" ? PADDING : undefined, - width: dimensions?.width ?? WIDGET_DEFAULT_WIDTH, + width: dialogDimensions?.width ?? WIDGET_DEFAULT_WIDTH, maxWidth: `calc(100% - ${PADDING * 2}px)`, - height: dimensions?.height ?? WIDGET_DEFAULT_HEIGHT, + height: dialogDimensions?.height ?? WIDGET_DEFAULT_HEIGHT, maxHeight: `calc(100% - ${PADDING * 2}px)`, }, (isMobile || fullScreen) && { From 8242f9958016496b4968353d96ef442d7b725e8c Mon Sep 17 00:00:00 2001 From: nflnkr Date: Mon, 13 May 2024 16:41:41 +0300 Subject: [PATCH 26/26] add container widget param: show button on mobile instead of quiz --- src/widgets/container/ContainerWidget.tsx | 30 +++++++++---------- src/widgets/container/QuizContainer.tsx | 35 +++++++++++++++++++++++ 2 files changed, 48 insertions(+), 17 deletions(-) create mode 100644 src/widgets/container/QuizContainer.tsx diff --git a/src/widgets/container/ContainerWidget.tsx b/src/widgets/container/ContainerWidget.tsx index ee056b6..8efc41d 100644 --- a/src/widgets/container/ContainerWidget.tsx +++ b/src/widgets/container/ContainerWidget.tsx @@ -1,29 +1,27 @@ -import QuizAnswerer from "@/components/QuizAnswerer"; +import { ComponentPropsWithoutRef } from "react"; import { Root, createRoot } from "react-dom/client"; import { pollForSelector } from "../shared/pollForSelector"; +import QuizContainer from "./QuizContainer"; +type Props = ComponentPropsWithoutRef; + export class ContainerWidget { root: Root | undefined; - constructor({ selector, quizId, selectorPollingTimeLimit = 60 }: { - quizId: string; + constructor(props: Props & { selector: string; /** * In seconds, null - polling disabled */ selectorPollingTimeLimit?: number | null; }) { + const { selector, selectorPollingTimeLimit = 60 } = props; + const element = document.querySelector(selector); if (element) { this.root = createRoot(element); - this.root.render( - - ); + this.render(props); return; } @@ -35,16 +33,14 @@ export class ContainerWidget { pollForSelector(selector, selectorPollingTimeLimit, (element) => { this.root = createRoot(element); - this.root.render( - - ); + this.render(props); }); } + render(props: Props) { + this.root?.render(); + } + destroy() { if (this.root) this.root.unmount(); } diff --git a/src/widgets/container/QuizContainer.tsx b/src/widgets/container/QuizContainer.tsx new file mode 100644 index 0000000..3d6f267 --- /dev/null +++ b/src/widgets/container/QuizContainer.tsx @@ -0,0 +1,35 @@ +import QuizAnswerer from "@/components/QuizAnswerer"; +import { Box, useMediaQuery } from "@mui/material"; +import { ComponentPropsWithoutRef } from "react"; +import OpenQuizButton from "../button/OpenQuizButton"; + + +type Props = ComponentPropsWithoutRef & { + quizId: string; + showButtonOnMobile?: boolean; + dimensions?: { width: string; height: string; }; +}; + +export default function QuizContainer(props: Props) { + const { quizId, dimensions, showButtonOnMobile = false } = props; + const isMobile = useMediaQuery("(max-width: 600px)"); + + return showButtonOnMobile && isMobile ? ( + + ) : ( + + + + ); +}