add banner widget features
This commit is contained in:
parent
ab6f58066e
commit
4e0bdb6f3f
@ -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
|
||||
|
25
src/widgets/shared/BannerIcon.tsx
Normal file
25
src/widgets/shared/BannerIcon.tsx
Normal file
@ -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>
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue
Block a user