add widgets

This commit is contained in:
nflnkr 2024-04-24 18:56:11 +03:00
parent 2ee1a72259
commit 5f04a9d198
15 changed files with 596 additions and 64 deletions

@ -1,22 +1,23 @@
import { getQuizData } from "@/api/quizRelase";
import { QuizViewContext, createQuizViewStore } from "@/stores/quizView";
import LoadingSkeleton from "@/ui_kit/LoadingSkeleton";
import { QuizDataContext } from "@contexts/QuizDataContext";
import { RootContainerWidthContext } from "@contexts/RootContainerWidthContext";
import { QuizSettings } from "@model/settingsData";
import { Box, CssBaseline, ThemeProvider } from "@mui/material";
import ScopedCssBaseline from "@mui/material/ScopedCssBaseline";
import { LocalizationProvider } from "@mui/x-date-pickers";
import { AdapterMoment } from "@mui/x-date-pickers/AdapterMoment";
import { ruRU } from '@mui/x-date-pickers/locales';
import { ruRU } from "@mui/x-date-pickers/locales";
import { handleComponentError } from "@utils/handleComponentError";
import lightTheme from "@utils/themes/light";
import moment from "moment";
import { SnackbarProvider } from 'notistack';
import { SnackbarProvider } from "notistack";
import { startTransition, useEffect, useLayoutEffect, useRef, useState } from "react";
import { ErrorBoundary } from "react-error-boundary";
import useSWR from "swr";
import { ApologyPage } from "./ViewPublicationPage/ApologyPage";
import ViewPublicationPage from "./ViewPublicationPage/ViewPublicationPage";
import { QuizViewContext, createQuizViewStore } from "@/stores/quizView";
moment.locale("ru");
@ -28,9 +29,10 @@ type Props = {
preview?: boolean;
changeFaviconAndTitle?: boolean;
className?: string;
disableGlobalCss?: boolean;
};
export default function QuizAnswerer({ quizSettings, quizId, preview = false, changeFaviconAndTitle = true, className }: Props) {
function QuizAnswererInner({ quizSettings, quizId, preview = false, changeFaviconAndTitle = true, className, disableGlobalCss = false }: Props) {
const [quizViewStore] = useState(createQuizViewStore);
const [rootContainerWidth, setRootContainerWidth] = useState<number>(() => window.innerWidth);
const rootContainerRef = useRef<HTMLDivElement>(null);
@ -58,63 +60,71 @@ export default function QuizAnswerer({ quizSettings, quizId, preview = false, ch
};
}, []);
if (isLoading) return (
<ThemeProvider theme={lightTheme}>
<LoadingSkeleton />
</ThemeProvider>
);
if (error) return (
<ThemeProvider theme={lightTheme}>
<ApologyPage error={error} />
</ThemeProvider>
);
if (isLoading) return <LoadingSkeleton />;
if (error) return <ApologyPage error={error} />;
quizSettings ??= data;
if (!quizSettings) throw new Error("Quiz data is null");
if (quizSettings.questions.length === 0) return (
<ThemeProvider theme={lightTheme}>
<ApologyPage error={new Error("No questions found")} />
</ThemeProvider>
);
if(!quizId) return (
<ThemeProvider theme={lightTheme}>
<ApologyPage error={error} />
</ThemeProvider>
);
if (quizSettings.questions.length === 0) return <ApologyPage error={new Error("No questions found")} />;
if (!quizId) return <ApologyPage error={new Error("No quiz id")} />;
const quizContainer = (
<Box
ref={rootContainerRef}
className={className}
sx={{
width: "100%",
height: "100%",
position: "relative",
}}
>
<ErrorBoundary
FallbackComponent={ApologyPage}
onError={handleComponentError}
>
<ViewPublicationPage />
</ErrorBoundary>
</Box>
);
return (
<QuizViewContext.Provider value={quizViewStore}>
<RootContainerWidthContext.Provider value={rootContainerWidth}>
<QuizDataContext.Provider value={{ ...quizSettings, quizId, preview, changeFaviconAndTitle }}>
<LocalizationProvider dateAdapter={AdapterMoment} adapterLocale="ru" localeText={localeText}>
<ThemeProvider theme={lightTheme}>
<SnackbarProvider
preventDuplicate={true}
style={{ backgroundColor: lightTheme.palette.brightPurple.main }}
>
<CssBaseline />
<Box
ref={rootContainerRef}
className={className}
sx={{
width: "100%",
height: "100%",
}}
>
<ErrorBoundary
FallbackComponent={ApologyPage}
onError={handleComponentError}
>
<ViewPublicationPage />
</ErrorBoundary>
</Box>
</SnackbarProvider>
</ThemeProvider>
</LocalizationProvider>
{disableGlobalCss ? (
<ScopedCssBaseline
sx={{
height: "100%",
width: "100%",
backgroundColor: "transparent",
}}
>
{quizContainer}
</ScopedCssBaseline>
) : (
<CssBaseline>
{quizContainer}
</CssBaseline>
)}
</QuizDataContext.Provider>
</RootContainerWidthContext.Provider>
</QuizViewContext.Provider>
);
}
export default function QuizAnswerer(props: Props) {
return (
<LocalizationProvider dateAdapter={AdapterMoment} adapterLocale="ru" localeText={localeText}>
<ThemeProvider theme={lightTheme}>
<SnackbarProvider
preventDuplicate={true}
style={{ backgroundColor: lightTheme.palette.brightPurple.main }}
>
<QuizAnswererInner {...props} />
</SnackbarProvider>
</ThemeProvider>
</LocalizationProvider>
);
}

