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 lightTheme from "@/utils/themes/light";
import { Box, ThemeProvider, Typography } from "@mui/material"; import { Box, ThemeProvider, Typography } from "@mui/material";
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import { PopupWidget as Widget } from "./widgets"; import { ButtonWidget as Widget } from "./widgets";
const widgetProps: ConstructorParameters<typeof Widget>[0] = { const widgetProps: ConstructorParameters<typeof Widget>[0] = {
quizId: "3c49550d-8c77-4788-bc2d-42586a261514", quizId: "3c49550d-8c77-4788-bc2d-42586a261514",
selector: "#widget-button",
}; };
export default function WidgetDev() { export default function WidgetDev() {
@ -28,6 +29,7 @@ export default function WidgetDev() {
}} }}
> >
<Lorem /> <Lorem />
<Box id="widget-button"></Box>
<Lorem /> <Lorem />
<Lorem /> <Lorem />
<Lorem /> <Lorem />

@ -1,25 +1,28 @@
import { ComponentPropsWithoutRef } from "react";
import { Root, createRoot } from "react-dom/client"; import { Root, createRoot } from "react-dom/client";
import { pollForSelector } from "../shared/pollForSelector";
import OpenQuizButton from "./OpenQuizButton"; import OpenQuizButton from "./OpenQuizButton";
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
import { pollForSelector } from "../shared/pollForSelector";
type Props = ComponentPropsWithoutRef<typeof OpenQuizButton>;
export class ButtonWidget { export class ButtonWidget {
root: Root | undefined; root: Root | undefined;
element = document.createElement("div");
constructor({ quizId, selector, selectorPollingTimeLimit = 60 }: { constructor(props: Props & {
quizId: string;
selector: string; selector: string;
/** /**
* In seconds, null - polling disabled * In seconds, null - polling disabled
*/ */
selectorPollingTimeLimit?: number | null; selectorPollingTimeLimit?: number | null;
}) { }) {
const { selector, selectorPollingTimeLimit = 60 } = props;
const element = document.querySelector(selector); const element = document.querySelector(selector);
if (element) { if (element) {
this.root = createRoot(element); this.root = createRoot(element);
this.root.render(<OpenQuizButton quizId={quizId} />); this.render(props);
return; return;
} }
@ -31,13 +34,16 @@ export class ButtonWidget {
pollForSelector(selector, selectorPollingTimeLimit, (element) => { pollForSelector(selector, selectorPollingTimeLimit, (element) => {
this.root = createRoot(element); this.root = createRoot(element);
this.root.render(<OpenQuizButton quizId={quizId} />); this.render(props);
}); });
} }
render(props: Props) {
this.root?.render(<OpenQuizButton {...props} />);
}
destroy() { destroy() {
if (this.root) this.root.unmount(); if (this.root) this.root.unmount();
this.element.remove();
} }
} }
@ -45,22 +51,17 @@ export class ButtonWidgetFixed {
root: Root | undefined; root: Root | undefined;
element = document.createElement("div"); element = document.createElement("div");
constructor({ quizId, side }: { constructor(props: Props) {
quizId: string;
side: "left" | "right";
}) {
this.element.style.setProperty("display", "none"); this.element.style.setProperty("display", "none");
document.body.appendChild(this.element); document.body.appendChild(this.element);
this.root = createRoot(this.element); this.root = createRoot(this.element);
this.root.render(createPortal( this.render(props);
<OpenQuizButton }
fixedSide={side}
quizId={quizId} render(props: Props) {
/>, this.root?.render(createPortal(<OpenQuizButton {...props} />, document.body));
document.body
));
} }
destroy() { destroy() {

@ -1,26 +1,108 @@
import lightTheme from "@/utils/themes/light"; import lightTheme from "@/utils/themes/light";
import { Button, ThemeProvider } from "@mui/material"; import { Button, ThemeProvider, useMediaQuery } from "@mui/material";
import { useState } from "react"; import { useEffect, useRef, useState } from "react";
import QuizDialog from "../shared/QuizDialog"; 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 { interface Props {
fixedSide?: "left" | "right";
quizId: string; 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) { export default function OpenQuizButton({
const [isQuizDialogOpen, setIsQuizDialogOpen] = useState<boolean>(false); 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 ( return (
<ThemeProvider theme={lightTheme}> <ThemeProvider theme={lightTheme}>
<Button <Button
className="pena-quiz-widget-button" className="pena-quiz-widget-button"
onClick={() => setIsQuizDialogOpen(p => !p)} onClick={openQuiz}
variant="contained" variant="contained"
disableFocusRipple
sx={[ 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) && { Boolean(fixedSide) && {
position: "fixed", position: "fixed",
@ -38,12 +120,17 @@ export default function OpenQuizButton({ quizId, fixedSide }: Props) {
}, },
]} ]}
> >
Пройти квиз {!isQuizCompleted && isFlashEnabled && <RunningStripe />}
{buttonText}
</Button> </Button>
<QuizDialog <QuizDialog
open={isQuizDialogOpen} open={isQuizShown}
quizId={quizId} quizId={quizId}
onClose={() => setIsQuizDialogOpen(false)} onClose={() => setIsQuizShown(false)}
paperSx={{
width: dimensions?.width ?? WIDGET_DEFAULT_WIDTH,
height: dimensions?.height ?? WIDGET_DEFAULT_HEIGHT,
}}
/> />
</ThemeProvider> </ThemeProvider>
); );

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