Merge branch 'dev' into 'staging'

Dev

See merge request frontend/squzanswerer!123
This commit is contained in:
Nastya 2024-04-26 06:33:13 +00:00
commit 0b0c7fbdce
26 changed files with 745 additions and 125 deletions

@ -38,7 +38,13 @@ const DeviceType = device.type;
let Device = md.mobile();
if (Device === null) { Device = userAgent; }
export const publicationMakeRequest = ({ url, body }: any) => {
type PublicationMakeRequestParams = {
url: string;
body: FormData;
method: "POST";
}
export const publicationMakeRequest = ({ url, body }: PublicationMakeRequestParams) => {
return axios(url, {
data: body,
headers: {
@ -56,7 +62,7 @@ export const publicationMakeRequest = ({ url, body }: any) => {
export async function getData(quizId: string): Promise<{
data: GetQuizDataResponse | null;
isRecentlyCompleted: boolean;
error?: any;
error?: AxiosError;
}> {
try {
const { data, headers } = await axios<GetQuizDataResponse>(
@ -116,7 +122,14 @@ export async function getQuizData(quizId: string) {
return res;
}
export function sendAnswer({ questionId, body, qid, preview }: any) {
type SendAnswerProps = {
questionId: string;
body: string | string[];
qid: string;
preview: boolean;
}
export function sendAnswer({ questionId, body, qid, preview }: SendAnswerProps) {
if (preview) return;
const formData = new FormData();
@ -138,11 +151,26 @@ export function sendAnswer({ questionId, body, qid, preview }: any) {
}
//body ={file, filename}
export function sendFile({ questionId, body, qid, preview }: any) {
if (preview) return;
type SendFileParams = {
questionId: string;
body: {
name: string;
file: File;
preview: boolean;
};
qid: string;
}
type Answer = {
question_id: string;
content: string;
};
export function sendFile({ questionId, body, qid }: SendFileParams) {
if (body.preview) return;
const formData = new FormData();
const answers: any = [
const answers: Answer[] = [
{
question_id: questionId,
content: "file:" + body.name,
@ -162,7 +190,20 @@ export function sendFile({ questionId, body, qid, preview }: any) {
}
//форма контактов
export function sendFC({ questionId, body, qid, preview }: any) {
export type SendFCParams = {
questionId: string;
body: {
name?: string;
email?: string;
phone?: string;
address?: string;
customs?: Record<string, string>;
};
qid: string;
preview: boolean;
}
export function sendFC({ questionId, body, qid, preview }: SendFCParams) {
if (preview) return;
const formData = new FormData();

@ -4,7 +4,7 @@ type InfoProps = {
width?: number;
height?: number;
sx?: SxProps;
onClick?: any;
onClick?: () => void;
className?: string;
color?: string
};

@ -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 (

@ -1,9 +1,11 @@
import { FC, useRef, useState, useEffect } from "react";
import AddressIcon from "@icons/ContactFormIcon/AddressIcon";
import EmailIcon from "@icons/ContactFormIcon/EmailIcon";
import NameIcon from "@icons/ContactFormIcon/NameIcon";
import PhoneIcon from "@icons/ContactFormIcon/PhoneIcon";
import TextIcon from "@icons/ContactFormIcon/TextIcon";
import {
FC,
useRef,
useState,
useEffect,
Dispatch,
SetStateAction,
} from "react";
import {
Box,
Button,
@ -14,11 +16,18 @@ import {
Typography,
useTheme,
} from "@mui/material";
import { useIMask } from "react-imask";
import CustomCheckbox from "@ui_kit/CustomCheckbox";
import AddressIcon from "@icons/ContactFormIcon/AddressIcon";
import EmailIcon from "@icons/ContactFormIcon/EmailIcon";
import NameIcon from "@icons/ContactFormIcon/NameIcon";
import PhoneIcon from "@icons/ContactFormIcon/PhoneIcon";
import TextIcon from "@icons/ContactFormIcon/TextIcon";
import { DESIGN_LIST } from "@/utils/designList";
import { sendFC } from "@api/quizRelase";
import { sendFC, SendFCParams } from "@api/quizRelase";
import { useQuizData } from "@contexts/QuizDataContext";
import { NameplateLogo } from "@icons/NameplateLogo";
import { QuizQuestionResult } from "@model/questionTypes/result";
@ -26,6 +35,32 @@ import { AnyTypedQuizQuestion } from "@model/questionTypes/shared";
import { quizThemes } from "@utils/themes/Publication/themePublication";
import { enqueueSnackbar } from "notistack";
import { useRootContainerSize } from "../../contexts/RootContainerWidthContext";
import {
FormContactFieldData,
FormContactFieldName,
} from "@model/settingsData.ts";
type InputProps = {
title: string;
desc: string;
Icon: FC<{ color: string; backgroundColor: string }>;
onChange: TextFieldProps["onChange"];
id: string;
mask?: string;
};
type InputsProps = {
name: string;
setName: Dispatch<SetStateAction<string>>;
email: string;
setEmail: Dispatch<SetStateAction<string>>;
phone: string;
setPhone: Dispatch<SetStateAction<string>>;
text: string;
setText: Dispatch<SetStateAction<string>>;
adress: string;
setAdress: Dispatch<SetStateAction<string>>;
};
const TextField = MuiTextField as unknown as FC<TextFieldProps>; // temporary fix ts(2590)
const EMAIL_REGEXP =
@ -86,7 +121,7 @@ export const ContactForm = ({ currentQuestion, onShowResult }: Props) => {
const inputHC = async () => {
const FC = settings.cfg.formContact.fields || settings.cfg.formContact;
const body = {} as any;
const body: SendFCParams["body"] = {};
if (name.length > 0) body.name = name;
if (email.length > 0) body.email = email;
if (phone.length > 0) body.phone = phone;
@ -113,19 +148,21 @@ export const ContactForm = ({ currentQuestion, onShowResult }: Props) => {
}
};
const FCcopy: any =
const FCcopy: Record<FormContactFieldName, FormContactFieldData> =
settings.cfg.formContact.fields || settings.cfg.formContact;
const filteredFC: any = {};
const filteredFC: Partial<
Record<FormContactFieldName, FormContactFieldData>
> = {};
for (const i in FCcopy) {
const field = FCcopy[i];
const field = FCcopy[i as keyof typeof FCcopy];
if (field.used) {
filteredFC[i] = field;
filteredFC[i as FormContactFieldName] = field;
}
}
async function handleShowResultsClick() {
const FC: any = settings.cfg.formContact.fields;
const FC = settings.cfg.formContact.fields;
if (FC["email"].used !== EMAIL_REGEXP.test(email)) {
return enqueueSnackbar("введена некорректная почта");
}
@ -145,9 +182,7 @@ export const ContactForm = ({ currentQuestion, onShowResult }: Props) => {
try {
await inputHC();
fireOnce.current = false;
const sessions: any = JSON.parse(
localStorage.getItem("sessions") || "{}"
);
const sessions = JSON.parse(localStorage.getItem("sessions") || "{}");
sessions[quizId] = Date.now();
localStorage.setItem("sessions", JSON.stringify(sessions));
enqueueSnackbar("Данные успешно отправлены");
@ -389,7 +424,7 @@ const Inputs = ({
setText,
adress,
setAdress,
}: any) => {
}: InputsProps) => {
const { settings } = useQuizData();
const FC = settings.cfg.formContact.fields;
@ -421,6 +456,7 @@ const Inputs = ({
title={FC["phone"].innerText || "Введите номер телефона"}
desc={FC["phone"].text || "Номер телефона"}
Icon={PhoneIcon}
mask="+7 (000) 000-00-00"
/>
);
const Text = (
@ -463,22 +499,12 @@ const Inputs = ({
}
};
const CustomInput = ({
title,
desc,
Icon,
onChange,
id,
}: {
id: string;
title: string;
desc: string;
Icon: FC<{ color: string; backgroundColor: string }>;
onChange: TextFieldProps["onChange"];
}) => {
const CustomInput = ({ title, desc, Icon, onChange, mask }: InputProps) => {
const theme = useTheme();
const isMobile = useRootContainerSize() < 600;
const { settings } = useQuizData();
const { ref } = useIMask({ mask });
return (
<Box m="10px 0">
<Typography mb="7px" color={theme.palette.text.primary}>
@ -486,6 +512,7 @@ const CustomInput = ({
</Typography>
<TextField
inputRef={ref}
onChange={onChange}
sx={{
width: isMobile ? "300px" : "390px",

@ -21,7 +21,7 @@ import { useRootContainerSize } from "../../../contexts/RootContainerWidthContex
import type { QuizQuestionFile } from "../../../model/questionTypes/file";
import { ACCEPT_SEND_FILE_TYPES_MAP, MAX_FILE_SIZE, UPLOAD_FILE_DESCRIPTIONS_MAP } from "../tools/fileUpload";
type ModalWarningType = "errorType" | "errorSize" | "picture" | "video" | "audio" | "document" | null;
export type ModalWarningType = "errorType" | "errorSize" | "picture" | "video" | "audio" | "document" | null;
type FileProps = {
currentQuestion: QuizQuestionFile;

@ -8,7 +8,7 @@ import {
import CustomTextField from "@ui_kit/CustomTextField";
import { useQuizViewStore } from "@stores/quizView";
import {Answer, useQuizViewStore} from "@stores/quizView";
import { sendAnswer } from "@api/quizRelase";
import { useQuizData } from "@contexts/QuizDataContext";
@ -114,7 +114,7 @@ export const Text = ({ currentQuestion, stepNumber }: TextProps) => {
interface Props {
currentQuestion: QuizQuestionText;
answer: any;
answer?: Answer;
inputHC: (a: string) => void;
stepNumber?: number | null;
}

@ -118,7 +118,7 @@ export type FormContactFieldName =
| "text"
| "address";
type FormContactFieldData = {
export type FormContactFieldData = {
text: string;
innerText: string;
key: string;

@ -6,9 +6,11 @@ import { createContext, useContext } from "react";
import { createStore, useStore } from "zustand";
import { immer } from "zustand/middleware/immer";
export type Answer = string | string[] | Moment;
type QuestionAnswer = {
questionId: string;
answer: string | string[] | Moment;
answer: Answer
};
type OwnVariant = {
@ -99,4 +101,4 @@ export const createQuizViewStore = () => createStore<QuizViewStore & QuizViewAct
},
})
)
);
);

@ -2,13 +2,14 @@ import { FormControl, TextField as MuiTextField, SxProps, Theme, useTheme } from
import type { InputProps, TextFieldProps } from "@mui/material";
import type { ChangeEvent, FC, FocusEvent, KeyboardEvent } from "react";
import {Answer} from "@stores/quizView.ts";
const TextField = MuiTextField as unknown as FC<TextFieldProps>; // temporary fix ts(2590)
interface CustomTextFieldProps {
placeholder: string;
value?: string;
value?: Answer;
error?: string;
onChange?: (event: ChangeEvent<HTMLInputElement>) => void;
onKeyDown?: (event: KeyboardEvent<HTMLInputElement>) => void;

@ -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 };
export { domain };

@ -89,6 +89,7 @@
"country-flag-emoji-polyfill": "^0.1.8",
"current-device": "^0.10.2",
"hex-rgb": "^5.0.0",
"mobile-detect": "^1.4.5"
"mobile-detect": "^1.4.5",
"react-imask": "^7.6.0"
}
}

@ -1,13 +0,0 @@
import QuizAnswerer from "../lib/components/QuizAnswerer";
interface Props {
quizId: string;
}
export default function WidgetApp({ quizId }: Props) {
return (
<QuizAnswerer quizId={quizId} />
);
}

@ -1,24 +1,29 @@
import { Root, createRoot } from "react-dom/client";
import WidgetApp from "./WidgetApp";
import QuizAnswerer from "@/components/QuizAnswerer";
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 }: {
create({ selector, quizId, changeFaviconAndTitle = true }: {
selector: string;
quizId: string;
changeFaviconAndTitle: boolean;
}) {
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(<WidgetApp quizId={quizId} />);
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.querySelector(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.querySelector(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>

@ -184,6 +184,14 @@
dependencies:
"@babel/helper-plugin-utils" "^7.22.5"
"@babel/runtime-corejs3@^7.24.4":
version "7.24.4"
resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.24.4.tgz#b9ebe728087cfbb22bbaccc6f9a70d69834124a0"
integrity sha512-VOQOexSilscN24VEY810G/PqtpFvx/z6UqDIjIWbDe2368HhDLkYN5TYwaEz/+eRCUkhJ2WaNLLmQAlxzfWj4w==
dependencies:
core-js-pure "^3.30.2"
regenerator-runtime "^0.14.0"
"@babel/runtime@^7.12.5", "@babel/runtime@^7.18.3", "@babel/runtime@^7.23.2", "@babel/runtime@^7.23.8", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.7":
version "7.23.8"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.8.tgz#8ee6fe1ac47add7122902f257b8ddf55c898f650"
@ -1563,6 +1571,11 @@ convert-source-map@^2.0.0:
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a"
integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==
core-js-pure@^3.30.2:
version "3.37.0"
resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.37.0.tgz#ce99fb4a7cec023fdbbe5b5bd1f06bbcba83316e"
integrity sha512-d3BrpyFr5eD4KcbRvQ3FTUx/KWmaDesr7+a3+1+P46IUnNoEt+oiLijPINZMEon7w9oGkIINWxrBAU9DEciwFQ==
core-util-is@1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
@ -2315,6 +2328,13 @@ ignore@^5.2.0, ignore@^5.2.4:
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.0.tgz#67418ae40d34d6999c95ff56016759c718c82f78"
integrity sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==
imask@^7.6.0:
version "7.6.0"
resolved "https://registry.yarnpkg.com/imask/-/imask-7.6.0.tgz#ed071748cfdf6b12ac153f69878e08c4333df984"
integrity sha512-6EHsq1q7v5+M4Vas2MGrs2oRpxPRWPwPDiL0HmG1ikBI/0hOwvkxRhVRFQnWIlZcTG7R8iw0az5V+z868qnQ9A==
dependencies:
"@babel/runtime-corejs3" "^7.24.4"
immer@^10.0.3:
version "10.0.3"
resolved "https://registry.yarnpkg.com/immer/-/immer-10.0.3.tgz#a8de42065e964aa3edf6afc282dfc7f7f34ae3c9"
@ -2984,6 +3004,14 @@ react-error-boundary@^4.0.12:
dependencies:
"@babel/runtime" "^7.12.5"
react-imask@^7.6.0:
version "7.6.0"
resolved "https://registry.yarnpkg.com/react-imask/-/react-imask-7.6.0.tgz#5948fc39e1d7d036292d4fade43df4636d43e7b7"
integrity sha512-SilPct67Xw4TN+dqn3SM4BVpy+FwNSeT0wblA/DXQ3El2KPBEWwrn4x3gQ39ZohFAphp7yG7w6gSKq5SeR/6Kg==
dependencies:
imask "^7.6.0"
prop-types "^15.8.1"
react-is@^16.13.1, react-is@^16.7.0:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"