@ -9,7 +9,7 @@ export const ApologyPage = ({ error }: Props) => {
if (error.response?.data === "quiz is inactive") message = "Квиз не активирован";
if (error.message === "No questions found") message = "Нет созданных вопросов";
if (error.message === "Quiz already completed") message = "Вы уже прошли этот опрос";
if (error.message === "No questions found") message = "Вопросы отсутствуют";
if (error.message === "No quiz id") message = "Отсутствует id квиза";
if (error.response?.data === "Invalid request data") message = "Такого квиза не существует";
return (

@ -6,10 +6,11 @@ let domain = "https://hbpn.link";
const currentDomain = location.hostname;
if (
currentDomain === "s.hbpn.link" ||
//Исключение - туризм. Он на стейджинговом квизе и на чужом для публикации домене
currentDomain === "tourism.pena.digital" ||
currentDomain.includes("localhost")
currentDomain === "s.hbpn.link"
//Исключение - туризм. Он на стейджинговом квизе и на чужом для публикации домене
|| currentDomain === "tourism.pena.digital"
|| currentDomain.includes("localhost")
|| currentDomain.includes("127.0.0.1")
) domain = "https://s.hbpn.link";
export { domain };

@ -1,9 +1,10 @@
import QuizAnswerer from "@/components/QuizAnswerer";
import { Root, createRoot } from "react-dom/client";
import { createRoot } from "react-dom/client";
// eslint-disable-next-line react-refresh/only-export-components
export * from "./widgets";
let root: Root | undefined = undefined;
// old widget
const widget = {
create({ selector, quizId, changeFaviconAndTitle = true }: {
selector: string;
@ -13,13 +14,16 @@ const widget = {
const element = document.getElementById(selector);
if (!element) throw new Error("Element for widget doesn't exist");
root = createRoot(element);
const root = createRoot(element);
root.render(<QuizAnswerer quizId={quizId} changeFaviconAndTitle={changeFaviconAndTitle} />);
root.render(
<QuizAnswerer
quizId={quizId}
changeFaviconAndTitle={changeFaviconAndTitle}
disableGlobalCss
/>
);
},
unmount() {
if (root) root.unmount();
}
};
export default widget;

@ -0,0 +1,35 @@
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 (
<Dialog
open={open}
onClose={onClose}
keepMounted
PaperProps={{
sx: {
backgroundColor: "transparent",
width: "calc(min(100%, max(70%, 700px)))",
height: "80%",
maxWidth: "100%",
m: "16px",
}
}}
>
<QuizAnswerer
quizId={quizId}
changeFaviconAndTitle={false}
disableGlobalCss
/>
</Dialog>
);
}

@ -0,0 +1,29 @@
import { Root, createRoot } from "react-dom/client";
import QuizBanner from "./QuizBanner";
import { ComponentPropsWithoutRef } from "react";
export class BannerWidget {
root: Root | undefined;
element = document.createElement("div");
constructor({ quizId, position }: ComponentPropsWithoutRef<typeof QuizBanner>) {
this.element.style.setProperty("display", "none");
document.body.appendChild(this.element);
this.root = createRoot(this.element);
this.root.render(
<QuizBanner
quizId={quizId}
position={position}
onClose={() => this.destroy()}
/>
);
}
destroy() {
if (this.root) this.root.unmount();
this.element.remove();
}
}

@ -0,0 +1,82 @@
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 { createPortal } from "react-dom";
import QuizDialog from "../QuizDialog";
const PADDING = 10;
interface Props {
position: "topleft" | "topright" | "bottomleft" | "bottomright";
quizId: string;
onClose: () => void;
}
export default function QuizBanner({ quizId, position, onClose }: Props) {
const [isQuizDialogOpen, setIsQuizDialogOpen] = useState<boolean>(false);
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%",
}}
>
Пройти квиз
</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>
<QuizDialog
open={isQuizDialogOpen}
quizId={quizId}
onClose={() => setIsQuizDialogOpen(false)}
/>
</ThemeProvider>,
document.body
);
}

