add banner widget features

This commit is contained in:
nflnkr 2024-05-10 09:59:12 +03:00
parent ab6f58066e
commit 4e0bdb6f3f
4 changed files with 225 additions and 66 deletions

@ -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<typeof Widget>[0] = {
quizId: "3c49550d-8c77-4788-bc2d-42586a261514",
selector: "#widget-button",
position: "bottomright",
pulsation: true,
rounded: true,
};
export default function WidgetDev() {

@ -3,21 +3,26 @@ import QuizBanner from "./QuizBanner";
import { ComponentPropsWithoutRef } from "react";
type Props = Omit<ComponentPropsWithoutRef<typeof QuizBanner>, "onClose">;
export class BannerWidget {
root: Root | undefined;
element = document.createElement("div");
constructor({ quizId, position }: ComponentPropsWithoutRef<typeof QuizBanner>) {
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(
<QuizBanner
quizId={quizId}
position={position}
onClose={() => this.destroy()}
{...props}
onWidgetClose={() => this.destroy()}
/>
);
}

@ -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<boolean>(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<boolean>(false);
const [isFlashEnabled, setIsFlashEnabled] = useState<boolean>(buttonFlash);
const isQuizCompleted = useQuizCompletionStatus(quizId);
const preventQuizAutoShowRef = useRef<boolean>(false);
const preventOpenOnLeaveAttemptRef = useRef<boolean>(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(
<ThemeProvider theme={lightTheme}>
<Box
className="pena-quiz-widget-banner"
sx={[
{
position: "fixed",
height: "70px",
width: `calc(min(calc(100% - ${PADDING * 2}px), max(500px, 70%)))`,
},
position === "topleft" && {
top: PADDING,
left: PADDING,
},
position === "topright" && {
top: PADDING,
right: PADDING,
},
position === "bottomleft" && {
bottom: PADDING,
left: PADDING,
},
position === "bottomright" && {
bottom: PADDING,
right: PADDING,
},
]}
>
<Button
onClick={() => setIsQuizDialogOpen(p => !p)}
variant="contained"
sx={{
height: "100%",
width: "100%",
}}
<Fade in={!isQuizShown}>
<Box
className="pena-quiz-widget-banner"
sx={[
{
position: "fixed",
height: "120px",
width: bannerFullWidth ? "100%" : "800px",
maxWidth: "100%",
},
position === "topleft" && {
top: bannerFullWidth ? 0 : PADDING,
left: bannerFullWidth ? 0 : PADDING,
},
position === "topright" && {
top: bannerFullWidth ? 0 : PADDING,
right: bannerFullWidth ? 0 : PADDING,
},
position === "bottomleft" && {
bottom: bannerFullWidth ? 0 : PADDING,
left: bannerFullWidth ? 0 : PADDING,
},
position === "bottomright" && {
bottom: bannerFullWidth ? 0 : PADDING,
right: bannerFullWidth ? 0 : PADDING,
},
pulsation && {
":before": {
content: "''",
position: "absolute",
height: "100%",
width: "100%",
pointerEvents: "none",
willChange: "box-shadow",
borderRadius: rounded ? "8px" : 0,
animation: "pena-pulsation linear 5s infinite",
"@keyframes pena-pulsation": {
"0%": {
boxShadow: "0 0 0 0 rgba(126, 42, 234, 0.5)",
},
"30%": {
boxShadow: "0 0 0 15px rgba(0, 0, 0, 0)",
},
"100%": {
boxShadow: "0 0 0 0 rgba(0, 0, 0, 0)",
},
},
},
},
]}
>
Пройти квиз
</Button>
<IconButton
onClick={onClose}
sx={{
position: "absolute",
top: 0,
right: 0,
p: 0,
width: "34px",
height: "34px",
borderRadius: "4px",
backgroundColor: "#333647",
}}
>
<CloseIcon sx={{ color: "#FFFFFF" }} />
</IconButton>
</Box>
<Button
onClick={openQuiz}
variant="contained"
sx={[
{
display: "flex",
gap: "20px",
overflow: "hidden",
height: "100%",
width: "100%",
px: "28px",
color: buttonTextColor,
backgroundColor: buttonBackgroundColor,
borderRadius: rounded ? "8px" : 0,
justifyContent: "start",
},
withShadow && {
boxShadow: "0px 0px 12px 0px rgba(0, 0, 0, 0.7)",
},
]}
>
<BannerIcon />
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "start",
}}
>
<Typography fontSize="24px" lineHeight="120%">{appealText}</Typography>
<Typography fontSize="44px" lineHeight="120%">{quizHeaderText}</Typography>
</Box>
{!isQuizCompleted && isFlashEnabled && <RunningStripe />}
</Button>
<IconButton
onClick={onWidgetClose}
sx={{
position: "absolute",
top: 0,
right: 0,
p: 0,
width: "34px",
height: "34px",
borderRadius: "4px",
backgroundColor: "#333647",
}}
>
<CloseIcon sx={{ color: "#FFFFFF" }} />
</IconButton>
</Box>
</Fade>
<QuizDialog
open={isQuizDialogOpen}
open={isQuizShown}
quizId={quizId}
onClose={() => setIsQuizDialogOpen(false)}
onClose={() => setIsQuizShown(false)}
disableScrollLock
/>
</ThemeProvider>,
document.body

@ -0,0 +1,25 @@
import { Box } from "@mui/material";
export default function BannerIcon() {
return (
<Box
sx={{
width: "80px",
height: "76px",
display: "flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
}}
>
<svg width="auto" height="auto" viewBox="0 0 20 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.2979 2.94922H15.4949C15.6488 2.94922 15.7964 3.01036 15.9052 3.11919C16.0141 3.22802 16.0752 3.37563 16.0752 3.52954V6.77848M7.21163 2.94922H5.04907C4.89516 2.94922 4.74755 3.01036 4.63872 3.11919C4.52989 3.22802 4.46875 3.37563 4.46875 3.52954V15.7163C4.46875 15.8702 4.52989 16.0178 4.63872 16.1267C4.74755 16.2355 4.89516 16.2966 5.04907 16.2966H8.53802M7.95068 16.2966H15.4949C15.6488 16.2966 15.7964 16.2355 15.9052 16.1267C16.0141 16.0178 16.0752 15.8702 16.0752 15.7163V11.9923" stroke="white" strokeWidth="0.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M9.40182 13.7891H7.65735C7.58039 13.7891 7.50659 13.762 7.45217 13.7139C7.39776 13.6659 7.36719 13.6006 7.36719 13.5326V8.14708C7.36719 8.07906 7.39776 8.01383 7.45217 7.96574C7.50659 7.91764 7.58039 7.89062 7.65735 7.89062H9.10815H12.8802C12.9572 7.89062 13.031 7.91764 13.0854 7.96574C13.1398 8.01383 13.1704 8.07906 13.1704 8.14708V9.58283" stroke="white" strokeWidth="0.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M7.36719 1.8125H13.1704V3.39705C13.1704 3.71756 12.9106 3.97737 12.5901 3.97737H7.94751C7.62701 3.97737 7.36719 3.71756 7.36719 3.39705V1.8125Z" stroke="white" strokeWidth="0.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M17.0844 8.36719L11.8615 13.5901L9.25 10.9786" stroke="white" strokeWidth="0.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</Box>
);
}