add button widget features

This commit is contained in:
nflnkr 2024-05-09 14:27:54 +03:00
parent cd972e493b
commit 92d727b2e9
4 changed files with 120 additions and 30 deletions

@ -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<typeof Widget>[0] = {
quizId: "3c49550d-8c77-4788-bc2d-42586a261514",
selector: "#widget-button",
};
export default function WidgetDev() {
@ -28,6 +29,7 @@ export default function WidgetDev() {
}}
>
<Lorem />
<Box id="widget-button"></Box>
<Lorem />
<Lorem />
<Lorem />

@ -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<typeof OpenQuizButton>;
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(<OpenQuizButton quizId={quizId} />);
this.render(props);
return;
}
@ -31,13 +34,16 @@ export class ButtonWidget {
pollForSelector(selector, selectorPollingTimeLimit, (element) => {
this.root = createRoot(element);
this.root.render(<OpenQuizButton quizId={quizId} />);
this.render(props);
});
}
render(props: Props) {
this.root?.render(<OpenQuizButton {...props} />);
}
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(
<OpenQuizButton
fixedSide={side}
quizId={quizId}
/>,
document.body
));
this.render(props);
}
render(props: Props) {
this.root?.render(createPortal(<OpenQuizButton {...props} />, document.body));
}
destroy() {

@ -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<boolean>(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<boolean>(false);
const isQuizCompleted = useQuizCompletionStatus(quizId);
const [isFlashEnabled, setIsFlashEnabled] = useState<boolean>(buttonFlash);
const preventQuizAutoShowRef = useRef<boolean>(false);
const preventOpenOnLeaveAttemptRef = useRef<boolean>(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 (
<ThemeProvider theme={lightTheme}>
<Button
className="pena-quiz-widget-button"
onClick={() => setIsQuizDialogOpen(p => !p)}
onClick={openQuiz}
variant="contained"
disableFocusRipple
sx={[
{
// normal styles
overflow: "hidden",
color: buttonTextColor,
backgroundColor: buttonBackgroundColor,
},
withShadow && {
boxShadow: "0px 0px 8px 0px rgba(0, 0, 0, 0.7)",
},
!rounded && {
borderRadius: 0,
},
Boolean(fixedSide) && {
position: "fixed",
@ -38,12 +120,17 @@ export default function OpenQuizButton({ quizId, fixedSide }: Props) {
},
]}
>
Пройти квиз
{!isQuizCompleted && isFlashEnabled && <RunningStripe />}
{buttonText}
</Button>
<QuizDialog
open={isQuizDialogOpen}
open={isQuizShown}
quizId={quizId}
onClose={() => setIsQuizDialogOpen(false)}
onClose={() => setIsQuizShown(false)}
paperSx={{
width: dimensions?.width ?? WIDGET_DEFAULT_WIDTH,
height: dimensions?.height ?? WIDGET_DEFAULT_HEIGHT,
}}
/>
</ThemeProvider>
);

@ -20,7 +20,7 @@ export default function RunningStripe({ sx = [] }: Props) {
transform: "rotate(-60deg)",
"@keyframes runningStripe": {
"0%": {
left: "-20%",
left: "-150px",
opacity: 1,
},
"25%, 100%": {