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", }} > [0] = { + quizId: "3c49550d-8c77-4788-bc2d-42586a261514", + selector: "#widget-container", +}; + +export default function WidgetDev() { + const widgetRef = useRef(null); + + useEffect(() => { + if (!widgetRef.current) { + widgetRef.current = new Widget(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..376161d 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 { RouteObject, RouterProvider, createBrowserRouter } from "react-router-dom"; import App from "./App"; +import { StrictMode, lazy } from "react"; -const router = createBrowserRouter([ +const routes: RouteObject[] = [ { path: "/", children: [ @@ -18,8 +18,23 @@ 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( + + + +); diff --git a/src/widgets/QuizDialog.tsx b/src/widgets/QuizDialog.tsx deleted file mode 100644 index 44cf2eb..0000000 --- a/src/widgets/QuizDialog.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import QuizAnswerer from "@/components/QuizAnswerer"; -import { Dialog } from "@mui/material"; - - -interface Props { - open?: boolean; - quizId: string; - onClose?: () => void; -} - -export default function QuizDialog({ open = true, quizId, onClose }: Props) { - - return ( - - - - ); -} 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 da72cfe..88427ba 100644 --- a/src/widgets/banner/QuizBanner.tsx +++ b/src/widgets/banner/QuizBanner.tsx @@ -1,80 +1,210 @@ 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 "../QuizDialog"; +import BannerIcon from "../shared/BannerIcon"; +import QuizDialog from "../shared/QuizDialog"; +import RunningStripe from "../shared/RunningStripe"; +import { useQuizCompletionStatus } from "../shared/useQuizCompletionStatus"; 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/button/ButtonWidget.tsx b/src/widgets/button/ButtonWidget.tsx index 35447f5..d322a3e 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 "../pollForSelector"; +type ButtonWidgetProps = Omit, "fixedSide">; + export class ButtonWidget { root: Root | undefined; - element = document.createElement("div"); - constructor({ quizId, selector, selectorPollingTimeLimit = 60 }: { - quizId: string; + constructor(props: ButtonWidgetProps & { 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,36 +34,38 @@ export class ButtonWidget { pollForSelector(selector, selectorPollingTimeLimit, (element) => { this.root = createRoot(element); - this.root.render(); + this.render(props); }); } + render(props: ButtonWidgetProps) { + this.root?.render(); + } + destroy() { if (this.root) this.root.unmount(); - this.element.remove(); } } +type ButtonWidgetFixedProps = Omit, "selector"> & { + fixedSide: "left" | "right"; +}; + export class ButtonWidgetFixed { root: Root | undefined; element = document.createElement("div"); - constructor({ quizId, side }: { - quizId: string; - side: "left" | "right"; - }) { + constructor(props: ButtonWidgetFixedProps) { 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: ButtonWidgetFixedProps) { + this.root?.render(createPortal(, document.body)); } destroy() { diff --git a/src/widgets/button/OpenQuizButton.tsx b/src/widgets/button/OpenQuizButton.tsx index 3670fe3..f60ba3a 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 QuizDialog from "../QuizDialog"; +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"; + dialogDimensions?: { width: string; height: string; }; + /** + * Открыть квиз через X секунд, 0 - сразу + */ + autoShowQuizTime?: number | null; + 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 = null, + dialogDimensions, + 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 === 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 ( setIsQuizDialogOpen(false)} + onClose={() => setIsQuizShown(false)} + paperSx={{ + width: dialogDimensions?.width ?? WIDGET_DEFAULT_WIDTH, + height: dialogDimensions?.height ?? WIDGET_DEFAULT_HEIGHT, + }} /> ); diff --git a/src/widgets/container/ContainerWidget.tsx b/src/widgets/container/ContainerWidget.tsx index e9b02aa..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 "../pollForSelector"; +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 ? ( + + ) : ( + + + + ); +} diff --git a/src/widgets/popup/PopupWidget.tsx b/src/widgets/popup/PopupWidget.tsx index b7a6c97..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 "../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..681c844 --- /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; + dialogDimensions?: { width: string; height: string; }; + /** + * Открыть квиз через X секунд, 0 - сразу + */ + autoShowQuizTime?: number | null; + hideOnMobile?: boolean; + openOnLeaveAttempt?: boolean; +} + +export default function QuizPopup({ + quizId, + dialogDimensions, + autoShowQuizTime = null, + hideOnMobile = false, + openOnLeaveAttempt = false, +}: Props) { + const initialIsQuizShown = (autoShowQuizTime !== null || 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 === 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]); + + if (isQuizCompleted) return null; + if (hideOnMobile && isMobile) return null; + + return ( + setIsQuizShown(false)} + paperSx={{ + width: dialogDimensions?.width ?? WIDGET_DEFAULT_WIDTH, + height: dialogDimensions?.height ?? WIDGET_DEFAULT_HEIGHT, + }} + /> + ); +} 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 ( + + + + + + + + + ); +} diff --git a/src/widgets/shared/QuizDialog.tsx b/src/widgets/shared/QuizDialog.tsx new file mode 100644 index 0000000..fa3895c --- /dev/null +++ b/src/widgets/shared/QuizDialog.tsx @@ -0,0 +1,79 @@ +import QuizAnswerer from "@/components/QuizAnswerer"; +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, + paperSx = [], + hideBackdrop, + disableScrollLock, + onClose +}: Props) { + + return ( + + + + + + + ); +} diff --git a/src/widgets/shared/RunningStripe.tsx b/src/widgets/shared/RunningStripe.tsx new file mode 100644 index 0000000..0faf39a --- /dev/null +++ b/src/widgets/shared/RunningStripe.tsx @@ -0,0 +1,36 @@ +import { Box, SxProps, Theme } from "@mui/material"; + + +interface Props { + sx?: SxProps; +} + +export default function RunningStripe({ sx = [] }: Props) { + + return ( + + ); +} diff --git a/src/widgets/pollForSelector.ts b/src/widgets/shared/pollForSelector.ts similarity index 100% rename from src/widgets/pollForSelector.ts rename to src/widgets/shared/pollForSelector.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/shared/useQuizCompletionStatus.ts b/src/widgets/shared/useQuizCompletionStatus.ts new file mode 100644 index 0000000..0d6ee8a --- /dev/null +++ b/src/widgets/shared/useQuizCompletionStatus.ts @@ -0,0 +1,17 @@ +import { useMemo } from "react"; + + +export function useQuizCompletionStatus(quizId: string): boolean { + return useMemo(() => { + 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 ad3ea43..aaa78a4 100644 --- a/src/widgets/side/QuizSideButton.tsx +++ b/src/widgets/side/QuizSideButton.tsx @@ -1,58 +1,122 @@ -import { QuizAnswerer } from "@/index"; import lightTheme from "@/utils/themes/light"; -import { Box, Button, Grow, ThemeProvider } from "@mui/material"; -import { useState } from "react"; +import { Button, Fade, ThemeProvider, 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 { useAutoOpenTimer } from "../shared/useAutoOpenTimer"; +import { useQuizCompletionStatus } from "../shared/useQuizCompletionStatus"; const PADDING = 10; +const WIDGET_DEFAULT_WIDTH = "600px"; +const WIDGET_DEFAULT_HEIGHT = "800px"; interface Props { quizId: string; position: "left" | "right"; + buttonBackgroundColor?: string; + buttonTextColor?: string; + dialogDimensions?: { width: string; height: string; }; + fullScreen?: boolean; + buttonFlash?: boolean; + /** + * Скрывать виджет первые X секунд + */ + autoOpenTime?: number; + /** + * Открыть квиз через X секунд, 0 - сразу + */ + autoShowQuizTime?: number | null; + hideOnMobile?: boolean; } -export default function QuizSideButton({ quizId, position }: Props) { +export default function QuizSideButton({ + quizId, + position, + buttonBackgroundColor, + buttonTextColor, + dialogDimensions, + fullScreen = false, + buttonFlash = false, + autoOpenTime = 0, + autoShowQuizTime = null, + 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); + const preventQuizAutoShowRef = useRef(false); + + useEffect(function setAutoShowQuizTimer() { + if (autoShowQuizTime === null) return; + + const timeout = setTimeout(() => { + if (!preventQuizAutoShowRef.current) setIsQuizShown(true); + }, autoShowQuizTime * 1000); + + return () => { + clearTimeout(timeout); + }; + }, [autoShowQuizTime]); + + function openQuiz() { + preventQuizAutoShowRef.current = true; + setIsQuizShown(true); + setIsFlashEnabled(false); + } + + if (hideOnMobile && isMobile) return null; return createPortal( - {isQuizShown ? ( - - - - - - ) : ( + setIsQuizShown(false)} + hideBackdrop + disableScrollLock + paperSx={[ + { + m: 0, + }, + !(isMobile || fullScreen) && { + position: "absolute", + bottom: PADDING, + right: position === "right" ? PADDING : undefined, + left: position === "left" ? PADDING : undefined, + width: dialogDimensions?.width ?? WIDGET_DEFAULT_WIDTH, + maxWidth: `calc(100% - ${PADDING * 2}px)`, + height: dialogDimensions?.height ?? WIDGET_DEFAULT_HEIGHT, + maxHeight: `calc(100% - ${PADDING * 2}px)`, + }, + (isMobile || fullScreen) && { + position: "relative", + width: "100%", + height: "100%", + maxHeight: "100%", + borderRadius: 0, + }, + ]} + /> + - )} + , document.body ); diff --git a/src/widgets/side/SideWidget.tsx b/src/widgets/side/SideWidget.tsx index f42f47b..2c7c4d3 100644 --- a/src/widgets/side/SideWidget.tsx +++ b/src/widgets/side/SideWidget.tsx @@ -3,22 +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 }: 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(); } destroy() {