@ -0,0 +1,31 @@
import { Root, createRoot } from "react-dom/client";
import OpenQuizButton from "./OpenQuizButton";
import { ComponentPropsWithoutRef } from "react";
export class ButtonWidget {
root: Root | undefined;
element = document.createElement("div");
constructor({ quizId, selector, fixedSide }: ComponentPropsWithoutRef<typeof OpenQuizButton>) {
if (!fixedSide && !selector) throw new Error("ButtonWidget: Either selector or fixedSide params must be provided");
this.element.style.setProperty("display", "none");
document.body.appendChild(this.element);
this.root = createRoot(this.element);
this.root.render(
<OpenQuizButton
selector={selector}
fixedSide={fixedSide}
quizId={quizId}
/>
);
}
destroy() {
if (this.root) this.root.unmount();
this.element.remove();
}
}

@ -0,0 +1,55 @@
import lightTheme from "@/utils/themes/light";
import { Button, ThemeProvider } from "@mui/material";
import { useState } from "react";
import { createPortal } from "react-dom";
import QuizDialog from "../QuizDialog";
interface Props {
selector?: string;
fixedSide?: "left" | "right";
quizId: string;
}
export default function OpenQuizButton({ selector, quizId, fixedSide }: Props) {
const [isQuizDialogOpen, setIsQuizDialogOpen] = useState<boolean>(false);
const portalContainer = !fixedSide && selector ? document.getElementById(selector)! : document.body;
return createPortal(
<ThemeProvider theme={lightTheme}>
<Button
className="pena-quiz-widget-button"
onClick={() => setIsQuizDialogOpen(p => !p)}
variant="contained"
sx={[
{
// generic styles
},
Boolean(fixedSide) && {
position: "fixed",
bottom: "50%",
},
fixedSide === "left" && {
left: 0,
transformOrigin: "left",
transform: "rotate(-90deg) translateY(50%) translateX(-50%)",
},
fixedSide === "right" && {
right: 0,
transformOrigin: "right",
transform: "rotate(-90deg) translateY(-50%) translateX(50%)",
},
]}
>
Пройти квиз
</Button>
<QuizDialog
open={isQuizDialogOpen}
quizId={quizId}
onClose={() => setIsQuizDialogOpen(false)}
/>
</ThemeProvider>,
portalContainer
);
}

@ -0,0 +1,29 @@
import QuizAnswerer from "@/components/QuizAnswerer";
import { Root, createRoot } from "react-dom/client";
export class ContainerWidget {
root: Root | undefined;
constructor({ selector, quizId }: {
quizId: string;
selector: string;
}) {
const element = document.getElementById(selector);
if (!element) throw new Error("Element for widget doesn't exist");
this.root = createRoot(element);
this.root.render(
<QuizAnswerer
quizId={quizId}
changeFaviconAndTitle={false}
disableGlobalCss
/>
);
}
destroy() {
if (this.root) this.root.unmount();
}
}

5
src/widgets/index.ts Normal file

@ -0,0 +1,5 @@
export * from "./banner/BannerWidget";
export * from "./button/ButtonWidget";
export * from "./container/ContainerWidget";
export * from "./popup/PopupWidget";
export * from "./side/SideWidget";

@ -0,0 +1,30 @@
import { Root, createRoot } from "react-dom/client";
import QuizDialog from "../QuizDialog";
export class PopupWidget {
root: Root | undefined;
element: HTMLDivElement;
constructor({ quizId }: {
quizId: string;
}) {
this.element = document.createElement("div");
this.element.style.setProperty("display", "none");
document.body.appendChild(this.element);
this.root = createRoot(this.element);
this.root.render(
<QuizDialog
quizId={quizId}
onClose={() => this.destroy()}
/>
);
}
destroy() {
if (this.root) this.root.unmount();
this.element.remove();
}
}

@ -0,0 +1,73 @@
import { QuizAnswerer } from "@/index";
import lightTheme from "@/utils/themes/light";
import { Box, Button, Grow, ThemeProvider } from "@mui/material";
import { useState } from "react";
import { createPortal } from "react-dom";
const PADDING = 10;
interface Props {
quizId: string;
position: "left" | "right";
}
export default function QuizSideButton({ quizId, position }: Props) {
const [isQuizShown, setIsQuizShown] = useState<boolean>(false);
return createPortal(
<ThemeProvider theme={lightTheme}>
{isQuizShown ? (
<Grow in={true}>
<Box
sx={[
{
position: "fixed",
height: `calc(min(calc(100% - ${PADDING * 2}px), 800px))`,
width: `calc(min(calc(100% - ${PADDING * 2}px), 600px))`,
},
position === "left" && {
bottom: PADDING,
left: PADDING,
},
position === "right" && {
bottom: PADDING,
right: PADDING,
},
]}
>
<QuizAnswerer
quizId={quizId}
changeFaviconAndTitle={false}
disableGlobalCss
/>
</Box>
</Grow>
) : (
<Button
className="pena-quiz-widget-button"
variant="contained"
onClick={() => setIsQuizShown(true)}
sx={[
{
position: "fixed",
height: "70px",
width: `calc(min(calc(100% - ${PADDING * 2}px), 600px))`,
},
position === "left" && {
bottom: PADDING,
left: PADDING,
},
position === "right" && {
bottom: PADDING,
right: PADDING,
},
]}
>
Пройти квиз
</Button>
)}
</ThemeProvider>,
document.body
);
}

@ -0,0 +1,28 @@
import { Root, createRoot } from "react-dom/client";
import QuizSideButton from "./QuizSideButton";
import { ComponentPropsWithoutRef } from "react";
export class SideWidget {
root: Root | undefined;
element = document.createElement("div");
constructor({ quizId, position }: ComponentPropsWithoutRef<typeof QuizSideButton>) {
this.element.style.setProperty("display", "none");
document.body.appendChild(this.element);
this.root = createRoot(this.element);
this.root.render(
<QuizSideButton
quizId={quizId}
position={position}
/>
);
}
destroy() {
if (this.root) this.root.unmount();
this.element.remove();
}
}

120
widget-test.html Normal file

@ -0,0 +1,120 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="theme-color" content="#000000" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Quiz</title>
<style>
#widget-container {
width: 400px;
height: 300px;
}
p {
font-size: x-large;
}
</style>
</head>
<body>
<p>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</p>
<p>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</p>
<p>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</p>
<!-- <div id="widget-container"></div> -->
<div id="button-container"></div>
<p>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</p>
<p>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</p>
<p>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</p>
<p>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</p>
<p>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</p>
<p>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</p>
<p>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</p>
<p>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</p>
<p>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</p>
<p>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</p>
<p>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</p>
<!-- <script type="module">
import { ContainerWidget } from "./widget/widget.js";
new ContainerWidget({
selector: "widget-container",
quizId: "3c49550d-8c77-4788-bc2d-42586a261514",
});
</script> -->
<!-- <script type="module">
import { PopupWidget } from "./widget/widget.js";
new PopupWidget({
quizId: "3c49550d-8c77-4788-bc2d-42586a261514",
});
</script> -->
<!-- <script type="module">
import { ButtonWidget } from "./widget/widget.js";
new ButtonWidget({
quizId: "3c49550d-8c77-4788-bc2d-42586a261514",
fixedSide: "right",
});
</script> -->
<!-- <script type="module">
import { BannerWidget } from "./widget/widget.js";
new BannerWidget({
quizId: "3c49550d-8c77-4788-bc2d-42586a261514",
position: "bottomright",
});
</script> -->
<script type="module">
import { SideWidget } from "./widget/widget.js";
new SideWidget({
quizId: "3c49550d-8c77-4788-bc2d-42586a261514",
position: "right",
});
</script>
</body>
</html>