diff --git a/Dockerfile b/Dockerfile index 7c04d70e..59651d48 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20.10-alpine3.18 as build +FROM penahub.gitlab.yandexcloud.net:5050/devops/dockerhub-backup/node as build RUN apk update && rm -rf /var/cache/apk/* WORKDIR /usr/app @@ -13,7 +13,7 @@ RUN yarn install --ignore-scripts --non-interactive --frozen-lockfile && yarn ca RUN yarn build -FROM nginx:latest as result +FROM penahub.gitlab.yandexcloud.net:5050/devops/dockerhub-backup/nginx as result WORKDIR /usr/share/nginx/html COPY --from=build /usr/app/build/ /usr/share/nginx/html COPY hub.conf /etc/nginx/conf.d/default.conf diff --git a/deployments/staging/docker-compose.yaml b/deployments/staging/docker-compose.yaml index 5cc81780..85b4d792 100644 --- a/deployments/staging/docker-compose.yaml +++ b/deployments/staging/docker-compose.yaml @@ -1,3 +1,4 @@ +version: "3" services: squiz: container_name: squiz diff --git a/package.json b/package.json index af12ffe6..c9875f6c 100755 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "@emotion/react": "^11.10.5", "@emotion/styled": "^11.10.5", "@frontend/kitui": "^1.0.82", - "@frontend/squzanswerer": "^1.0.38", + "@frontend/squzanswerer": "^1.0.51", "@mui/icons-material": "^5.10.14", "@mui/material": "^5.10.14", "@mui/x-charts": "^6.19.5", @@ -29,7 +29,6 @@ "cytoscape": "^3.26.0", "cytoscape-popper": "^2.0.0", "date-fns": "^3.0.6", - "dayjs": "^1.11.10", "emoji-mart": "^5.5.2", "file-saver": "^2.0.5", "formik": "^2.4.5", @@ -41,6 +40,7 @@ "notistack": "^3.0.1", "react": "^18.2.0", "react-beautiful-dnd": "^13.1.1", + "react-colorful": "^5.6.1", "react-cytoscapejs": "^2.0.0", "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", @@ -55,6 +55,7 @@ "react-scripts": "5.0.1", "react-slick": "^0.29.0", "slick-carousel": "^1.8.1", + "swiper": "^11.1.4", "swr": "^2.2.4", "typescript": "^5.2.2", "use-debounce": "^9.0.4", @@ -68,7 +69,7 @@ "test": "craco test", "eject": "craco eject", "cypress:open": "cypress open", - "code:format": "prettier ./src --write --ignore-unknown", + "code:format": "prettier --write --ignore-unknown", "prepare": "husky install" }, "browserslist": { @@ -97,5 +98,18 @@ }, "lint-staged": { "**/*": "yarn code:format" + }, + "prettier": { + "semi": true, + "trailingComma": "es5", + "singleQuote": false, + "printWidth": 120, + "tabWidth": 2, + "useTabs": false, + "endOfLine": "auto", + "bracketSpacing": true, + "arrowParens": "always", + "jsxSingleQuote": false, + "singleAttributePerLine": true } } diff --git a/prettierrc b/prettierrc deleted file mode 100644 index 80e56165..00000000 --- a/prettierrc +++ /dev/null @@ -1,12 +0,0 @@ -{ - "semi": true, - "trailingComma": "es5", - "singleQuote": false, - "printWidth": 120, - "tabWidth": 2, - "useTabs": false, - "endOfLine": "auto", - "bracketSpacing": true, - "arrowParens": "always", - "jsxSingleQuote": false -} diff --git a/public/index.html b/public/index.html index b9522be0..42fbc250 100755 --- a/public/index.html +++ b/public/index.html @@ -1,12 +1,16 @@ + + - - - - Pena Quiz - - + + Повышение конкурентоспособности " + /> - - - + + + + + - + - - - - - - - - - - - - - - -
- + // + var _tmr = window._tmr || (window._tmr = []); + _tmr.push({ + id: "3513005", + type: "pageView", + start: new Date().getTime(), + }); + (function (d, w, id) { + if (d.getElementById(id)) return; + var ts = d.createElement("script"); + ts.type = "text/javascript"; + ts.async = true; + ts.id = id; + ts.src = "https://top-fwz1.mail.ru/js/code.js"; + var f = function () { + var s = d.getElementsByTagName("script")[0]; + s.parentNode.insertBefore(ts, s); + }; + if (w.opera == "[object Opera]") { + d.addEventListener("DOMContentLoaded", f, false); + } else { + f(); + } + })(document, window, "tmr-code"); + // + } + if (domain === "squiz.pena.digital") { + ym(96979625, "init", { + clickmap: true, + trackLinks: true, + accurateTrackBounce: true, + webvisor: true, + }); + } + if (domain === "penaquiz.online" || domain === "penaquiz.ru") { + ym(97241101, "init", { + clickmap: true, + trackLinks: true, + accurateTrackBounce: true, + webvisor: true, + }); + } + + + + + + +
+ diff --git a/src/App.tsx b/src/App.tsx index 1ce92906..4e14a611 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,68 +1,39 @@ -import type { SuspenseProps } from "react"; -import { lazy, Suspense, useEffect, useLayoutEffect, useRef } from "react"; -import { lazily } from "react-lazily"; +import { clearAuthToken, getMessageFromFetchError, UserAccount, useUserFetcher } from "@frontend/kitui"; +import type { OriginalUserAccount } from "@root/user"; +import { clearUserData, setCustomerAccount, setUser, setUserAccount, useUserStore } from "@root/user"; import ContactFormModal from "@ui_kit/ContactForm"; -import dayjs from "dayjs"; -import "dayjs/locale/ru"; +import FloatingSupportChat from "@ui_kit/FloatingSupportChat"; +import PrivateRoute from "@ui_kit/PrivateRoute"; +import { useAfterpay } from "@utils/hooks/useAfterpay"; +import { useUserAccountFetcher } from "@utils/hooks/useUserAccountFetcher"; +import { enqueueSnackbar } from "notistack"; +import type { SuspenseProps } from "react"; +import { lazy, Suspense } from "react"; +import { lazily } from "react-lazily"; +import { Navigate, Route, Routes, useLocation, useNavigate } from "react-router-dom"; +import { useAmoAccount } from "./api/integration"; +import ListPageDummy from "./components/Dummys/pageDummys/listPageDummy"; +import "./index.css"; +import OutdatedLink from "./pages/auth/OutdatedLink"; +import RecoverPassword from "./pages/auth/RecoverPassword"; +import { Restore } from "./pages/auth/Restore"; import SigninDialog from "./pages/auth/Signin"; import SignupDialog from "./pages/auth/Signup"; -import { - Navigate, - Route, - Routes, - useLocation, - useNavigate, -} from "react-router-dom"; -import "./index.css"; +import { InfoPrivilege } from "./pages/InfoPrivilege"; +import AmoTokenExpiredDialog from "./pages/IntegrationsPage/IntegrationsModal/AmoTokenExpiredDialog"; import Landing from "./pages/Landing/Landing"; import Main from "./pages/main"; -import { - clearAuthToken, - createUserAccount, - devlog, - getMessageFromFetchError, - UserAccount, - useUserFetcher, -} from "@frontend/kitui"; -import makeRequest from "@api/makeRequest"; -import type { OriginalUserAccount } from "@root/user"; -import { - clearUserData, - setCustomerAccount, - setUser, - setUserAccount, - useUserStore, -} from "@root/user"; -import { enqueueSnackbar } from "notistack"; -import PrivateRoute from "@ui_kit/PrivateRoute"; -import FloatingSupportChat from "@ui_kit/FloatingSupportChat"; - -import { Restore } from "./pages/auth/Restore"; - -import { isAxiosError } from "axios"; -import RecoverPassword from "./pages/auth/RecoverPassword"; -import { InfoPrivilege } from "./pages/InfoPrivilege"; -import OutdatedLink from "./pages/auth/OutdatedLink"; -import { useAfterpay } from "@utils/hooks/useAfterpay"; const MyQuizzesFull = lazy(() => import("./pages/createQuize/MyQuizzesFull")); - +const QuizGallery = lazy(() => import("./pages/createQuize/QuizGallery")); const ViewPage = lazy(() => import("./pages/ViewPublicationPage")); const Analytics = lazy(() => import("./pages/Analytics/Analytics")); const EditPage = lazy(() => import("./pages/startPage/EditPage")); const { Tariffs } = lazily(() => import("./pages/Tariffs/Tariffs")); const { DesignPage } = lazily(() => import("./pages/DesignPage/DesignPage")); -const { IntegrationsPage } = lazily( - () => import("./pages/IntegrationsPage/IntegrationsPage"), -); -const { QuizAnswersPage } = lazily( - () => import("./pages/QuizAnswersPage/QuizAnswersPage"), -); -const ChatImageNewWindow = lazy( - () => import("@ui_kit/FloatingSupportChat/ChatImageNewWindow"), -); - -dayjs.locale("ru"); +const { IntegrationsPage } = lazily(() => import("./pages/IntegrationsPage/IntegrationsPage")); +const { QuizAnswersPage } = lazily(() => import("./pages/QuizAnswersPage/QuizAnswersPage")); +const ChatImageNewWindow = lazy(() => import("@ui_kit/FloatingSupportChat/ChatImageNewWindow")); const routeslink = [ { @@ -92,65 +63,14 @@ const LazyLoading = ({ children, fallback }: SuspenseProps) => ( }>{children} ); -export function useUserAccountFetcher({ - onError, - onNewUserAccount, - url, - userId, -}: { - url: string; - userId: string | null; - onNewUserAccount: (response: T) => void; - onError?: (error: any) => void; -}) { - const onNewUserAccountRef = useRef(onNewUserAccount); - const onErrorRef = useRef(onError); - useLayoutEffect(() => { - onNewUserAccountRef.current = onNewUserAccount; - onErrorRef.current = onError; - }, [onError, onNewUserAccount]); - useEffect(() => { - if (!userId) return; - const controller = new AbortController(); - makeRequest({ - url, - contentType: true, - method: "GET", - useToken: true, - withCredentials: false, - signal: controller.signal, - }) - .then((result) => { - devlog("User account", result); - onNewUserAccountRef.current(result); - }) - .catch((error) => { - devlog("Error fetching user account", error); - if (isAxiosError(error) && error.response?.status === 404) { - createUserAccount(controller.signal, url.replace("get", "create")) - .then((result) => { - devlog("Created user account", result); - onNewUserAccountRef.current(result as T); - }) - .catch((error) => { - devlog("Error creating user account", error); - onErrorRef.current?.(error); - }); - } else { - onErrorRef.current?.(error); - } - }); - return () => controller.abort(); - }, [url, userId]); -} - export default function App() { const userId = useUserStore((state) => state.userId); const location = useLocation(); const navigate = useNavigate(); + const { data: amoAccount } = useAmoAccount(); useUserFetcher({ - url: process.env.REACT_APP_DOMAIN + `/user/${userId}`, + url: `${process.env.REACT_APP_DOMAIN}/user/${userId}`, userId, onNewUser: setUser, onError: (error) => { @@ -164,7 +84,7 @@ export default function App() { }); useUserAccountFetcher({ - url: process.env.REACT_APP_DOMAIN + "/customer/account", + url: `${process.env.REACT_APP_DOMAIN}/customer/v1.0.0/account`, userId, onNewUserAccount: setCustomerAccount, onError: (error) => { @@ -179,7 +99,7 @@ export default function App() { }); useUserAccountFetcher({ - url: process.env.REACT_APP_DOMAIN + "/squiz/account/get", + url: `${process.env.REACT_APP_DOMAIN}/squiz/account/get`, userId, onNewUserAccount: setUserAccount, onError: (error) => { @@ -193,6 +113,8 @@ export default function App() { }, }); + useAfterpay(); + if (location.state?.redirectTo) return ( ); - useAfterpay(); - return ( <> + {amoAccount && } {location.state?.backgroundLocation && ( - } /> - } /> - } /> - } /> - } /> + } + /> + } + /> + } + /> + } + /> + } + /> )} - } /> + } + /> + } /> + } /> + } /> } /> + } />} + /> } />} + element={ + } + fallback={} + /> + } /> } />} /> - } /> + } + /> }> {routeslink.map((e, i) => ( { +): Promise<[RegisterResponse | null, string?]> => { try { const registerResponse = await makeRequest< RegisterRequest, RegisterResponse >({ - url: apiUrl + "/register", + url: `${API_URL}/register`, body: { login, password, phoneNumber }, useToken: false, withCredentials: true, @@ -32,15 +37,15 @@ export async function register( return [null, `Не удалось зарегестрировать аккаунт. ${error}`]; } -} +}; -export async function login( +export const login = async ( login: string, password: string, -): Promise<[LoginResponse | null, string?]> { +): Promise<[LoginResponse | null, string?]> => { try { const loginResponse = await makeRequest({ - url: apiUrl + "/login", + url: `${API_URL}/login`, body: { login, password }, useToken: false, withCredentials: true, @@ -52,13 +57,13 @@ export async function login( return [null, `Не удалось войти. ${error}`]; } -} +}; -export async function logout(): Promise<[unknown, string?]> { +export const logout = async (): Promise<[void | null, string?]> => { try { const logoutResponse = await makeRequest({ - url: apiUrl + "/logout", method: "POST", + url: `${API_URL}/logout`, useToken: true, withCredentials: true, }); @@ -67,30 +72,32 @@ export async function logout(): Promise<[unknown, string?]> { } catch (nativeError) { const [error] = parseAxiosError(nativeError); - return [null]; + return [null, `Не удалось выйти. ${error}`]; } -} +}; -export async function recover( +export const recover = async ( email: string, -): Promise<[unknown | null, string?]> { +): Promise<[RecoverResponse | null, string?]> => { try { const formData = new FormData(); formData.append("email", email); formData.append( "RedirectionURL", - process.env.REACT_APP_DOMAIN + "/changepwd", + `${process.env.REACT_APP_DOMAIN}/changepwd`, ); - const recoverResponse = await makeRequest({ - url: process.env.REACT_APP_DOMAIN + "/codeword/recover", + + const recoverResponse = await makeRequest({ + url: `${process.env.REACT_APP_DOMAIN}/codeword/recover`, body: formData, useToken: false, withCredentials: true, }); + return [recoverResponse]; } catch (nativeError) { const [error] = parseAxiosError(nativeError); return [null, `Не удалось восстановить пароль. ${error}`]; } -} +}; diff --git a/src/api/cart.ts b/src/api/cart.ts index bfa17da0..a497a51d 100644 --- a/src/api/cart.ts +++ b/src/api/cart.ts @@ -1,22 +1,63 @@ -import { UserAccount } from "@frontend/kitui"; -import makeRequest from "@api/makeRequest"; +import { makeRequest } from "@api/makeRequest"; import { parseAxiosError } from "@utils/parse-error"; -const apiUrl = process.env.REACT_APP_DOMAIN + "/customer"; +import type { UserAccount } from "@frontend/kitui"; -export async function payCart(): Promise<[UserAccount | null, string?]> { +const API_URL = `${process.env.REACT_APP_DOMAIN}/customer/v1.0.0/cart`; + +const payCart = async (): Promise<[UserAccount | null, string?]> => { try { const payCartResponse = await makeRequest({ - url: apiUrl + "/cart/pay", method: "POST", + url: `${API_URL}/pay`, useToken: true, }); return [payCartResponse]; } catch (nativeError) { - const [error] = parseAxiosError(nativeError); + const error = parseAxiosError(nativeError); return [null, `Не удалось оплатить товар из корзины. ${error}`]; } -} +}; + +const addCartItem = async ( + id: string, +): Promise<[UserAccount | null, string?]> => { + try { + const addedItem = await makeRequest({ + method: "PATCH", + url: `${API_URL}?id=${id}`, + }); + + return [addedItem]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + + return [null, `Не удалось добавить товар в корзину. ${error}`]; + } +}; + +const deleteCartItem = async ( + id: string, +): Promise<[UserAccount | null, string?]> => { + try { + const deletedItem = await makeRequest({ + method: "DELETE", + url: `${API_URL}?id=${id}`, + }); + + return [deletedItem]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + + return [null, `Не удалось удалить товар из корзины. ${error}`]; + } +}; + +export const cartApi = { + pay: payCart, + add: addCartItem, + delete: deleteCartItem, +}; diff --git a/src/api/contactForm.ts b/src/api/contactForm.ts index 00e9face..6ed0f71d 100644 --- a/src/api/contactForm.ts +++ b/src/api/contactForm.ts @@ -1,16 +1,31 @@ -import axios from "axios"; +import { makeRequest } from "@api/makeRequest"; -const domen = process.env.REACT_APP_DOMAIN; +import { parseAxiosError } from "@utils/parse-error"; -export function sendContactFormRequest(body: { +const API_URL = `${process.env.REACT_APP_DOMAIN}/feedback`; + +type SendContactFormBody = { contact: string; whoami: string; -}) { - return axios(`${domen}/feedback/callme`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - data: body, - }); -} +}; + +export const sendContactFormRequest = async ( + body: SendContactFormBody, +): Promise<[unknown | null, string?, number?]> => { + try { + const sendContactFormResponse = await makeRequest< + SendContactFormBody, + unknown + >({ + method: "POST", + url: `${API_URL}/callme`, + body, + }); + + return [sendContactFormResponse]; + } catch (nativeError) { + const [error, status] = parseAxiosError(nativeError); + + return [null, `Не удалось отправить контакты. ${error}`, status]; + } +}; diff --git a/src/api/discounts.ts b/src/api/discounts.ts index 025021b1..ce5c52b9 100644 --- a/src/api/discounts.ts +++ b/src/api/discounts.ts @@ -1,17 +1,19 @@ import { makeRequest } from "@frontend/kitui"; + import { parseAxiosError } from "@utils/parse-error"; import type { Discount } from "@model/discounts"; -const API_URL = process.env.REACT_APP_DOMAIN + "/price/discount"; +const API_URL = `${process.env.REACT_APP_DOMAIN}/price/discount`; -export async function getDiscounts( +export const getDiscounts = async ( userId: string, -): Promise<[Discount[] | null, string?]> { +): Promise<[Discount[] | null, string?]> => { try { - const { Discounts } = await makeRequest( - { method: "GET", url: `${API_URL}/user/${userId}` }, - ); + const { Discounts } = await makeRequest({ + method: "GET", + url: `${API_URL}/user/${userId}`, + }); return [Discounts]; } catch (nativeError) { @@ -19,4 +21,4 @@ export async function getDiscounts( return [null, `Не удалось получить скидки. ${error}`]; } -} +}; diff --git a/src/api/integration.ts b/src/api/integration.ts new file mode 100644 index 00000000..c2ec31ff --- /dev/null +++ b/src/api/integration.ts @@ -0,0 +1,377 @@ +import { QuestionKeys } from "@/pages/IntegrationsPage/IntegrationsModal/types"; +import { makeRequest } from "@api/makeRequest"; +import { parseAxiosError } from "@utils/parse-error"; +import useSWR from "swr"; + +export type PaginationRequest = { + page: number; + size: number; +}; + +const API_URL = `${process.env.REACT_APP_DOMAIN}/squiz/amocrm`; + +// получение информации об аккаунте + +export type AccountResponse = { + id: number; + accountID: string; + amoID: number; + name: string; + deleted: boolean; + createdAt: string; + subdomain: string; + country: string; + driveURL: string; + stale: boolean; +}; + +export const getAccount = async (): Promise<[AccountResponse | null, string?]> => { + try { + const response = await makeRequest({ + method: "GET", + url: `${API_URL}/account`, + useToken: true, + }); + return [response]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + return [null, ""]; + // return [null, `Не удалось получить информацию об аккаунте. ${error}`]; + } +}; + +export function useAmoAccount() { + return useSWR("amoAccount", () => + makeRequest({ + method: "GET", + url: `${API_URL}/account`, + useToken: true, + }) + ); +} + +// подключить Amo + +export const connectAmo = async (): Promise<[string | null, string?]> => { + try { + const response = await makeRequest({ + method: "POST", + url: `${API_URL}/account`, + useToken: true, + withCredentials: true, + }); + return [response.link]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + return [null, `Не удалось подключить аккаунт. ${error}`]; + } +}; + +// получение токена + +export type TokenPair = { + accessToken: string; + refreshToken: string; +}; + +export const getTokens = async (): Promise<[TokenPair | null, string?]> => { + try { + const response = await makeRequest({ + method: "GET", + url: `${API_URL}/webhook/create`, + useToken: true, + }); + return [response]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + return [null, `Failed to get tokens. ${error}`]; + } +}; + +//получение списка тегов + +export type Tag = { + ID: number; + AmoID: number; + AccountID: number; + Entity: string; + Name: string; + Color: string; + Deleted: boolean; + CreatedAt: number; +}; + +export type TagsResponse = { + count: number; + items: Tag[]; +}; + +export const getTags = async ({ page, size }: PaginationRequest): Promise<[TagsResponse | null, string?]> => { + try { + const tagsResponse = await makeRequest({ + method: "GET", + url: `${API_URL}/tags?page=${page}&size=${size}`, + }); + return [tagsResponse]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + return [null, `Не удалось получить список тегов. ${error}`]; + } +}; + +//получение списка пользователей + +export type User = { + id: number; + amoID: number; + name: string; + email: string; + role: number; + group: number; + deleted: boolean; + createdAt: string; + amoUserID: number; + + // Subdomain: string; + // AccountID: string; +}; + +export type UsersResponse = { + count: number; + items: User[]; +}; + +export const getUsers = async ({ page, size }: PaginationRequest): Promise<[UsersResponse | null, string?]> => { + try { + const usersResponse = await makeRequest({ + method: "GET", + url: `${API_URL}/users?page=${page}&size=${size}`, + }); + return [usersResponse]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + return [null, `Не удалось получить список пользователей. ${error}`]; + } +}; + +//получение списка шагов + +export type Step = { + ID: number; + AmoID: number; + PipelineID: number; + AccountID: number; + Name: string; + Color: string; + Deleted: boolean; + CreatedAt: number; +}; + +export type StepsResponse = { + count: number; + items: Step[]; +}; + +export const getSteps = async ({ + page, + size, + pipelineId, +}: PaginationRequest & { pipelineId: number }): Promise<[StepsResponse | null, string?]> => { + try { + const stepsResponse = await makeRequest({ + method: "GET", + url: `${API_URL}/steps?page=${page}&size=${size}&pipelineID=${pipelineId}`, + }); + return [stepsResponse]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + return [null, `Не удалось получить список шагов. ${error}`]; + } +}; + +//получение списка воронок + +export type Pipeline = { + ID: number; + AmoID: number; + AccountID: number; + Name: string; + IsArchive: boolean; + Deleted: boolean; + CreatedAt: number; +}; + +export type PipelinesResponse = { + count: number; + items: Pipeline[]; +}; + +export const getPipelines = async ({ page, size }: PaginationRequest): Promise<[PipelinesResponse | null, string?]> => { + try { + const pipelinesResponse = await makeRequest({ + method: "GET", + url: `${API_URL}/pipelines?page=${page}&size=${size}`, + }); + return [pipelinesResponse]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + return [null, `Не удалось получить список воронок. ${error}`]; + } +}; + +//получение настроек интеграции +export type QuestionID = Record; + +export type IntegrationRules = { + PipelineID: number; + StepID: number; + PerformerID?: number; + FieldsRule: FieldsRule; + TagsToAdd: { + Lead: number[] | null; + Contact: number[] | null; + Company: number[] | null; + Customer: number[] | null; + }; +}; +export type FieldsRule = Record, null | [{ QuestionID: QuestionID }]>; + +export const getIntegrationRules = async (quizID: string): Promise<[IntegrationRules | null, string?]> => { + try { + const settingsResponse = await makeRequest({ + method: "GET", + url: `${API_URL}/rules/${quizID}`, + }); + return [settingsResponse || null]; + } catch (nativeError) { + if (nativeError.response.status === 404) return [null, "first"]; + const [error] = parseAxiosError(nativeError); + return [null, `Не удалось получить настройки интеграции. ${error}`]; + } +}; + +//обновление настроек интеграции + +export type IntegrationRulesUpdate = { + PerformerID: number; + PipelineID: number; + StepID: number; + Utms: number[]; + FieldsRule: { + Lead: { QuestionID: number }[]; + Contact: { ContactRuleMap: string }[]; + Company: { QuestionID: number }[]; + Customer: { QuestionID: number }[]; + }; +}; + +export const setIntegrationRules = async ( + quizID: string, + settings: IntegrationRulesUpdate +): Promise<[string | null, string?]> => { + try { + const updateResponse = await makeRequest({ + method: "POST", + url: `${API_URL}/rules/${quizID}`, + body: settings, + }); + return [updateResponse]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + return [null, `Failed to update integration settings. ${error}`]; + } +}; +export const updateIntegrationRules = async ( + quizID: string, + settings: IntegrationRulesUpdate +): Promise<[string | null, string?]> => { + try { + const updateResponse = await makeRequest({ + method: "PATCH", + url: `${API_URL}/rules/${quizID}`, + body: settings, + }); + return [updateResponse]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + return [null, `Failed to update integration settings. ${error}`]; + } +}; + +// Получение кастомных полей + +export type CustomField = { + ID: number; + AmoID: number; + Code: string; + AccountID: number; + Name: string; + EntityType: string; + Type: string; + Deleted: boolean; + CreatedAt: number; +}; +export type Field = { + ID: number; + AmoID: number; + Code: string; + AccountID: number; + Name: string; + Entity: string; + Type: string; + Deleted: boolean; + CreatedAt: number; +}; + +export type CustomFieldsResponse = { + count: number; + items: CustomField[]; +}; +export type FieldsResponse = { + count: number; + items: Field[]; +}; + +export const getCustomFields = async ( + pagination: PaginationRequest +): Promise<[CustomFieldsResponse | null, string?]> => { + try { + const customFieldsResponse = await makeRequest({ + method: "GET", + url: `${API_URL}/fields?page=${pagination.page}&size=${pagination.size}`, + }); + return [customFieldsResponse]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + return [null, `Не удалось получить список кастомных полей. ${error}`]; + } +}; + +//Отвязать аккаунт амо от публикации + +export const removeAmoAccount = async (): Promise<[void | null, string?]> => { + try { + await makeRequest({ + method: "DELETE", + url: `${API_URL}/account`, + }); + return [null, ""]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + return [null, `Не удалось отвязать аккаунт. ${error}`]; + } +}; + + +export const getFields = async ( pagination: PaginationRequest ): Promise<[FieldsResponse | null, string?]> => { + try { + const fieldsResponse = await makeRequest({ + method: "GET", + url: `${API_URL}/fields?page=${pagination.page}&size=${pagination.size}`, + }); + return [fieldsResponse, ""]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + return [null, `Не удалось получить список полей. ${error}`]; + } +}; diff --git a/src/api/makeRequest.ts b/src/api/makeRequest.ts index b9b4c436..930a89b3 100644 --- a/src/api/makeRequest.ts +++ b/src/api/makeRequest.ts @@ -1,10 +1,13 @@ import * as KIT from "@frontend/kitui"; import { Method, ResponseType, AxiosError } from "axios"; +import { redirect } from "react-router-dom"; import { clearAuthToken } from "@frontend/kitui"; + import { cleanAuthTicketData } from "@root/ticket"; import { clearUserData } from "@root/user"; import { clearQuizData } from "@root/quizes/store"; -import { redirect } from "react-router-dom"; + +import type { AxiosResponse } from "axios"; interface MakeRequest { method?: Method | undefined; @@ -17,18 +20,22 @@ interface MakeRequest { withCredentials?: boolean | undefined; } -async function makeRequest( - data: MakeRequest, -): Promise { - try { - const response = await KIT.makeRequest(data); +type ExtendedAxiosResponse = AxiosResponse & { message: string }; + +export const makeRequest = async ( + data: MakeRequest, +): Promise => { + try { + const response = await KIT.makeRequest(data); + + return response; + } catch (nativeError) { + const error = nativeError as AxiosError; - return response as TResponse; - } catch (e) { - const error = e as AxiosError; if ( error.response?.status === 400 && - error.response?.data?.message === "refreshToken is empty" + (error.response?.data as ExtendedAxiosResponse)?.message === + "refreshToken is empty" ) { cleanAuthTicketData(); clearAuthToken(); @@ -36,7 +43,7 @@ async function makeRequest( clearQuizData(); redirect("/"); } - throw e; + + throw nativeError; } -} -export default makeRequest; +}; diff --git a/src/api/promocode.ts b/src/api/promocode.ts index 0f409653..87e90a01 100644 --- a/src/api/promocode.ts +++ b/src/api/promocode.ts @@ -1,25 +1,30 @@ -import makeRequest from "@api/makeRequest"; +import { makeRequest } from "@api/makeRequest"; import { parseAxiosError } from "@utils/parse-error"; -const apiUrl = process.env.REACT_APP_DOMAIN + "/codeword/promocode"; +type ActivatePromocodeRequest = { codeword: string } | { fastLink: string }; +type ActivatePromocodeResponse = { greetings: string }; -export async function activatePromocode(promocode: string) { +const API_URL = `${process.env.REACT_APP_DOMAIN}/codeword/promocode`; + +export const activatePromocode = async ( + promocode: string, +): Promise<[string | null, string?]> => { try { const response = await makeRequest< - { codeword: string } | { fastLink: string }, - { greetings: string } + ActivatePromocodeRequest, + ActivatePromocodeResponse >({ - url: apiUrl + "/activate", method: "POST", - contentType: true, + url: `${API_URL}/activate`, body: { codeword: promocode }, + contentType: true, }); - return response.greetings; + return [response.greetings]; } catch (nativeError) { const [error] = parseAxiosError(nativeError); - throw new Error(error); + return [null, `Ошибка при активации промокода. ${error}`]; } -} +}; diff --git a/src/api/question.ts b/src/api/question.ts index 3990299d..f15fcee6 100644 --- a/src/api/question.ts +++ b/src/api/question.ts @@ -1,84 +1,155 @@ -import makeRequest from "@api/makeRequest"; -import { CreateQuestionRequest } from "model/question/create"; -import { RawQuestion } from "model/question/question"; -import { +import { makeRequest } from "@api/makeRequest"; + +import { replaceSpacesToEmptyLines } from "@utils/replaceSpacesToEmptyLines"; +import { parseAxiosError } from "@utils/parse-error"; + +import type { CreateQuestionRequest } from "model/question/create"; +import type { RawQuestion } from "model/question/question"; +import type { GetQuestionListRequest, GetQuestionListResponse, } from "@model/question/getList"; -import { +import type { EditQuestionRequest, EditQuestionResponse, } from "@model/question/edit"; -import { +import type { DeleteQuestionRequest, DeleteQuestionResponse, } from "@model/question/delete"; -import { +import type { CopyQuestionRequest, CopyQuestionResponse, } from "@model/question/copy"; -import { replaceSpacesToEmptyLines } from "../utils/replaceSpacesToEmptyLines"; -const baseUrl = process.env.REACT_APP_DOMAIN + "/squiz"; +const API_URL = `${process.env.REACT_APP_DOMAIN}/squiz`; -function createQuestion(body: CreateQuestionRequest) { - return makeRequest({ - url: `${baseUrl}/question/create`, - body, - method: "POST", - }); -} +export const createQuestion = async ( + body: CreateQuestionRequest, +): Promise<[RawQuestion | null, string?]> => { + try { + const createdQuestion = await makeRequest< + CreateQuestionRequest, + RawQuestion + >({ + method: "POST", + url: `${API_URL}/question/create`, + body, + }); -async function getQuestionList(body?: Partial) { - if (!body?.quiz_id) return null; + return [createdQuestion]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); - const response = await makeRequest< - GetQuestionListRequest, - GetQuestionListResponse - >({ - url: `${baseUrl}/question/getList`, - body: { ...defaultGetQuestionListBody, ...body }, - method: "POST", - }); - const clearArrayFromEmptySpaceBlaBlaValue = response.items?.map( - (question) => { - let data = question; - for (let key in question) { - const k = key as keyof RawQuestion; - //@ts-ignore - if (question[key] === " ") data[key] = ""; - } - return data; - }, - ); + return [null, `Не удалось создать вопрос. ${error}`]; + } +}; - return replaceSpacesToEmptyLines(clearArrayFromEmptySpaceBlaBlaValue); -} +const getQuestionList = async ( + body?: Partial, +): Promise<[RawQuestion[] | null, string?]> => { + try { + if (!body?.quiz_id) return [null, "Квиз не найден"]; -function editQuestion(body: EditQuestionRequest, signal?: AbortSignal) { - return makeRequest({ - url: `${baseUrl}/question/edit`, - body, - method: "PATCH", - signal, - }); -} + const response = await makeRequest< + GetQuestionListRequest, + GetQuestionListResponse + >({ + method: "POST", + url: `${API_URL}/question/getList`, + body: { ...defaultGetQuestionListBody, ...body }, + }); -function copyQuestion(questionId: number, quizId: number) { - return makeRequest({ - url: `${baseUrl}/question/copy`, - body: { id: questionId, quiz_id: quizId }, - method: "POST", - }); -} + const clearArrayFromEmptySpaceBlaBlaValue = response.items?.map( + (question) => { + let data = question; -function deleteQuestion(id: number) { - return makeRequest({ - url: `${baseUrl}/question/delete`, - body: { id }, - method: "DELETE", - }); -} + for (let key in question) { + if (question[key as keyof RawQuestion] === " ") { + //@ts-ignore + data[key] = ""; + } + } + + return data; + }, + ); + + return [ + replaceSpacesToEmptyLines(clearArrayFromEmptySpaceBlaBlaValue) ?? null, + ]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + + return [null, `Не удалось получить список вопросов. ${error}`]; + } +}; + +export const editQuestion = async ( + body: EditQuestionRequest, + signal?: AbortSignal, +): Promise<[EditQuestionResponse | null, string?]> => { + try { + const editedQuestion = await makeRequest< + EditQuestionRequest, + EditQuestionResponse + >({ + method: "PATCH", + url: `${API_URL}/question/edit`, + body, + signal, + }); + + return [editedQuestion]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + + return [null, `Не удалось изменить вопрос. ${error}`]; + } +}; + +export const copyQuestion = async ( + questionId: number, + quizId: number, +): Promise<[CopyQuestionResponse | null, string?]> => { + try { + const copiedQuestion = await makeRequest< + CopyQuestionRequest, + CopyQuestionResponse + >({ + method: "POST", + url: `${API_URL}/question/copy`, + body: { id: questionId, quiz_id: quizId }, + }); + + return [copiedQuestion]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + + return [null, `Не удалось скопировать вопрос. ${error}`]; + } +}; + +export const deleteQuestion = async ( + id: number, +): Promise<[DeleteQuestionResponse | null, string?]> => { + try { + const deletedQuestion = await makeRequest< + DeleteQuestionRequest, + DeleteQuestionResponse + >({ + url: `${API_URL}/question/delete`, + body: { id }, + method: "DELETE", + }); + + return [deletedQuestion]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + + return [null, `Не удалось удалить вопрос. ${error}`]; + } +}; export const questionApi = { create: createQuestion, diff --git a/src/api/quiz.ts b/src/api/quiz.ts index a876b68f..d534da88 100644 --- a/src/api/quiz.ts +++ b/src/api/quiz.ts @@ -1,79 +1,190 @@ -import makeRequest from "@api/makeRequest"; +import { makeRequest } from "@api/makeRequest"; import { defaultQuizConfig } from "@model/quizSettings"; -import { CopyQuizRequest, CopyQuizResponse } from "model/quiz/copy"; -import { CreateQuizRequest } from "model/quiz/create"; -import { DeleteQuizRequest, DeleteQuizResponse } from "model/quiz/delete"; -import { EditQuizRequest, EditQuizResponse } from "model/quiz/edit"; -import { GetQuizRequest, GetQuizResponse } from "model/quiz/get"; -import { GetQuizListRequest, GetQuizListResponse } from "model/quiz/getList"; -import { RawQuiz } from "model/quiz/quiz"; -const baseUrl = process.env.REACT_APP_DOMAIN + "/squiz"; -const imagesUrl = process.env.REACT_APP_DOMAIN + "/squizstorer"; +import { parseAxiosError } from "@utils/parse-error"; -function createQuiz(body?: Partial) { - return makeRequest({ - url: `${baseUrl}/quiz/create`, - body: { ...defaultCreateQuizBody, ...body }, - method: "POST", - }); -} +import type { RawQuiz } from "model/quiz/quiz"; +import type { CopyQuizRequest, CopyQuizResponse } from "model/quiz/copy"; +import type { CreateQuizRequest } from "model/quiz/create"; +import type { DeleteQuizRequest, DeleteQuizResponse } from "model/quiz/delete"; +import type { EditQuizRequest, EditQuizResponse } from "model/quiz/edit"; +import type { GetQuizRequest, GetQuizResponse } from "model/quiz/get"; +import type { + GetQuizListRequest, + GetQuizListResponse, +} from "model/quiz/getList"; -async function getQuizList(body?: Partial) { - const response = await makeRequest({ - url: `${baseUrl}/quiz/getList`, - body: { ...defaultGetQuizListBody, ...body }, - method: "POST", - }); +type AddedQuizImagesResponse = { + [key: string]: string; +}; - return response.items; -} +const API_URL = `${process.env.REACT_APP_DOMAIN}/squiz`; +const IMAGES_URL = `${process.env.REACT_APP_DOMAIN}/squizstorer/v1.0.0`; -function getQuiz(body?: Partial) { - return makeRequest({ - url: `${baseUrl}/quiz/get`, - body: { ...defaultGetQuizBody, ...body }, - method: "GET", - }); -} +export const createQuiz = async ( + body?: Partial, +): Promise<[RawQuiz | null, string?]> => { + try { + const createdQuiz = await makeRequest({ + method: "POST", + url: `${API_URL}/quiz/create`, + body: { ...defaultCreateQuizBody, ...body }, + }); -async function editQuiz(body: EditQuizRequest, signal?: AbortSignal) { - return makeRequest({ - url: `${baseUrl}/quiz/edit`, - body, - method: "PATCH", - signal, - }); -} + return [createdQuiz]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); -function copyQuiz(id: number) { - return makeRequest({ - url: `${baseUrl}/quiz/copy`, - body: { id }, - method: "POST", - }); -} + return [null, `Не удалось создать квиз. ${error}`]; + } +}; -function deleteQuiz(id: number) { - return makeRequest({ - url: `${baseUrl}/quiz/delete`, - body: { id }, - method: "DELETE", - }); -} +export const getQuizList = async ( + body?: Partial, +): Promise<[RawQuiz[] | null, string?]> => { + try { + const { items } = await makeRequest< + GetQuizListRequest, + GetQuizListResponse + >({ + method: "POST", + url: `${API_URL}/quiz/getList`, + body: { ...defaultGetQuizListBody, ...body }, + }); -function addQuizImages(quizId: number, image: Blob) { - const formData = new FormData(); + return [items]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); - formData.append("quiz", quizId.toString()); - formData.append("image", image); + return [null, `Не удалось получить список квизов. ${error}`]; + } +}; - return makeRequest({ - url: `${imagesUrl}/quiz/putImages`, - body: formData, - method: "PUT", - }); -} +export const getQuiz = async ( + body?: Partial, +): Promise<[GetQuizResponse | null, string?]> => { + try { + const quiz = await makeRequest({ + method: "GET", + url: `${API_URL}/quiz/get`, + body: { ...defaultGetQuizBody, ...body }, + }); + + return [quiz]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + + return [null, `Не удалось получить квиз. ${error}`]; + } +}; + +export const editQuiz = async ( + body: EditQuizRequest, + signal?: AbortSignal, +): Promise<[EditQuizResponse | null, string?]> => { + try { + const editedQuiz = await makeRequest({ + method: "PATCH", + url: `${API_URL}/quiz/edit`, + body, + signal, + }); + + return [editedQuiz]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + + return [null, `Не удалось изменить квиз. ${error}`]; + } +}; + +export const copyQuiz = async ( + id: number, +): Promise<[EditQuizResponse | null, string?]> => { + try { + const copiedQuiz = await makeRequest({ + method: "POST", + url: `${API_URL}/quiz/copy`, + body: { id }, + }); + + return [copiedQuiz]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + + return [null, `Не удалось скопировать квиз. ${error}`]; + } +}; + +export const deleteQuiz = async ( + id: number, +): Promise<[DeleteQuizResponse | null, string?]> => { + try { + const deletedQuiz = await makeRequest< + DeleteQuizRequest, + DeleteQuizResponse + >({ + method: "DELETE", + url: `${API_URL}/quiz/delete`, + body: { id }, + }); + + return [deletedQuiz]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + + return [null, `Не удалось удалить квиз. ${error}`]; + } +}; + +export const addQuizImages = async ( + quizId: number, + image: Blob, +): Promise<[AddedQuizImagesResponse | null, string?]> => { + try { + const formData = new FormData(); + + formData.append("quiz", quizId.toString()); + formData.append("image", image); + + const addedQuizImages = await makeRequest< + FormData, + AddedQuizImagesResponse + >({ + url: `${IMAGES_URL}/quiz/putImages`, + body: formData, + method: "PUT", + }); + + return [addedQuizImages]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + + return [null, `Не удалось добавить изображение. ${error}`]; + } +}; + +export const copyQuizTemplate = async ( + qid: string, +): Promise<[number | null, string?]> => { + try { + const { id } = await makeRequest<{ Qid: string }, { id: number }>({ + method: "POST", + url: `${API_URL}/quiz/template`, + body: { Qid: qid }, + }); + + if (!id) { + return [null, `Не удалось скопировать шаблон квиза.`]; + } + + return [id]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + + return [null, `Не удалось скопировать шаблон квиза. ${error}`]; + } +}; export const quizApi = { create: createQuiz, @@ -83,6 +194,7 @@ export const quizApi = { copy: copyQuiz, delete: deleteQuiz, addImages: addQuizImages, + copyTemplate: copyQuizTemplate, }; const defaultCreateQuizBody: CreateQuizRequest = { diff --git a/src/api/result.ts b/src/api/result.ts index 8d6dab82..74d0a5d8 100644 --- a/src/api/result.ts +++ b/src/api/result.ts @@ -1,5 +1,8 @@ -import makeRequest from "@api/makeRequest"; -import { RawResult } from "@model/result/result"; +import { makeRequest } from "@api/makeRequest"; + +import { parseAxiosError } from "@utils/parse-error"; + +import type { RawResult } from "@model/result/result"; interface IResultListBody { to: number; @@ -29,47 +32,113 @@ export interface IAnswerResult { question_id: number; } -async function getResultList(quizId: number, page: number, body: any) { - return makeRequest({ - url: process.env.REACT_APP_DOMAIN + `/squiz/results/getResults/${quizId}`, - method: "POST", - body: { page: page, limit: 10, ...body }, - }); -} +type ResultFilter = { + from?: string; + new?: boolean; + to?: string; +}; -function deleteResult(resultId: number) { - return makeRequest({ - url: process.env.REACT_APP_DOMAIN + `/squiz/results/delete/${resultId}`, - body: {}, - method: "DELETE", - }); -} +type ObsolescenceRequest = { + answers: number[]; +}; -function obsolescenceResult(idResultArray: number[]) { - return makeRequest({ - url: process.env.REACT_APP_DOMAIN + `/squiz/result/seen`, - body: { - answers: idResultArray, - }, - method: "PATCH", - }); -} +const API_URL = `${process.env.REACT_APP_DOMAIN}/squiz`; -function getAnswerResultList(resultId: number) { - return makeRequest({ - url: process.env.REACT_APP_DOMAIN + `/squiz/result/${resultId}`, - method: "GET", - }); -} +const getResultList = async ( + quizId: number, + page: number, + body: ResultFilter, +): Promise<[RawResult | null, string?]> => { + try { + const resultList = await makeRequest({ + method: "POST", + url: `${API_URL}/results/getResults/${quizId}`, + body: { page: page, limit: 10, ...body }, + }); -function AnswerResultListEx(quizId: number, body: any) { - return makeRequest({ - responseType: "blob", - url: process.env.REACT_APP_DOMAIN + `/squiz/results/${quizId}/export`, - method: "POST", - body: body, - }); -} + return [resultList]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + + return [null, `Не удалось получить результат. ${error}`]; + } +}; + +const deleteResult = async ( + resultId: number, +): Promise<[string | null, string?]> => { + try { + const deletedResult = await makeRequest({ + method: "DELETE", + url: `${API_URL}/results/delete/${resultId}`, + body: {}, + }); + + return [deletedResult]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + + return [null, `Не удалось удалить результат. ${error}`]; + } +}; + +const obsolescenceResult = async ( + idResultArray: number[], +): Promise<[null, string?]> => { + try { + const obsolescencedResult = await makeRequest({ + method: "PATCH", + url: `${API_URL}/result/seen`, + body: { answers: idResultArray }, + }); + + return [obsolescencedResult]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + + return [null, `Не удалось изменить результат. ${error}`]; + } +}; + +const getAnswerResultList = async ( + resultId: number, +): Promise<[IAnswerResult[] | null, string?]> => { + try { + const answerResultList = await makeRequest({ + method: "GET", + url: `${API_URL}/result/${resultId}`, + }); + + return [answerResultList]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + + return [null, `Не удалось получить список результатов. ${error}`]; + } +}; + +const AnswerResultListEx = async ( + quizId: number, + body: ResultFilter, +): Promise<[Blob | null, string?]> => { + try { + const answerResultListEx = await makeRequest({ + method: "POST", + url: `${API_URL}/results/${quizId}/export`, + body, + responseType: "blob", + }); + + return [answerResultListEx]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + + return [ + null, + `Не удалось получить список устаревших результатов. ${error}`, + ]; + } +}; export const resultApi = { getList: getResultList, diff --git a/src/api/statistic.ts b/src/api/statistic.ts index f9ac1e6b..f9e13396 100644 --- a/src/api/statistic.ts +++ b/src/api/statistic.ts @@ -1,9 +1,7 @@ -import makeRequest from "@api/makeRequest"; +import { makeRequest } from "@api/makeRequest"; import { parseAxiosError } from "@utils/parse-error"; -const apiUrl = process.env.REACT_APP_DOMAIN + "/squiz/statistic"; - export type DevicesResponse = { Device: Record; OS: Record; @@ -23,12 +21,15 @@ export type QuestionsResponse = { Results: Record; Questions: Record>; }; +export type GraphicsResponse = unknown; type TRequest = { to: number; from: number; }; +const API_URL = `${process.env.REACT_APP_DOMAIN}/squiz/statistic`; + export const getDevices = async ( quizId: string, to: number, @@ -37,7 +38,7 @@ export const getDevices = async ( try { const devicesResponse = await makeRequest({ method: "POST", - url: `${apiUrl}/${quizId}/devices`, + url: `${API_URL}/${quizId}/devices`, withCredentials: true, body: { to, from }, }); @@ -58,7 +59,7 @@ export const getGeneral = async ( try { const generalResponse = await makeRequest({ method: "POST", - url: `${apiUrl}/${quizId}/general`, + url: `${API_URL}/${quizId}/general`, withCredentials: true, body: { to, from }, }); @@ -79,7 +80,7 @@ export const getQuestions = async ( try { const questionsResponse = await makeRequest({ method: "POST", - url: `${apiUrl}/${quizId}/questions`, + url: `${API_URL}/${quizId}/questions`, withCredentials: true, body: { to, from }, }); @@ -91,3 +92,24 @@ export const getQuestions = async ( return [null, `Не удалось получить статистику по результатам. ${error}`]; } }; + +export const getGraphics = async ( + quizId: string, + to: number, + from: number, +): Promise<[GraphicsResponse | null, string?]> => { + try { + const questionsResponse = await makeRequest({ + method: "get", + url: `${API_URL}s/${quizId}/pipelines?from=${from}&to=${to}`, + withCredentials: true, + }); + + console.log(questionsResponse) + return [questionsResponse]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + + return [null, `Не удалось получить статистику. ${error}`]; + } +}; \ No newline at end of file diff --git a/src/api/tariff.ts b/src/api/tariff.ts new file mode 100644 index 00000000..0ddaceee --- /dev/null +++ b/src/api/tariff.ts @@ -0,0 +1,23 @@ +import { makeRequest } from "@api/makeRequest"; + +import { parseAxiosError } from "@utils/parse-error"; + +import type { GetTariffsResponse } from "@frontend/kitui"; + +const API_URL = `${process.env.REACT_APP_DOMAIN}/strator/tariff`; + +export const getTariffs = async ( + page: number, +): Promise<[GetTariffsResponse | null, string?]> => { + try { + const tariffs = await makeRequest({ + method: "GET", + url: `${API_URL}?page=${page}&limit=100`, + }); + return [tariffs]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + + return [null, `Ошибка при получении списка тарифов. ${error}`]; + } +}; diff --git a/src/api/ticket.ts b/src/api/ticket.ts index 6d00beed..48404ed3 100644 --- a/src/api/ticket.ts +++ b/src/api/ticket.ts @@ -1,20 +1,30 @@ -import makeRequest from "@api/makeRequest"; -import { parseAxiosError } from "../utils/parse-error"; +import { createTicket as createTicketRequest } from "@frontend/kitui"; -import { SendTicketMessageRequest } from "@frontend/kitui"; +import { makeRequest } from "@api/makeRequest"; -const apiUrl = process.env.REACT_APP_DOMAIN + "/heruvym"; +import { parseAxiosError } from "@utils/parse-error"; -export async function sendTicketMessage( +import type { + SendTicketMessageRequest, + CreateTicketResponse, +} from "@frontend/kitui"; + +type SendFileResponse = { + message: string; +}; + +const API_URL = `${process.env.REACT_APP_DOMAIN}/heruvym/v1.0.0`; + +export const sendTicketMessage = async ( ticketId: string, message: string, -): Promise<[null, string?]> { +): Promise<[null, string?]> => { try { const sendTicketMessageResponse = await makeRequest< SendTicketMessageRequest, null >({ - url: `${apiUrl}/send`, + url: `${API_URL}/send`, method: "POST", useToken: true, body: { ticket: ticketId, message: message, lang: "ru", files: [] }, @@ -26,12 +36,12 @@ export async function sendTicketMessage( return [null, `Не удалось отправить сообщение. ${error}`]; } -} +}; -export async function shownMessage(id: string): Promise<[null, string?]> { +export const shownMessage = async (id: string): Promise<[null, string?]> => { try { const shownMessageResponse = await makeRequest<{ id: string }, null>({ - url: apiUrl + "/shown", + url: `${API_URL}/shown`, method: "POST", useToken: true, body: { id }, @@ -43,4 +53,47 @@ export async function shownMessage(id: string): Promise<[null, string?]> { return [null, `Не удалось прочесть сообщение. ${error}`]; } -} +}; + +export const sendFile = async ( + ticketId: string, + file: File, +): Promise<[SendFileResponse | null, string?]> => { + try { + const body = new FormData(); + + body.append(file.name, file); + body.append("ticket", ticketId); + + const sendResponse = await makeRequest({ + method: "POST", + url: `${API_URL}/sendFiles`, + body, + }); + + return [sendResponse]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + + return [null, `Не удалось отправить файл. ${error}`]; + } +}; + +export const createTicket = async ( + message: string, + useToken: boolean, +): Promise<[CreateTicketResponse | null, string?]> => { + try { + const createdTicket = await createTicketRequest({ + url: `${process.env.REACT_APP_DOMAIN}/heruvym/v1.0.0/create`, + body: { Title: "Unauth title", Message: message }, + useToken, + }); + + return [createdTicket]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + + return [null, `Не удалось создать тикет. ${error}`]; + } +}; diff --git a/src/api/user.ts b/src/api/user.ts new file mode 100644 index 00000000..e50b1818 --- /dev/null +++ b/src/api/user.ts @@ -0,0 +1,66 @@ +import { makeRequest } from "@api/makeRequest"; + +import { parseAxiosError } from "@utils/parse-error"; + +import type { UserAccount } from "@frontend/kitui"; +import type { OriginalUserAccount } from "@root/user"; + +type RecoverUserRequest = { + password: string; +}; + +export const getUser = async (): Promise<[UserAccount | null, string?]> => { + try { + const user = await makeRequest({ + method: "GET", + url: `${process.env.REACT_APP_DOMAIN}/customer/v1.0.0/account`, + }); + + return [user]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + + return [null, `Не удалось получить пользователя. ${error}`]; + } +}; + +export const getAccount = async (): Promise< + [OriginalUserAccount | null, string?] +> => { + try { + const controller = new AbortController(); + + const account = await makeRequest({ + url: `${process.env.REACT_APP_DOMAIN}/squiz/account/get`, + contentType: true, + method: "GET", + useToken: true, + withCredentials: false, + signal: controller.signal, + }); + + return [account]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + + return [null, `Не удалось получить данные аккаунта. ${error}`]; + } +}; + +export const recoverUser = async ( + password: string, +): Promise<[unknown | null, string?]> => { + try { + const recoverResponse = await makeRequest({ + url: `${process.env.REACT_APP_DOMAIN}/user`, + method: "PATCH", + body: { password }, + }); + + return [recoverResponse]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + + return [null, `Не удалось восстановить пароль. ${error}`]; + } +}; diff --git a/src/assets/BannerImg.png b/src/assets/BannerImg.png deleted file mode 100644 index 0e380878..00000000 Binary files a/src/assets/BannerImg.png and /dev/null differ diff --git a/src/assets/VidjetImg.png b/src/assets/VidjetImg.png deleted file mode 100644 index b659fd5a..00000000 Binary files a/src/assets/VidjetImg.png and /dev/null differ diff --git a/src/assets/icons/Amologo.png b/src/assets/icons/Amologo.png new file mode 100644 index 00000000..058ea796 Binary files /dev/null and b/src/assets/icons/Amologo.png differ diff --git a/src/assets/icons/CopyIcon.tsx b/src/assets/icons/CopyIcon.tsx index 16409e1e..2079ab74 100644 --- a/src/assets/icons/CopyIcon.tsx +++ b/src/assets/icons/CopyIcon.tsx @@ -4,10 +4,15 @@ interface Props { color?: string; bgcolor?: string; marL?: string; - width?: string + width?: string; } -export default function CopyIcon({ color, bgcolor, marL, width = "36px" }: Props) { +export default function CopyIcon({ + color, + bgcolor, + marL, + width = "36px", +}: Props) { const theme = useTheme(); return ( diff --git a/src/assets/icons/NumberThree.tsx b/src/assets/icons/NumberThree.tsx deleted file mode 100644 index f9978180..00000000 --- a/src/assets/icons/NumberThree.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { Box } from "@mui/material"; - -interface Props { - color: string; -} - -export default function NumberThree({ color }: Props) { - return ( - - - - - - - ); -} diff --git a/src/assets/icons/NumberTwo.tsx b/src/assets/icons/NumberTwo.tsx deleted file mode 100644 index 85116206..00000000 --- a/src/assets/icons/NumberTwo.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { Box } from "@mui/material"; - -interface Props { - color: string; -} - -export default function NumberTwo({ color }: Props) { - return ( - - - - - - - ); -} diff --git a/src/assets/icons/OneIconBorder.tsx b/src/assets/icons/OneIconBorder.tsx deleted file mode 100644 index e9ac34b4..00000000 --- a/src/assets/icons/OneIconBorder.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { Box } from "@mui/material"; - -interface Props { - color: string; -} - -export default function OneIconBorder({ color }: Props) { - return ( - - - - - - - ); -} diff --git a/src/assets/icons/questionsPage/AlignIcon.tsx b/src/assets/icons/questionsPage/AlignIcon.tsx new file mode 100644 index 00000000..2816e4b5 --- /dev/null +++ b/src/assets/icons/questionsPage/AlignIcon.tsx @@ -0,0 +1,22 @@ +import { FC } from "react"; +import { Box } from "@mui/material"; + +export const AlignIcon: FC = () => ( + + + + + +); diff --git a/src/assets/icons/questionsPage/EditIcon.tsx b/src/assets/icons/questionsPage/EditIcon.tsx new file mode 100644 index 00000000..61e7090e --- /dev/null +++ b/src/assets/icons/questionsPage/EditIcon.tsx @@ -0,0 +1,35 @@ +import { FC } from "react"; +import { Box } from "@mui/material"; + +export const EditIcon: FC = () => ( + + + + + + + + +); diff --git a/src/assets/icons/questionsPage/ExpandIcon.tsx b/src/assets/icons/questionsPage/ExpandIcon.tsx new file mode 100644 index 00000000..4d3cb5d6 --- /dev/null +++ b/src/assets/icons/questionsPage/ExpandIcon.tsx @@ -0,0 +1,29 @@ +import { FC } from "react"; +import { Box } from "@mui/material"; + +export const ExpandIcon: FC = () => ( + + + + + + +); diff --git a/src/assets/icons/questionsPage/GrayPlus.tsx b/src/assets/icons/questionsPage/GrayPlus.tsx new file mode 100644 index 00000000..4dc6ebd2 --- /dev/null +++ b/src/assets/icons/questionsPage/GrayPlus.tsx @@ -0,0 +1,29 @@ +import { FC } from "react"; +import { Box } from "@mui/material"; + +export const GrayPlus: FC = () => ( + + + + + + +); diff --git a/src/assets/icons/questionsPage/RoundedCheckedIcon.tsx b/src/assets/icons/questionsPage/RoundedCheckedIcon.tsx new file mode 100644 index 00000000..9c94926a --- /dev/null +++ b/src/assets/icons/questionsPage/RoundedCheckedIcon.tsx @@ -0,0 +1,38 @@ +import { FC } from "react"; +import { Box } from "@mui/material"; + +export const RoundedCheckedIcon: FC = () => ( + + + + + + +); diff --git a/src/assets/quiz-template-1.png b/src/assets/quiz-template-1.png deleted file mode 100755 index 0703b74a..00000000 Binary files a/src/assets/quiz-template-1.png and /dev/null differ diff --git a/src/assets/quiz-template-2.png b/src/assets/quiz-template-2.png deleted file mode 100755 index f92c8c6b..00000000 Binary files a/src/assets/quiz-template-2.png and /dev/null differ diff --git a/src/assets/quiz-template-3.png b/src/assets/quiz-template-3.png deleted file mode 100755 index 8bb8e8c3..00000000 Binary files a/src/assets/quiz-template-3.png and /dev/null differ diff --git a/src/assets/quiz-template-4.png b/src/assets/quiz-template-4.png deleted file mode 100755 index 11ecf89f..00000000 Binary files a/src/assets/quiz-template-4.png and /dev/null differ diff --git a/src/assets/quiz-template-5.png b/src/assets/quiz-template-5.png deleted file mode 100755 index 4cbde4e2..00000000 Binary files a/src/assets/quiz-template-5.png and /dev/null differ diff --git a/src/assets/quiz-template-6.png b/src/assets/quiz-template-6.png deleted file mode 100755 index 0308e86f..00000000 Binary files a/src/assets/quiz-template-6.png and /dev/null differ diff --git a/src/assets/quiz-templates/auto/auto-1.jpg b/src/assets/quiz-templates/auto/auto-1.jpg new file mode 100644 index 00000000..4f3d9fac Binary files /dev/null and b/src/assets/quiz-templates/auto/auto-1.jpg differ diff --git a/src/assets/quiz-templates/auto/auto-10.jpg b/src/assets/quiz-templates/auto/auto-10.jpg new file mode 100644 index 00000000..474c8c1a Binary files /dev/null and b/src/assets/quiz-templates/auto/auto-10.jpg differ diff --git a/src/assets/quiz-templates/auto/auto-2.jpg b/src/assets/quiz-templates/auto/auto-2.jpg new file mode 100644 index 00000000..3bd51d36 Binary files /dev/null and b/src/assets/quiz-templates/auto/auto-2.jpg differ diff --git a/src/assets/quiz-templates/auto/auto-3.jpg b/src/assets/quiz-templates/auto/auto-3.jpg new file mode 100644 index 00000000..320f7d0b Binary files /dev/null and b/src/assets/quiz-templates/auto/auto-3.jpg differ diff --git a/src/assets/quiz-templates/auto/auto-4.jpg b/src/assets/quiz-templates/auto/auto-4.jpg new file mode 100644 index 00000000..1a3c4983 Binary files /dev/null and b/src/assets/quiz-templates/auto/auto-4.jpg differ diff --git a/src/assets/quiz-templates/auto/auto-5.jpg b/src/assets/quiz-templates/auto/auto-5.jpg new file mode 100644 index 00000000..e0a5b948 Binary files /dev/null and b/src/assets/quiz-templates/auto/auto-5.jpg differ diff --git a/src/assets/quiz-templates/auto/auto-6.jpg b/src/assets/quiz-templates/auto/auto-6.jpg new file mode 100644 index 00000000..94d71b30 Binary files /dev/null and b/src/assets/quiz-templates/auto/auto-6.jpg differ diff --git a/src/assets/quiz-templates/auto/auto-7.jpg b/src/assets/quiz-templates/auto/auto-7.jpg new file mode 100644 index 00000000..286e7a61 Binary files /dev/null and b/src/assets/quiz-templates/auto/auto-7.jpg differ diff --git a/src/assets/quiz-templates/auto/auto-8.jpg b/src/assets/quiz-templates/auto/auto-8.jpg new file mode 100644 index 00000000..b1887c9f Binary files /dev/null and b/src/assets/quiz-templates/auto/auto-8.jpg differ diff --git a/src/assets/quiz-templates/auto/auto-9.jpg b/src/assets/quiz-templates/auto/auto-9.jpg new file mode 100644 index 00000000..4521fcc4 Binary files /dev/null and b/src/assets/quiz-templates/auto/auto-9.jpg differ diff --git a/src/assets/quiz-templates/education/education-1.jpg b/src/assets/quiz-templates/education/education-1.jpg new file mode 100644 index 00000000..a4eddf4d Binary files /dev/null and b/src/assets/quiz-templates/education/education-1.jpg differ diff --git a/src/assets/quiz-templates/education/education-10.jpg b/src/assets/quiz-templates/education/education-10.jpg new file mode 100644 index 00000000..708c3f03 Binary files /dev/null and b/src/assets/quiz-templates/education/education-10.jpg differ diff --git a/src/assets/quiz-templates/education/education-2.jpg b/src/assets/quiz-templates/education/education-2.jpg new file mode 100644 index 00000000..9de16bf0 Binary files /dev/null and b/src/assets/quiz-templates/education/education-2.jpg differ diff --git a/src/assets/quiz-templates/education/education-3.jpg b/src/assets/quiz-templates/education/education-3.jpg new file mode 100644 index 00000000..30ffb39f Binary files /dev/null and b/src/assets/quiz-templates/education/education-3.jpg differ diff --git a/src/assets/quiz-templates/education/education-4.jpg b/src/assets/quiz-templates/education/education-4.jpg new file mode 100644 index 00000000..fc47aa35 Binary files /dev/null and b/src/assets/quiz-templates/education/education-4.jpg differ diff --git a/src/assets/quiz-templates/education/education-5.jpg b/src/assets/quiz-templates/education/education-5.jpg new file mode 100644 index 00000000..5b9dbbb1 Binary files /dev/null and b/src/assets/quiz-templates/education/education-5.jpg differ diff --git a/src/assets/quiz-templates/education/education-6.jpg b/src/assets/quiz-templates/education/education-6.jpg new file mode 100644 index 00000000..2848f557 Binary files /dev/null and b/src/assets/quiz-templates/education/education-6.jpg differ diff --git a/src/assets/quiz-templates/education/education-7.jpg b/src/assets/quiz-templates/education/education-7.jpg new file mode 100644 index 00000000..7a291aab Binary files /dev/null and b/src/assets/quiz-templates/education/education-7.jpg differ diff --git a/src/assets/quiz-templates/education/education-8.jpg b/src/assets/quiz-templates/education/education-8.jpg new file mode 100644 index 00000000..562122fc Binary files /dev/null and b/src/assets/quiz-templates/education/education-8.jpg differ diff --git a/src/assets/quiz-templates/education/education-9.jpg b/src/assets/quiz-templates/education/education-9.jpg new file mode 100644 index 00000000..33852229 Binary files /dev/null and b/src/assets/quiz-templates/education/education-9.jpg differ diff --git a/src/assets/quiz-templates/health/health-1.jpg b/src/assets/quiz-templates/health/health-1.jpg new file mode 100644 index 00000000..8e3c5adb Binary files /dev/null and b/src/assets/quiz-templates/health/health-1.jpg differ diff --git a/src/assets/quiz-templates/health/health-10.jpg b/src/assets/quiz-templates/health/health-10.jpg new file mode 100644 index 00000000..ff8f4b4e Binary files /dev/null and b/src/assets/quiz-templates/health/health-10.jpg differ diff --git a/src/assets/quiz-templates/health/health-11.jpg b/src/assets/quiz-templates/health/health-11.jpg new file mode 100644 index 00000000..32ec9c3e Binary files /dev/null and b/src/assets/quiz-templates/health/health-11.jpg differ diff --git a/src/assets/quiz-templates/health/health-12.jpg b/src/assets/quiz-templates/health/health-12.jpg new file mode 100644 index 00000000..3107437f Binary files /dev/null and b/src/assets/quiz-templates/health/health-12.jpg differ diff --git a/src/assets/quiz-templates/health/health-13.jpg b/src/assets/quiz-templates/health/health-13.jpg new file mode 100644 index 00000000..ed2be7fb Binary files /dev/null and b/src/assets/quiz-templates/health/health-13.jpg differ diff --git a/src/assets/quiz-templates/health/health-14.jpg b/src/assets/quiz-templates/health/health-14.jpg new file mode 100644 index 00000000..5298f25c Binary files /dev/null and b/src/assets/quiz-templates/health/health-14.jpg differ diff --git a/src/assets/quiz-templates/health/health-15.jpg b/src/assets/quiz-templates/health/health-15.jpg new file mode 100644 index 00000000..d2ca84c6 Binary files /dev/null and b/src/assets/quiz-templates/health/health-15.jpg differ diff --git a/src/assets/quiz-templates/health/health-16.jpg b/src/assets/quiz-templates/health/health-16.jpg new file mode 100644 index 00000000..7f8163c6 Binary files /dev/null and b/src/assets/quiz-templates/health/health-16.jpg differ diff --git a/src/assets/quiz-templates/health/health-17.jpg b/src/assets/quiz-templates/health/health-17.jpg new file mode 100644 index 00000000..a9621787 Binary files /dev/null and b/src/assets/quiz-templates/health/health-17.jpg differ diff --git a/src/assets/quiz-templates/health/health-18.jpg b/src/assets/quiz-templates/health/health-18.jpg new file mode 100644 index 00000000..4225df96 Binary files /dev/null and b/src/assets/quiz-templates/health/health-18.jpg differ diff --git a/src/assets/quiz-templates/health/health-19.jpg b/src/assets/quiz-templates/health/health-19.jpg new file mode 100644 index 00000000..e2707907 Binary files /dev/null and b/src/assets/quiz-templates/health/health-19.jpg differ diff --git a/src/assets/quiz-templates/health/health-2.jpg b/src/assets/quiz-templates/health/health-2.jpg new file mode 100644 index 00000000..1ee282a7 Binary files /dev/null and b/src/assets/quiz-templates/health/health-2.jpg differ diff --git a/src/assets/quiz-templates/health/health-20.jpg b/src/assets/quiz-templates/health/health-20.jpg new file mode 100644 index 00000000..c99d10cd Binary files /dev/null and b/src/assets/quiz-templates/health/health-20.jpg differ diff --git a/src/assets/quiz-templates/health/health-3.jpg b/src/assets/quiz-templates/health/health-3.jpg new file mode 100644 index 00000000..22c0ec91 Binary files /dev/null and b/src/assets/quiz-templates/health/health-3.jpg differ diff --git a/src/assets/quiz-templates/health/health-4.jpg b/src/assets/quiz-templates/health/health-4.jpg new file mode 100644 index 00000000..204d518f Binary files /dev/null and b/src/assets/quiz-templates/health/health-4.jpg differ diff --git a/src/assets/quiz-templates/health/health-5.jpg b/src/assets/quiz-templates/health/health-5.jpg new file mode 100644 index 00000000..6fe60567 Binary files /dev/null and b/src/assets/quiz-templates/health/health-5.jpg differ diff --git a/src/assets/quiz-templates/health/health-6.jpg b/src/assets/quiz-templates/health/health-6.jpg new file mode 100644 index 00000000..6b742ba6 Binary files /dev/null and b/src/assets/quiz-templates/health/health-6.jpg differ diff --git a/src/assets/quiz-templates/health/health-7.jpg b/src/assets/quiz-templates/health/health-7.jpg new file mode 100644 index 00000000..25a40b9a Binary files /dev/null and b/src/assets/quiz-templates/health/health-7.jpg differ diff --git a/src/assets/quiz-templates/health/health-8.jpg b/src/assets/quiz-templates/health/health-8.jpg new file mode 100644 index 00000000..f777831f Binary files /dev/null and b/src/assets/quiz-templates/health/health-8.jpg differ diff --git a/src/assets/quiz-templates/health/health-9.jpg b/src/assets/quiz-templates/health/health-9.jpg new file mode 100644 index 00000000..fdb1d6b0 Binary files /dev/null and b/src/assets/quiz-templates/health/health-9.jpg differ diff --git a/src/assets/quiz-templates/production/production-1.jpg b/src/assets/quiz-templates/production/production-1.jpg new file mode 100644 index 00000000..552e5424 Binary files /dev/null and b/src/assets/quiz-templates/production/production-1.jpg differ diff --git a/src/assets/quiz-templates/production/production-10.jpg b/src/assets/quiz-templates/production/production-10.jpg new file mode 100644 index 00000000..da3b0e6d Binary files /dev/null and b/src/assets/quiz-templates/production/production-10.jpg differ diff --git a/src/assets/quiz-templates/production/production-2.jpg b/src/assets/quiz-templates/production/production-2.jpg new file mode 100644 index 00000000..ff72fd2d Binary files /dev/null and b/src/assets/quiz-templates/production/production-2.jpg differ diff --git a/src/assets/quiz-templates/production/production-3.jpg b/src/assets/quiz-templates/production/production-3.jpg new file mode 100644 index 00000000..48861a81 Binary files /dev/null and b/src/assets/quiz-templates/production/production-3.jpg differ diff --git a/src/assets/quiz-templates/production/production-4.jpg b/src/assets/quiz-templates/production/production-4.jpg new file mode 100644 index 00000000..7bc8fb01 Binary files /dev/null and b/src/assets/quiz-templates/production/production-4.jpg differ diff --git a/src/assets/quiz-templates/production/production-5.jpg b/src/assets/quiz-templates/production/production-5.jpg new file mode 100644 index 00000000..587d05a3 Binary files /dev/null and b/src/assets/quiz-templates/production/production-5.jpg differ diff --git a/src/assets/quiz-templates/production/production-6.jpg b/src/assets/quiz-templates/production/production-6.jpg new file mode 100644 index 00000000..e0316fdb Binary files /dev/null and b/src/assets/quiz-templates/production/production-6.jpg differ diff --git a/src/assets/quiz-templates/production/production-7.jpg b/src/assets/quiz-templates/production/production-7.jpg new file mode 100644 index 00000000..4381c548 Binary files /dev/null and b/src/assets/quiz-templates/production/production-7.jpg differ diff --git a/src/assets/quiz-templates/production/production-8.jpg b/src/assets/quiz-templates/production/production-8.jpg new file mode 100644 index 00000000..fd9095f6 Binary files /dev/null and b/src/assets/quiz-templates/production/production-8.jpg differ diff --git a/src/assets/quiz-templates/production/production-9.jpg b/src/assets/quiz-templates/production/production-9.jpg new file mode 100644 index 00000000..9c7599f3 Binary files /dev/null and b/src/assets/quiz-templates/production/production-9.jpg differ diff --git a/src/assets/quiz-templates/real-estate/real-estate-1.jpg b/src/assets/quiz-templates/real-estate/real-estate-1.jpg new file mode 100644 index 00000000..bb2bee4a Binary files /dev/null and b/src/assets/quiz-templates/real-estate/real-estate-1.jpg differ diff --git a/src/assets/quiz-templates/real-estate/real-estate-10.jpg b/src/assets/quiz-templates/real-estate/real-estate-10.jpg new file mode 100644 index 00000000..d3056e71 Binary files /dev/null and b/src/assets/quiz-templates/real-estate/real-estate-10.jpg differ diff --git a/src/assets/quiz-templates/real-estate/real-estate-2.jpg b/src/assets/quiz-templates/real-estate/real-estate-2.jpg new file mode 100644 index 00000000..2e888341 Binary files /dev/null and b/src/assets/quiz-templates/real-estate/real-estate-2.jpg differ diff --git a/src/assets/quiz-templates/real-estate/real-estate-3.jpg b/src/assets/quiz-templates/real-estate/real-estate-3.jpg new file mode 100644 index 00000000..4138b906 Binary files /dev/null and b/src/assets/quiz-templates/real-estate/real-estate-3.jpg differ diff --git a/src/assets/quiz-templates/real-estate/real-estate-4.jpg b/src/assets/quiz-templates/real-estate/real-estate-4.jpg new file mode 100644 index 00000000..a127edae Binary files /dev/null and b/src/assets/quiz-templates/real-estate/real-estate-4.jpg differ diff --git a/src/assets/quiz-templates/real-estate/real-estate-5.jpg b/src/assets/quiz-templates/real-estate/real-estate-5.jpg new file mode 100644 index 00000000..cc166feb Binary files /dev/null and b/src/assets/quiz-templates/real-estate/real-estate-5.jpg differ diff --git a/src/assets/quiz-templates/real-estate/real-estate-6.jpg b/src/assets/quiz-templates/real-estate/real-estate-6.jpg new file mode 100644 index 00000000..37956278 Binary files /dev/null and b/src/assets/quiz-templates/real-estate/real-estate-6.jpg differ diff --git a/src/assets/quiz-templates/real-estate/real-estate-7.jpg b/src/assets/quiz-templates/real-estate/real-estate-7.jpg new file mode 100644 index 00000000..2d2b5f9b Binary files /dev/null and b/src/assets/quiz-templates/real-estate/real-estate-7.jpg differ diff --git a/src/assets/quiz-templates/real-estate/real-estate-8.jpg b/src/assets/quiz-templates/real-estate/real-estate-8.jpg new file mode 100644 index 00000000..dc04f3a8 Binary files /dev/null and b/src/assets/quiz-templates/real-estate/real-estate-8.jpg differ diff --git a/src/assets/quiz-templates/real-estate/real-estate-9.jpg b/src/assets/quiz-templates/real-estate/real-estate-9.jpg new file mode 100644 index 00000000..52166193 Binary files /dev/null and b/src/assets/quiz-templates/real-estate/real-estate-9.jpg differ diff --git a/src/assets/quiz-templates/repair/repair-1.jpg b/src/assets/quiz-templates/repair/repair-1.jpg new file mode 100644 index 00000000..955aaa4c Binary files /dev/null and b/src/assets/quiz-templates/repair/repair-1.jpg differ diff --git a/src/assets/quiz-templates/repair/repair-10.jpg b/src/assets/quiz-templates/repair/repair-10.jpg new file mode 100644 index 00000000..d5fb0a01 Binary files /dev/null and b/src/assets/quiz-templates/repair/repair-10.jpg differ diff --git a/src/assets/quiz-templates/repair/repair-2.jpg b/src/assets/quiz-templates/repair/repair-2.jpg new file mode 100644 index 00000000..2dcc183c Binary files /dev/null and b/src/assets/quiz-templates/repair/repair-2.jpg differ diff --git a/src/assets/quiz-templates/repair/repair-3.jpg b/src/assets/quiz-templates/repair/repair-3.jpg new file mode 100644 index 00000000..4d458251 Binary files /dev/null and b/src/assets/quiz-templates/repair/repair-3.jpg differ diff --git a/src/assets/quiz-templates/repair/repair-4.jpg b/src/assets/quiz-templates/repair/repair-4.jpg new file mode 100644 index 00000000..1559a542 Binary files /dev/null and b/src/assets/quiz-templates/repair/repair-4.jpg differ diff --git a/src/assets/quiz-templates/repair/repair-5.jpg b/src/assets/quiz-templates/repair/repair-5.jpg new file mode 100644 index 00000000..5a27a6c4 Binary files /dev/null and b/src/assets/quiz-templates/repair/repair-5.jpg differ diff --git a/src/assets/quiz-templates/repair/repair-6.jpg b/src/assets/quiz-templates/repair/repair-6.jpg new file mode 100644 index 00000000..5e44c5b9 Binary files /dev/null and b/src/assets/quiz-templates/repair/repair-6.jpg differ diff --git a/src/assets/quiz-templates/repair/repair-7.jpg b/src/assets/quiz-templates/repair/repair-7.jpg new file mode 100644 index 00000000..f58f34c0 Binary files /dev/null and b/src/assets/quiz-templates/repair/repair-7.jpg differ diff --git a/src/assets/quiz-templates/repair/repair-8.jpg b/src/assets/quiz-templates/repair/repair-8.jpg new file mode 100644 index 00000000..7a07141e Binary files /dev/null and b/src/assets/quiz-templates/repair/repair-8.jpg differ diff --git a/src/assets/quiz-templates/repair/repair-9.jpg b/src/assets/quiz-templates/repair/repair-9.jpg new file mode 100644 index 00000000..a0a44e86 Binary files /dev/null and b/src/assets/quiz-templates/repair/repair-9.jpg differ diff --git a/src/assets/quiz-templates/research/research-1.jpg b/src/assets/quiz-templates/research/research-1.jpg new file mode 100644 index 00000000..c2880274 Binary files /dev/null and b/src/assets/quiz-templates/research/research-1.jpg differ diff --git a/src/assets/quiz-templates/research/research-10.jpg b/src/assets/quiz-templates/research/research-10.jpg new file mode 100644 index 00000000..302be762 Binary files /dev/null and b/src/assets/quiz-templates/research/research-10.jpg differ diff --git a/src/assets/quiz-templates/research/research-2.jpg b/src/assets/quiz-templates/research/research-2.jpg new file mode 100644 index 00000000..db853cd9 Binary files /dev/null and b/src/assets/quiz-templates/research/research-2.jpg differ diff --git a/src/assets/quiz-templates/research/research-3.jpg b/src/assets/quiz-templates/research/research-3.jpg new file mode 100644 index 00000000..a9a7cf02 Binary files /dev/null and b/src/assets/quiz-templates/research/research-3.jpg differ diff --git a/src/assets/quiz-templates/research/research-4.jpg b/src/assets/quiz-templates/research/research-4.jpg new file mode 100644 index 00000000..a2060d11 Binary files /dev/null and b/src/assets/quiz-templates/research/research-4.jpg differ diff --git a/src/assets/quiz-templates/research/research-5.jpg b/src/assets/quiz-templates/research/research-5.jpg new file mode 100644 index 00000000..84cb8e6c Binary files /dev/null and b/src/assets/quiz-templates/research/research-5.jpg differ diff --git a/src/assets/quiz-templates/research/research-6.jpg b/src/assets/quiz-templates/research/research-6.jpg new file mode 100644 index 00000000..3a3c4894 Binary files /dev/null and b/src/assets/quiz-templates/research/research-6.jpg differ diff --git a/src/assets/quiz-templates/research/research-7.jpg b/src/assets/quiz-templates/research/research-7.jpg new file mode 100644 index 00000000..02ec0b83 Binary files /dev/null and b/src/assets/quiz-templates/research/research-7.jpg differ diff --git a/src/assets/quiz-templates/research/research-8.jpg b/src/assets/quiz-templates/research/research-8.jpg new file mode 100644 index 00000000..06cf702c Binary files /dev/null and b/src/assets/quiz-templates/research/research-8.jpg differ diff --git a/src/assets/quiz-templates/research/research-9.jpg b/src/assets/quiz-templates/research/research-9.jpg new file mode 100644 index 00000000..884cb602 Binary files /dev/null and b/src/assets/quiz-templates/research/research-9.jpg differ diff --git a/src/assets/quiz-templates/services/service-1.jpg b/src/assets/quiz-templates/services/service-1.jpg new file mode 100644 index 00000000..51a36fb4 Binary files /dev/null and b/src/assets/quiz-templates/services/service-1.jpg differ diff --git a/src/assets/quiz-templates/services/service-10.jpg b/src/assets/quiz-templates/services/service-10.jpg new file mode 100644 index 00000000..a2303dff Binary files /dev/null and b/src/assets/quiz-templates/services/service-10.jpg differ diff --git a/src/assets/quiz-templates/services/service-11.jpg b/src/assets/quiz-templates/services/service-11.jpg new file mode 100644 index 00000000..15111b28 Binary files /dev/null and b/src/assets/quiz-templates/services/service-11.jpg differ diff --git a/src/assets/quiz-templates/services/service-2.jpg b/src/assets/quiz-templates/services/service-2.jpg new file mode 100644 index 00000000..08140977 Binary files /dev/null and b/src/assets/quiz-templates/services/service-2.jpg differ diff --git a/src/assets/quiz-templates/services/service-3.jpg b/src/assets/quiz-templates/services/service-3.jpg new file mode 100644 index 00000000..96b87209 Binary files /dev/null and b/src/assets/quiz-templates/services/service-3.jpg differ diff --git a/src/assets/quiz-templates/services/service-4.jpg b/src/assets/quiz-templates/services/service-4.jpg new file mode 100644 index 00000000..a04654e3 Binary files /dev/null and b/src/assets/quiz-templates/services/service-4.jpg differ diff --git a/src/assets/quiz-templates/services/service-5.jpg b/src/assets/quiz-templates/services/service-5.jpg new file mode 100644 index 00000000..8879f78e Binary files /dev/null and b/src/assets/quiz-templates/services/service-5.jpg differ diff --git a/src/assets/quiz-templates/services/service-6.jpg b/src/assets/quiz-templates/services/service-6.jpg new file mode 100644 index 00000000..aabcbbe4 Binary files /dev/null and b/src/assets/quiz-templates/services/service-6.jpg differ diff --git a/src/assets/quiz-templates/services/service-7.jpg b/src/assets/quiz-templates/services/service-7.jpg new file mode 100644 index 00000000..2abf2b63 Binary files /dev/null and b/src/assets/quiz-templates/services/service-7.jpg differ diff --git a/src/assets/quiz-templates/services/service-8.jpg b/src/assets/quiz-templates/services/service-8.jpg new file mode 100644 index 00000000..20d3bb74 Binary files /dev/null and b/src/assets/quiz-templates/services/service-8.jpg differ diff --git a/src/assets/quiz-templates/services/service-9.jpg b/src/assets/quiz-templates/services/service-9.jpg new file mode 100644 index 00000000..26556403 Binary files /dev/null and b/src/assets/quiz-templates/services/service-9.jpg differ diff --git a/src/assets/quiz-templates/template.svg b/src/assets/quiz-templates/template.svg new file mode 100644 index 00000000..5ab271a0 --- /dev/null +++ b/src/assets/quiz-templates/template.svg @@ -0,0 +1,15 @@ + + + diff --git a/src/assets/quiz-templates/tourism/tourism-1.jpg b/src/assets/quiz-templates/tourism/tourism-1.jpg new file mode 100644 index 00000000..f4c788be Binary files /dev/null and b/src/assets/quiz-templates/tourism/tourism-1.jpg differ diff --git a/src/assets/quiz-templates/tourism/tourism-10.jpg b/src/assets/quiz-templates/tourism/tourism-10.jpg new file mode 100644 index 00000000..c11ab435 Binary files /dev/null and b/src/assets/quiz-templates/tourism/tourism-10.jpg differ diff --git a/src/assets/quiz-templates/tourism/tourism-2.jpg b/src/assets/quiz-templates/tourism/tourism-2.jpg new file mode 100644 index 00000000..c6628bc7 Binary files /dev/null and b/src/assets/quiz-templates/tourism/tourism-2.jpg differ diff --git a/src/assets/quiz-templates/tourism/tourism-3.jpg b/src/assets/quiz-templates/tourism/tourism-3.jpg new file mode 100644 index 00000000..895d366c Binary files /dev/null and b/src/assets/quiz-templates/tourism/tourism-3.jpg differ diff --git a/src/assets/quiz-templates/tourism/tourism-4.jpg b/src/assets/quiz-templates/tourism/tourism-4.jpg new file mode 100644 index 00000000..863b593f Binary files /dev/null and b/src/assets/quiz-templates/tourism/tourism-4.jpg differ diff --git a/src/assets/quiz-templates/tourism/tourism-5.jpg b/src/assets/quiz-templates/tourism/tourism-5.jpg new file mode 100644 index 00000000..86913e09 Binary files /dev/null and b/src/assets/quiz-templates/tourism/tourism-5.jpg differ diff --git a/src/assets/quiz-templates/tourism/tourism-6.jpg b/src/assets/quiz-templates/tourism/tourism-6.jpg new file mode 100644 index 00000000..40912313 Binary files /dev/null and b/src/assets/quiz-templates/tourism/tourism-6.jpg differ diff --git a/src/assets/quiz-templates/tourism/tourism-7.jpg b/src/assets/quiz-templates/tourism/tourism-7.jpg new file mode 100644 index 00000000..25efa443 Binary files /dev/null and b/src/assets/quiz-templates/tourism/tourism-7.jpg differ diff --git a/src/assets/quiz-templates/tourism/tourism-8.jpg b/src/assets/quiz-templates/tourism/tourism-8.jpg new file mode 100644 index 00000000..a76a4437 Binary files /dev/null and b/src/assets/quiz-templates/tourism/tourism-8.jpg differ diff --git a/src/assets/quiz-templates/tourism/tourism-9.jpg b/src/assets/quiz-templates/tourism/tourism-9.jpg new file mode 100644 index 00000000..7db92d83 Binary files /dev/null and b/src/assets/quiz-templates/tourism/tourism-9.jpg differ diff --git a/src/assets/widget-preview.png b/src/assets/widget-preview.png new file mode 100644 index 00000000..7631e894 Binary files /dev/null and b/src/assets/widget-preview.png differ diff --git a/src/components/AmoButton/AmoButton.tsx b/src/components/AmoButton/AmoButton.tsx new file mode 100644 index 00000000..0c04b643 --- /dev/null +++ b/src/components/AmoButton/AmoButton.tsx @@ -0,0 +1,51 @@ +import { Button, Typography, useMediaQuery, useTheme } from "@mui/material"; +import AmoLogo from "../../assets/icons/Amologo.png"; +import { FC } from "react"; + +type AmoButtonProps = { + onClick?: () => void; +}; + +export const AmoButton: FC = ({ onClick }) => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down(600)); + return ( + + ); +}; diff --git a/src/components/CustomRadioGroup/CustomRadioGroup.tsx b/src/components/CustomRadioGroup/CustomRadioGroup.tsx index 3710f25d..db3fdafc 100644 --- a/src/components/CustomRadioGroup/CustomRadioGroup.tsx +++ b/src/components/CustomRadioGroup/CustomRadioGroup.tsx @@ -1,33 +1,111 @@ import * as React from "react"; -import { FC } from "react"; -import Radio from "@mui/material/Radio"; -import RadioGroup from "@mui/material/RadioGroup"; -import FormControlLabel from "@mui/material/FormControlLabel"; -import Box from "@mui/material/Box"; +import { FC, useMemo } from "react"; import CheckboxIcon from "@icons/Checkbox"; -import { useTheme } from "@mui/material"; +import { SelectChangeEvent, Typography, useTheme, Box, FormControlLabel, RadioGroup, Radio } from "@mui/material"; +import { MinifiedData, TagKeys } from "@/pages/IntegrationsPage/IntegrationsModal/types"; type CustomRadioGroupProps = { - items: string[]; - selectedValue: string | null; - setSelectedValue: (value: string | null) => void; + items: MinifiedData[] | []; + selectedItemId?: string | null; + setSelectedItem: (value: string | null) => void; + handleScroll: () => void; + activeScope?: TagKeys; }; export const CustomRadioGroup: FC = ({ items, - selectedValue, - setSelectedValue, + selectedItemId = "", + setSelectedItem, + handleScroll, + activeScope, }) => { const theme = useTheme(); - const [currentValue, setCurrentValue] = React.useState( - selectedValue, - ); - const handleChange = (event: React.ChangeEvent) => { - setSelectedValue((event.target as HTMLInputElement).value); - setCurrentValue((event.target as HTMLInputElement).value); - }; + + const currentItem = useMemo(() => { + if (selectedItemId !== null && selectedItemId.length > 0) { + return items.find((item) => item.id === selectedItemId) || null; + } + return null; + }, [selectedItemId, items]); + + const filteredItems = useMemo(() => { + let newArray = items; + if (activeScope !== undefined) + newArray = newArray.filter((item) => { + return item.entity === activeScope; + }); + return newArray; + }, items); + + const onScroll = React.useCallback((e: React.UIEvent) => { + const scrollHeight = e.currentTarget.scrollHeight; + const scrollTop = e.currentTarget.scrollTop; + const clientHeight = e.currentTarget.clientHeight; + const scrolledToBottom = scrollTop / (scrollHeight - clientHeight) > 0.9; + + if (scrolledToBottom) { + handleScroll(); + } + }, []); + + const formControlLabels = useMemo(() => { + if (filteredItems.length !== 0) { + return filteredItems.map((item) => ( + .MuiTypography-root": { + width: "200px", + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap" + }, + }} + value={item.id} + control={ + + } + icon={} + /> + } + label={item.title} + labelPlacement={"start"} + /> + )); + } + return ( + + Нет элементов + + ); + }, [filteredItems, selectedItemId]); + return ( = ({ ) => setSelectedItem(target.value)} + onScroll={onScroll} > - {items.map((item) => ( - - } - icon={} - /> - } - label={item} - labelPlacement={"start"} - /> - ))} + {formControlLabels} ); diff --git a/src/components/CustomSelect/CustomSelect.css b/src/components/CustomSelect/CustomSelect.css deleted file mode 100644 index e16e64cc..00000000 --- a/src/components/CustomSelect/CustomSelect.css +++ /dev/null @@ -1,22 +0,0 @@ -.MuiOutlinedInput-root .MuiOutlinedInput-notchedOutline { - border: 0; -} - -.MuiPaper-root.MuiMenu-paper { - padding-top: 50px; - margin-top: -50px; - border-radius: 28px; -} - -.MuiInputBase-root.MuiOutlinedInput-root { - display: block; -} - -.MuiInputBase-root.MuiOutlinedInput-root > div:first-child, -.MuiInputBase-root.MuiOutlinedInput-root .MuiSelect-icon { - display: none; -} - -.MuiMenu-root.MuiModal-root { - z-index: 0; -} diff --git a/src/components/CustomSelect/CustomSelect.tsx b/src/components/CustomSelect/CustomSelect.tsx index c98c5d55..a2c7c719 100644 --- a/src/components/CustomSelect/CustomSelect.tsx +++ b/src/components/CustomSelect/CustomSelect.tsx @@ -1,47 +1,97 @@ -import { - Avatar, - MenuItem, - Select, - SelectChangeEvent, - Typography, - useMediaQuery, - useTheme, -} from "@mui/material"; -import Box from "@mui/material/Box"; -import { FC, useCallback, useRef, useState } from "react"; -import "./CustomSelect.css"; +import * as React from "react"; +import { FC, useCallback, useMemo, useRef, useState } from "react"; +import { Avatar, MenuItem, Select, SelectChangeEvent, Typography, useMediaQuery, useTheme, Box } from "@mui/material"; import arrow_down from "../../assets/icons/arrow_down.svg"; +import { MinifiedData } from "@/pages/IntegrationsPage/IntegrationsModal/types"; type CustomSelectProps = { - items: string[]; - selectedItem: string | null; + items: MinifiedData[] | []; + selectedItemId: string | null; setSelectedItem: (value: string | null) => void; + handleScroll: () => void; }; -export const CustomSelect: FC = ({ - items, - selectedItem, - setSelectedItem, -}) => { +export const CustomSelect: FC = ({ items, selectedItemId, setSelectedItem, handleScroll }) => { const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down(600)); - const [opened, setOpened] = useState(false); - const [currentValue, setCurrentValue] = useState(selectedItem); - const ref = useRef(null); - const onSelectItem = useCallback( - (event: SelectChangeEvent) => { - const newValue = event.target.value.toString(); - setCurrentValue(newValue); - setSelectedItem(newValue); - }, - [setSelectedItem, setCurrentValue], - ); + const ref = useRef(null); + const [opened, setOpened] = useState(false); const toggleOpened = useCallback(() => { setOpened((isOpened) => !isOpened); }, []); + const onScroll = useCallback((e: React.UIEvent) => { + const scrollHeight = e.currentTarget.scrollHeight; + const scrollTop = e.currentTarget.scrollTop; + const clientHeight = e.currentTarget.clientHeight; + const scrolledToBottom = scrollTop / (scrollHeight - clientHeight) > 0.9; + + if (scrolledToBottom) { + handleScroll(); + } + }, []); + + const currentItem = useMemo(() => items.find((item) => item.id === selectedItemId) || null, [selectedItemId, items]); + + const menuItems = useMemo(() => { + if (items.length !== 0) { + return items.map((item) => ( + + + + {item.title} + + + {item.subTitle} + + + + )); + } + return ( + + нет данных + + ); + }, [items, selectedItemId]); + return ( = ({ width: "100%", height: "56px", padding: "5px", - color: - currentValue === null - ? theme.palette.grey2.main - : theme.palette.brightPurple.main, + color: currentItem === null ? theme.palette.grey2.main : theme.palette.brightPurple.main, border: `2px solid ${theme.palette.common.white}`, borderRadius: "30px", background: "#EFF0F5", @@ -62,13 +109,15 @@ export const CustomSelect: FC = ({ alignItems: "center", cursor: "pointer", }} - onClick={() => ref.current?.click()} + onClick={() => { + if (ref.current !== null) ref.current?.click(); + }} > = ({ flexGrow: 1, }} > - {currentValue || "Выберите ответственного за сделку"} + {currentItem?.title || "Выберите ответственного за сделку"} = ({ ); diff --git a/src/components/Dummys/AvailablePrivilegeDummy.tsx b/src/components/Dummys/AvailablePrivilegeDummy.tsx new file mode 100644 index 00000000..1c9dfe19 --- /dev/null +++ b/src/components/Dummys/AvailablePrivilegeDummy.tsx @@ -0,0 +1,25 @@ +import {Box, Skeleton, Typography} from "@mui/material"; +import React from "react"; + +export default function AvailablePrivilegeDummy() { + return ( + + + Вам доступно: + + + + + + ); +} diff --git a/src/components/Dummys/QuizCardDummy.tsx b/src/components/Dummys/QuizCardDummy.tsx new file mode 100644 index 00000000..e89c5d6a --- /dev/null +++ b/src/components/Dummys/QuizCardDummy.tsx @@ -0,0 +1,126 @@ +import {Box, Button, IconButton, Skeleton, Typography, useMediaQuery, useTheme} from "@mui/material"; +import LinkIcon from "@icons/LinkIcon"; +import PencilIcon from "@icons/PencilIcon"; +import ChartIcon from "@icons/ChartIcon"; +import CopyIcon from "@icons/CopyIcon"; +import MoreHorizIcon from "@mui/icons-material/MoreHoriz"; +import React from "react"; + +export default function QuizCardDummy() { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down(600)); + return ( + + + + + + + + + + Открытий + + + + Заявок + + + + Конверсия + + + + + + + + + + + + + + + + + ); +} \ No newline at end of file diff --git a/src/components/Dummys/pageDummys/listPageDummy.tsx b/src/components/Dummys/pageDummys/listPageDummy.tsx new file mode 100644 index 00000000..5ee5c817 --- /dev/null +++ b/src/components/Dummys/pageDummys/listPageDummy.tsx @@ -0,0 +1,32 @@ +import { + Box, + Button, + SxProps, + Theme, + Typography, + useMediaQuery, + useTheme, +} from "@mui/material"; +import SectionWrapper from "@ui_kit/SectionWrapper"; +import React from "react"; +import HeaderFull from "@ui_kit/Header/HeaderFull"; +import QuizCardDummy from "../QuizCardDummy"; +import AvailablePrivilegeDummy from "../AvailablePrivilegeDummy"; + + +export default function ListPageDummy() { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down(500)); + + + return ( + <> + + + + + ); +} diff --git a/src/constants/base.ts b/src/constants/base.ts index 5d2d57a8..0d3e2673 100644 --- a/src/constants/base.ts +++ b/src/constants/base.ts @@ -1,7 +1,4 @@ -import type { - QuizQuestionBase, - QuestionBranchingRuleMain, -} from "../model/questionTypes/shared"; +import { QuizQuestionBase } from "@frontend/squzanswerer"; export const QUIZ_QUESTION_BASE: Omit = { quizId: 0, @@ -21,7 +18,7 @@ export const QUIZ_QUESTION_BASE: Omit = { }, rule: { children: [], - main: [] as QuestionBranchingRuleMain[], + main: [], parentId: "", default: "", }, diff --git a/src/constants/date.ts b/src/constants/date.ts index 6455fac2..a8a7264b 100644 --- a/src/constants/date.ts +++ b/src/constants/date.ts @@ -1,7 +1,6 @@ +import { QuizQuestionDate } from "@frontend/squzanswerer"; import { QUIZ_QUESTION_BASE } from "./base"; -import type { QuizQuestionDate } from "../model/questionTypes/date"; - export const QUIZ_QUESTION_DATE: Omit = { ...QUIZ_QUESTION_BASE, type: "date", diff --git a/src/constants/default.ts b/src/constants/default.ts index e746ee18..90071c57 100644 --- a/src/constants/default.ts +++ b/src/constants/default.ts @@ -1,5 +1,4 @@ import { QuestionType } from "@model/question/question"; -import { AnyTypedQuizQuestion } from "@model/questionTypes/shared"; import { QUIZ_QUESTION_DATE } from "./date"; import { QUIZ_QUESTION_EMOJI } from "./emoji"; import { QUIZ_QUESTION_FILE } from "./file"; @@ -12,11 +11,9 @@ import { QUIZ_QUESTION_TEXT } from "./text"; import { QUIZ_QUESTION_VARIANT } from "./variant"; import { QUIZ_QUESTION_VARIMG } from "./varimg"; import { QUIZ_QUESTION_RESULT } from "./result"; +import { AnyTypedQuizQuestion } from "@frontend/squzanswerer"; -export const defaultQuestionByType: Record< - QuestionType, - Omit -> = { +export const defaultQuestionByType: Record> = { date: QUIZ_QUESTION_DATE, emoji: QUIZ_QUESTION_EMOJI, file: QUIZ_QUESTION_FILE, diff --git a/src/constants/emoji.ts b/src/constants/emoji.ts index 67c8aa04..efcc6208 100644 --- a/src/constants/emoji.ts +++ b/src/constants/emoji.ts @@ -1,27 +1,26 @@ import { QUIZ_QUESTION_BASE } from "./base"; -import type { QuizQuestionEmoji } from "../model/questionTypes/emoji"; +import type { QuizQuestionEmoji } from "@frontend/squzanswerer"; import { nanoid } from "nanoid"; -export const QUIZ_QUESTION_EMOJI: Omit = - { - ...QUIZ_QUESTION_BASE, - type: "emoji", - content: { - ...QUIZ_QUESTION_BASE.content, - multi: false, - own: false, - innerNameCheck: false, - innerName: "", - required: false, - variants: [ - { - id: nanoid(), - answer: "", - extendedText: "", - hints: "", - originalImageUrl: "", - }, - ], - }, - }; +export const QUIZ_QUESTION_EMOJI: Omit = { + ...QUIZ_QUESTION_BASE, + type: "emoji", + content: { + ...QUIZ_QUESTION_BASE.content, + multi: false, + own: false, + innerNameCheck: false, + innerName: "", + required: false, + variants: [ + { + id: nanoid(), + answer: "", + extendedText: "", + hints: "", + originalImageUrl: "", + }, + ], + }, +}; diff --git a/src/constants/file.ts b/src/constants/file.ts index 8e36b232..b43a2277 100644 --- a/src/constants/file.ts +++ b/src/constants/file.ts @@ -1,6 +1,6 @@ import { QUIZ_QUESTION_BASE } from "./base"; -import type { QuizQuestionFile } from "../model/questionTypes/file"; +import type { QuizQuestionFile } from "@frontend/squzanswerer"; export const QUIZ_QUESTION_FILE: Omit = { ...QUIZ_QUESTION_BASE, diff --git a/src/constants/images.ts b/src/constants/images.ts index 974b1cc1..ec1f2526 100644 --- a/src/constants/images.ts +++ b/src/constants/images.ts @@ -1,12 +1,9 @@ import { QUIZ_QUESTION_BASE } from "./base"; -import type { QuizQuestionImages } from "../model/questionTypes/images"; +import type { QuizQuestionImages } from "@frontend/squzanswerer"; import { nanoid } from "nanoid"; -export const QUIZ_QUESTION_IMAGES: Omit< - QuizQuestionImages, - "id" | "backendId" -> = { +export const QUIZ_QUESTION_IMAGES: Omit = { ...QUIZ_QUESTION_BASE, type: "images", content: { diff --git a/src/constants/number.ts b/src/constants/number.ts index f2fc09ae..5ce97b7f 100644 --- a/src/constants/number.ts +++ b/src/constants/number.ts @@ -1,11 +1,8 @@ import { QUIZ_QUESTION_BASE } from "./base"; -import type { QuizQuestionNumber } from "../model/questionTypes/number"; +import type { QuizQuestionNumber } from "@frontend/squzanswerer"; -export const QUIZ_QUESTION_NUMBER: Omit< - QuizQuestionNumber, - "id" | "backendId" -> = { +export const QUIZ_QUESTION_NUMBER: Omit = { ...QUIZ_QUESTION_BASE, type: "number", content: { diff --git a/src/constants/page.ts b/src/constants/page.ts index 5fbb2965..1fb653cc 100644 --- a/src/constants/page.ts +++ b/src/constants/page.ts @@ -1,6 +1,6 @@ import { QUIZ_QUESTION_BASE } from "./base"; -import type { QuizQuestionPage } from "../model/questionTypes/page"; +import type { QuizQuestionPage } from "@frontend/squzanswerer"; export const QUIZ_QUESTION_PAGE: Omit = { ...QUIZ_QUESTION_BASE, @@ -13,5 +13,6 @@ export const QUIZ_QUESTION_PAGE: Omit = { picture: "", originalPicture: "", video: "", + useImage: true, }, }; diff --git a/src/constants/questions.dummy.ts b/src/constants/questions.dummy.ts deleted file mode 100644 index b4a747a3..00000000 --- a/src/constants/questions.dummy.ts +++ /dev/null @@ -1,290 +0,0 @@ -import type { AnyTypedQuizQuestion } from "../model/questionTypes/shared"; - -export const QUESTIONS_DUMMY: AnyTypedQuizQuestion[] = [ - { - id: "1", - title: "", - type: "variant", - expanded: true, - required: false, - deleted: false, - deleteTimeoutId: 0, - backendId: 1111, - description: "", - openedModalSettings: false, - page: 1, - quizId: 1, - content: { - hint: { - text: "", - video: "", - }, - rule: { - main: [], - default: "1", - }, - back: "", - originalBack: "", - autofill: false, - largeCheck: false, - multi: false, - own: false, - innerNameCheck: false, - required: false, - innerName: "", - variants: [], - }, - }, - { - id: "2", - title: "Вы идёте в школу", - type: "page", - expanded: true, - required: false, - deleted: false, - deleteTimeoutId: 0, - backendId: 1112, - description: "", - openedModalSettings: false, - page: 1, - quizId: 1, - content: { - hint: { - text: "", - video: "", - }, - rule: { - main: [], - default: "1", - }, - back: "", - originalBack: "", - autofill: false, - - innerNameCheck: false, - innerName: "", - text: "", - picture: "", - originalPicture: "", - video: "", - }, - }, - { - id: "3", - title: "Вы немного опоздали, как поступите?", - type: "variant", - expanded: true, - required: true, - deleted: false, - deleteTimeoutId: 0, - backendId: 1113, - description: "", - openedModalSettings: false, - page: 1, - quizId: 1, - content: { - hint: { - text: "", - video: "", - }, - rule: { - main: [], - default: "2", - }, - back: "", - originalBack: "", - autofill: false, - largeCheck: false, - multi: false, - own: false, - innerNameCheck: false, - required: false, - innerName: "", - variants: [ - { - id: "answer1", - answer: "Извинюсь за опоздание и займу своё место", - extendedText: "", - hints: "", - }, - { - id: "answer2", - answer: - "Вынесу дверь с ноги и распинав всех направлюсь к месту у розетки", - extendedText: "", - hints: "", - }, - { - id: "answer3", - answer: "Влечу в кабинет, пробегу его и выпрыгну в окно", - extendedText: "", - hints: "", - }, - ], - }, - }, - { - id: "4", - title: "Вы открываете дверь кабинета, приготовившись исполнить задуманное", - type: "page", - expanded: true, - required: false, - deleted: false, - deleteTimeoutId: 0, - backendId: 1114, - description: "", - openedModalSettings: false, - page: 1, - quizId: 1, - content: { - hint: { - text: "", - video: "", - }, - rule: { - main: [ - //момент выбора куда мы пойдём - { - next: "7", - or: true, - rules: [ - { - question: "2", - answers: ["answer1"], - }, - ], - }, - { - next: "6", - or: true, - rules: [ - { - question: "2", - answers: ["answer2"], - }, - ], - }, - { - next: "5", - or: true, - rules: [ - { - question: "2", - answers: ["answer3"], - }, - ], - }, - ], - default: "2", - }, - back: "", - originalBack: "", - autofill: false, - innerNameCheck: false, - innerName: "", - text: "", - picture: "", - originalPicture: "", - video: "", - }, - }, - { - id: "5", - title: "Вы умерли", - type: "page", - expanded: true, - required: false, - deleted: false, - deleteTimeoutId: 0, - backendId: 1115, - description: "", - openedModalSettings: false, - page: 1, - quizId: 1, - content: { - hint: { - text: "", - video: "", - }, - rule: { - main: [], - default: "", - }, - back: "", - originalBack: "", - autofill: false, - innerNameCheck: false, - innerName: "", - text: "", - picture: "", - originalPicture: "", - video: "", - }, - }, - { - id: "6", - title: "Вас вызвали к директору", - type: "page", - expanded: true, - required: false, - deleted: false, - deleteTimeoutId: 0, - backendId: 1116, - description: "", - openedModalSettings: false, - page: 1, - quizId: 1, - content: { - hint: { - text: "", - video: "", - }, - rule: { - main: [], - default: "", - }, - back: "", - originalBack: "", - autofill: false, - innerNameCheck: false, - innerName: "", - text: "", - picture: "", - originalPicture: "", - video: "", - }, - }, - { - id: "7", - title: "Вы получили отлично", - type: "page", - expanded: true, - required: false, - deleted: false, - deleteTimeoutId: 0, - backendId: 1117, - description: "", - openedModalSettings: false, - page: 1, - quizId: 1, - content: { - hint: { - text: "", - video: "", - }, - rule: { - main: [], - default: "", - }, - back: "", - originalBack: "", - autofill: false, - innerNameCheck: false, - innerName: "", - text: "", - picture: "", - originalPicture: "", - video: "", - }, - }, -]; diff --git a/src/constants/rating.ts b/src/constants/rating.ts index 3ff92c02..0dab34f5 100644 --- a/src/constants/rating.ts +++ b/src/constants/rating.ts @@ -1,11 +1,8 @@ import { QUIZ_QUESTION_BASE } from "./base"; -import type { QuizQuestionRating } from "../model/questionTypes/rating"; +import type { QuizQuestionRating } from "@frontend/squzanswerer"; -export const QUIZ_QUESTION_RATING: Omit< - QuizQuestionRating, - "id" | "backendId" -> = { +export const QUIZ_QUESTION_RATING: Omit = { ...QUIZ_QUESTION_BASE, type: "rating", content: { diff --git a/src/constants/result.ts b/src/constants/result.ts index ab8fa049..86d276f1 100644 --- a/src/constants/result.ts +++ b/src/constants/result.ts @@ -1,12 +1,8 @@ import { QUIZ_QUESTION_BASE } from "./base"; -import type { QuizQuestionResult } from "../model/questionTypes/result"; -import { nanoid } from "nanoid"; +import type { QuizQuestionResult } from "@frontend/squzanswerer"; -export const QUIZ_QUESTION_RESULT: Omit< - QuizQuestionResult, - "id" | "backendId" -> = { +export const QUIZ_QUESTION_RESULT: Omit = { ...QUIZ_QUESTION_BASE, type: "result", content: { diff --git a/src/constants/select.ts b/src/constants/select.ts index 8f139aa2..9aac719e 100644 --- a/src/constants/select.ts +++ b/src/constants/select.ts @@ -1,12 +1,9 @@ import { QUIZ_QUESTION_BASE } from "./base"; -import type { QuizQuestionSelect } from "../model/questionTypes/select"; +import type { QuizQuestionSelect } from "@frontend/squzanswerer"; import { nanoid } from "nanoid"; -export const QUIZ_QUESTION_SELECT: Omit< - QuizQuestionSelect, - "id" | "backendId" -> = { +export const QUIZ_QUESTION_SELECT: Omit = { ...QUIZ_QUESTION_BASE, type: "select", content: { diff --git a/src/constants/text.ts b/src/constants/text.ts index a64c4f9d..d647365a 100644 --- a/src/constants/text.ts +++ b/src/constants/text.ts @@ -1,6 +1,6 @@ import { QUIZ_QUESTION_BASE } from "./base"; -import type { QuizQuestionText } from "../model/questionTypes/text"; +import type { QuizQuestionText } from "@frontend/squzanswerer"; export const QUIZ_QUESTION_TEXT: Omit = { ...QUIZ_QUESTION_BASE, diff --git a/src/constants/variant.ts b/src/constants/variant.ts index 322dc169..bb513174 100644 --- a/src/constants/variant.ts +++ b/src/constants/variant.ts @@ -1,12 +1,9 @@ import { QUIZ_QUESTION_BASE } from "./base"; -import type { QuizQuestionVariant } from "../model/questionTypes/variant"; +import type { QuizQuestionVariant } from "@frontend/squzanswerer"; import { nanoid } from "nanoid"; -export const QUIZ_QUESTION_VARIANT: Omit< - QuizQuestionVariant, - "id" | "backendId" -> = { +export const QUIZ_QUESTION_VARIANT: Omit = { ...QUIZ_QUESTION_BASE, type: "variant", content: { diff --git a/src/constants/varimg.ts b/src/constants/varimg.ts index 64ed0e46..c9804006 100644 --- a/src/constants/varimg.ts +++ b/src/constants/varimg.ts @@ -1,12 +1,9 @@ import { QUIZ_QUESTION_BASE } from "./base"; -import type { QuizQuestionVarImg } from "../model/questionTypes/varimg"; +import type { QuizQuestionVarImg } from "@frontend/squzanswerer"; import { nanoid } from "nanoid"; -export const QUIZ_QUESTION_VARIMG: Omit< - QuizQuestionVarImg, - "id" | "backendId" -> = { +export const QUIZ_QUESTION_VARIMG: Omit = { ...QUIZ_QUESTION_BASE, type: "varimg", content: { diff --git a/src/index.tsx b/src/index.tsx index 0e4fd069..3e3fe6e1 100755 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,10 +1,8 @@ import { CssBaseline, ThemeProvider, Button } from "@mui/material"; import { LocalizationProvider } from "@mui/x-date-pickers"; -import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; +import { AdapterMoment } from "@mui/x-date-pickers/AdapterMoment"; import { ruRU } from "@mui/x-date-pickers/locales"; import App from "./App"; -import dayjs from "dayjs"; -import "dayjs/locale/ru"; import { SnackbarProvider, closeSnackbar } from "notistack"; import { DndProvider } from "react-dnd"; import { HTML5Backend } from "react-dnd-html5-backend"; @@ -20,7 +18,6 @@ import CloseIcon from "@icons/CloseBold"; import type { SnackbarKey } from "notistack"; import { CheckFastlink } from "@ui_kit/CheckFastlink"; -dayjs.locale("ru"); moment.locale("ru"); polyfillCountryFlagEmojis(); const localeText = @@ -50,7 +47,7 @@ root.render( > diff --git a/src/model/question/edit.ts b/src/model/question/edit.ts index 3a283879..62665ed4 100644 --- a/src/model/question/edit.ts +++ b/src/model/question/edit.ts @@ -1,4 +1,4 @@ -import { AnyTypedQuizQuestion } from "@model/questionTypes/shared"; +import { AnyTypedQuizQuestion } from "@frontend/squzanswerer"; import { QuestionType } from "./question"; export interface EditQuestionRequest { @@ -15,9 +15,7 @@ export interface EditQuestionResponse { updated: number; } -export function questionToEditQuestionRequest( - question: AnyTypedQuizQuestion, -): EditQuestionRequest { +export function questionToEditQuestionRequest(question: AnyTypedQuizQuestion): EditQuestionRequest { return { id: question.backendId, title: question.title, diff --git a/src/model/question/question.ts b/src/model/question/question.ts index 955888e3..d0fea011 100644 --- a/src/model/question/question.ts +++ b/src/model/question/question.ts @@ -1,4 +1,4 @@ -import { AnyTypedQuizQuestion } from "@model/questionTypes/shared"; +import { AnyTypedQuizQuestion } from "@frontend/squzanswerer"; import { defaultQuestionByType } from "../../constants/default"; import { nanoid } from "nanoid"; @@ -44,21 +44,15 @@ export interface RawQuestion { updated_at: string; } -export function rawQuestionToQuestion( - rawQuestion: RawQuestion, -): AnyTypedQuizQuestion { +export function rawQuestionToQuestion(rawQuestion: RawQuestion): AnyTypedQuizQuestion { let content = defaultQuestionByType[rawQuestion.type].content; const frontId = nanoid(); try { content = JSON.parse(rawQuestion.content); - if (content.id.length === 0 || content.id.length === undefined) - content.id = frontId; + if (content.id.length === 0 || content.id.length === undefined) content.id = frontId; } catch (error) { - console.warn( - "Cannot parse question content from string, using default content", - error, - ); + console.warn("Cannot parse question content from string, using default content", error); } return { diff --git a/src/model/questionTypes/date.ts b/src/model/questionTypes/date.ts deleted file mode 100644 index 5c8935ff..00000000 --- a/src/model/questionTypes/date.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { QuizQuestionBase, QuestionHint, PreviewRule } from "./shared"; - -export interface QuizQuestionDate extends QuizQuestionBase { - type: "date"; - content: { - id: string; - /** Чекбокс "Необязательный вопрос" */ - required: boolean; - /** Чекбокс "Внутреннее название вопроса" */ - innerNameCheck: boolean; - /** Поле "Внутреннее название вопроса" */ - innerName: string; - dateRange: boolean; - time: boolean; - hint: QuestionHint; - rule: PreviewRule; - back: string; - originalBack: string; - autofill: boolean; - }; -} diff --git a/src/model/questionTypes/emoji.ts b/src/model/questionTypes/emoji.ts deleted file mode 100644 index c3cf8519..00000000 --- a/src/model/questionTypes/emoji.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { - QuizQuestionBase, - QuestionVariant, - QuestionHint, - PreviewRule, -} from "./shared"; - -export interface QuizQuestionEmoji extends QuizQuestionBase { - type: "emoji"; - content: { - id: string; - /** Чекбокс "Можно несколько" */ - multi: boolean; - /** Чекбокс "Вариант "свой ответ"" */ - own: boolean; - /** Чекбокс "Внутреннее название вопроса" */ - innerNameCheck: boolean; - /** Поле "Внутреннее название вопроса" */ - innerName: string; - /** Чекбокс "Необязательный вопрос" */ - required: boolean; - variants: QuestionVariant[]; - hint: QuestionHint; - rule: PreviewRule; - back: string; - originalBack: string; - autofill: boolean; - }; -} diff --git a/src/model/questionTypes/file.ts b/src/model/questionTypes/file.ts deleted file mode 100644 index 818cbb7c..00000000 --- a/src/model/questionTypes/file.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { QuizQuestionBase, QuestionHint, PreviewRule } from "./shared"; - -export const UPLOAD_FILE_TYPES_MAP = { - picture: "Изображения", - video: "Видео", - audio: "Аудио", - document: "Документ", -} as const; - -export type UploadFileType = keyof typeof UPLOAD_FILE_TYPES_MAP; - -export interface QuizQuestionFile extends QuizQuestionBase { - type: "file"; - content: { - id: string; - /** Чекбокс "Необязательный вопрос" */ - required: boolean; - /** Чекбокс "Внутреннее название вопроса" */ - innerNameCheck: boolean; - /** Поле "Внутреннее название вопроса" */ - innerName: string; - /** Чекбокс "Автозаполнение адреса" */ - autofill: boolean; - type: UploadFileType; - hint: QuestionHint; - rule: PreviewRule; - back: string; - originalBack: string; - }; -} diff --git a/src/model/questionTypes/images.ts b/src/model/questionTypes/images.ts deleted file mode 100644 index 35550ccb..00000000 --- a/src/model/questionTypes/images.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { - QuestionHint, - QuestionVariant, - QuizQuestionBase, - PreviewRule, -} from "./shared"; - -export interface QuizQuestionImages extends QuizQuestionBase { - type: "images"; - content: { - id: string; - /** Чекбокс "Вариант "свой ответ"" */ - own: boolean; - /** Чекбокс "Можно несколько" */ - multi: boolean; - /** Пропорции */ - xy: "1:1" | "1:2" | "2:1"; - /** Чекбокс "Внутреннее название вопроса" */ - innerNameCheck: boolean; - /** Поле "Внутреннее название вопроса" */ - innerName: string; - /** Чекбокс "Большие картинки" */ - large: boolean; - /** Форма */ - format: "carousel" | "masonry"; - /** Чекбокс "Необязательный вопрос" */ - required: boolean; - /** Варианты (картинки) */ - variants: QuestionVariant[]; - hint: QuestionHint; - rule: QuestionBranchingRule; - back: string | null; - originalBack: string | null; - autofill: boolean; - largeCheck: boolean; - }; -} diff --git a/src/model/questionTypes/number.ts b/src/model/questionTypes/number.ts deleted file mode 100644 index 4b3718c2..00000000 --- a/src/model/questionTypes/number.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { QuizQuestionBase, QuestionHint, PreviewRule } from "./shared"; - -export interface QuizQuestionNumber extends QuizQuestionBase { - type: "number"; - content: { - id: string; - /** Чекбокс "Необязательный вопрос" */ - required: boolean; - /** Чекбокс "Внутреннее название вопроса" */ - innerNameCheck: boolean; - /** Поле "Внутреннее название вопроса" */ - innerName: string; - /** Диапазон */ - range: string; - /** Начальное значение */ - start: number; - /** Начальное значение */ - defaultValue: number; - /** Шаг */ - step: number; - steps: number; - /** Чекбокс "Выбор диапазона (два ползунка)" */ - chooseRange: boolean; - hint: QuestionHint; - rule: PreviewRule; - back: string; - originalBack: string; - autofill: boolean; - form: "star" | "trophie" | "flag" | "heart" | "like" | "bubble" | "hashtag"; - }; -} diff --git a/src/model/questionTypes/page.ts b/src/model/questionTypes/page.ts deleted file mode 100644 index ba6eb261..00000000 --- a/src/model/questionTypes/page.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { QuizQuestionBase, QuestionHint, PreviewRule } from "./shared"; - -export interface QuizQuestionPage extends QuizQuestionBase { - type: "page"; - content: { - id: string; - /** Чекбокс "Внутреннее название вопроса" */ - innerNameCheck: boolean; - /** Поле "Внутреннее название вопроса" */ - innerName: string; - text: string; - picture: string; - originalPicture: string; - useImage: boolean; - video: string; - hint: QuestionHint; - rule: PreviewRule; - back: string; - originalBack: string; - autofill: boolean; - }; -} diff --git a/src/model/questionTypes/rating.ts b/src/model/questionTypes/rating.ts deleted file mode 100644 index 31785ddf..00000000 --- a/src/model/questionTypes/rating.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { QuizQuestionBase, QuestionHint, PreviewRule } from "./shared"; - -export interface QuizQuestionRating extends QuizQuestionBase { - type: "rating"; - content: { - id: string; - /** Чекбокс "Необязательный вопрос" */ - required: boolean; - /** Чекбокс "Внутреннее название вопроса" */ - innerNameCheck: boolean; - /** Поле "Внутреннее название вопроса" */ - innerName: string; - steps: number; - ratingExpanded: boolean; - /** Форма иконки */ - form: string; - hint: QuestionHint; - rule: PreviewRule; - back: string; - originalBack: string; - autofill: boolean; - /** Позитивное описание рейтинга */ - ratingPositiveDescription: string; - /** Негативное описание рейтинга */ - ratingNegativeDescription: string; - }; -} diff --git a/src/model/questionTypes/result.ts b/src/model/questionTypes/result.ts deleted file mode 100644 index 5b084765..00000000 --- a/src/model/questionTypes/result.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { - QuizQuestionBase, - QuestionBranchingRule, - QuestionHint, -} from "./shared"; - -export interface QuizQuestionResult extends QuizQuestionBase { - type: "result"; - content: { - id: string; - back: string; - originalBack: string; - video: string; - innerName: string; - text: string; - price: [number] | [number, number]; - useImage: boolean; - rule: QuestionBranchingRule; - hint: QuestionHint; - autofill: boolean; - usage: boolean; - redirect: string; - }; -} diff --git a/src/model/questionTypes/select.ts b/src/model/questionTypes/select.ts deleted file mode 100644 index 42856da4..00000000 --- a/src/model/questionTypes/select.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { - QuizQuestionBase, - QuestionVariant, - QuestionHint, - PreviewRule, -} from "./shared"; - -export interface QuizQuestionSelect extends QuizQuestionBase { - type: "select"; - content: { - id: string; - /** Чекбокс "Можно несколько" */ - multi: boolean; - /** Чекбокс "Необязательный вопрос" */ - required: boolean; - /** Чекбокс "Внутреннее название вопроса" */ - innerNameCheck: boolean; - /** Поле "Внутреннее название вопроса" */ - innerName: string; - /** Поле "Текст в выпадающем списке" */ - default: string; - variants: QuestionVariant[]; - rule: PreviewRule; - hint: QuestionHint; - back: string; - originalBack: string; - autofill: boolean; - }; -} diff --git a/src/model/questionTypes/shared.ts b/src/model/questionTypes/shared.ts index 7cdd8a28..e85692af 100644 --- a/src/model/questionTypes/shared.ts +++ b/src/model/questionTypes/shared.ts @@ -1,77 +1,6 @@ -import { QuestionType } from "@model/question/question"; -import type { QuizQuestionDate } from "./date"; -import type { QuizQuestionEmoji } from "./emoji"; -import type { QuizQuestionFile } from "./file"; -import type { QuizQuestionImages } from "./images"; -import type { QuizQuestionNumber } from "./number"; -import type { QuizQuestionPage } from "./page"; -import type { QuizQuestionRating } from "./rating"; -import type { QuizQuestionSelect } from "./select"; -import type { QuizQuestionText } from "./text"; -import type { QuizQuestionVariant } from "./variant"; -import type { QuizQuestionVarImg } from "./varimg"; -import type { QuizQuestionResult } from "./result"; +import { QuestionBranchingRuleMain, QuestionVariant } from "@frontend/squzanswerer"; import { nanoid } from "nanoid"; -export interface QuestionBranchingRuleMain { - next: string; - or: boolean; - rules: { - question: string; //id родителя (пока что) - answers: string[]; - }[]; -} -export interface QuestionBranchingRule { - children: string[]; - //список условий - main: QuestionBranchingRuleMain[]; - parentId: string | null | "root"; - default: string; -} - -export interface QuestionHint { - /** Текст подсказки */ - text: string; - /** URL видео подсказки */ - video: string; -} - -export type QuestionVariant = { - id: string; - /** Текст */ - answer: string; - /** Текст подсказки */ - hints: string; - /** Дополнительное поле для текста, emoji, ссылки на картинку */ - extendedText: string; - /** Оригинал изображения (до кропа) */ - originalImageUrl: string; -}; - -export interface QuizQuestionBase { - backendId: number; - /** Stable id, generated on client */ - id: string; - quizId: number; - title: string; - description: string; - page: number; - type?: QuestionType | null; - expanded: boolean; - openedModalSettings: boolean; - deleted: boolean; - deleteTimeoutId: number; - required: boolean; - content: { - id: string; - hint: QuestionHint; - rule: QuestionBranchingRule; - back: string; - originalBack: string; - autofill: boolean; - }; -} - export interface UntypedQuizQuestion { type: null; id: string; @@ -82,46 +11,25 @@ export interface UntypedQuizQuestion { deleted: boolean; } -export type AnyTypedQuizQuestion = - | QuizQuestionVariant - | QuizQuestionImages - | QuizQuestionVarImg - | QuizQuestionEmoji - | QuizQuestionText - | QuizQuestionSelect - | QuizQuestionDate - | QuizQuestionNumber - | QuizQuestionFile - | QuizQuestionPage - | QuizQuestionRating - | QuizQuestionResult; - -type FilterQuestionsWithVariants = T extends { - content: { variants: QuestionVariant[] }; +export function createBranchingRuleMain(targetId: string, parentId: string): QuestionBranchingRuleMain { + return { + next: targetId, + or: false, + rules: [ + { + question: parentId, + answers: [], + }, + ], + }; } - ? T - : never; -export type QuizQuestionsWithVariants = - FilterQuestionsWithVariants; - -export const createBranchingRuleMain: ( - targetId: string, - parentId: string, -) => QuestionBranchingRuleMain = (targetId, parentId) => ({ - next: targetId, - or: false, - rules: [ - { - question: parentId, - answers: [] as string[], - }, - ], -}); -export const createQuestionVariant: () => QuestionVariant = () => ({ - id: nanoid(), - answer: "", - extendedText: "", - hints: "", - originalImageUrl: "", -}); +export function createQuestionVariant(): QuestionVariant { + return { + id: nanoid(), + answer: "", + extendedText: "", + hints: "", + originalImageUrl: "", + }; +} diff --git a/src/model/questionTypes/text.ts b/src/model/questionTypes/text.ts deleted file mode 100644 index c38101aa..00000000 --- a/src/model/questionTypes/text.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { QuizQuestionBase, QuestionHint, PreviewRule } from "./shared"; - -export interface QuizQuestionText extends QuizQuestionBase { - type: "text"; - content: { - id: string; - placeholder: string; - /** Чекбокс "Внутреннее название вопроса" */ - innerNameCheck: boolean; - /** Поле "Внутреннее название вопроса" */ - innerName: string; - /** Чекбокс "Необязательный вопрос" */ - required: boolean; - /** Чекбокс "Автозаполнение адреса" */ - autofill: boolean; - answerType: "single" | "multi"; - hint: QuestionHint; - rule: PreviewRule; - back: string; - originalBack: string; - onlyNumbers: boolean; - }; -} diff --git a/src/model/questionTypes/variant.ts b/src/model/questionTypes/variant.ts deleted file mode 100644 index 5bed9840..00000000 --- a/src/model/questionTypes/variant.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { - QuizQuestionBase, - QuestionVariant, - QuestionHint, - PreviewRule, -} from "./shared"; - -export interface QuizQuestionVariant extends QuizQuestionBase { - type: "variant"; - content: { - id: string; - /** Чекбокс "Длинный текстовый ответ" */ - largeCheck: boolean; - /** Чекбокс "Можно несколько" */ - multi: boolean; - /** Чекбокс "Вариант "свой ответ"" */ - own: boolean; - /** Чекбокс "Внутреннее название вопроса" */ - innerNameCheck: boolean; - /** Чекбокс "Необязательный вопрос" */ - required: boolean; - /** Поле "Внутреннее название вопроса" */ - innerName: string; - /** Варианты ответов */ - variants: QuestionVariant[]; - hint: QuestionHint; - rule: PreviewRule; - back: string; - originalBack: string; - autofill: boolean; - }; -} diff --git a/src/model/questionTypes/varimg.ts b/src/model/questionTypes/varimg.ts deleted file mode 100644 index 504db463..00000000 --- a/src/model/questionTypes/varimg.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { - QuestionHint, - QuestionVariant, - QuizQuestionBase, - PreviewRule, -} from "./shared"; - -export interface QuizQuestionVarImg extends QuizQuestionBase { - type: "varimg"; - content: { - id: string; - /** Чекбокс "Вариант "свой ответ"" */ - own: boolean; - /** Чекбокс "Внутреннее название вопроса" */ - innerNameCheck: boolean; - /** Поле "Внутреннее название вопроса" */ - innerName: string; - /** Чекбокс "Необязательный вопрос" */ - required: boolean; - variants: QuestionVariant[]; - hint: QuestionHint; - rule: QuestionBranchingRule; - back: string; - originalBack: string; - autofill: boolean; - largeCheck: boolean; - replText: string; - }; -} diff --git a/src/model/quiz/create.ts b/src/model/quiz/create.ts index 081871a4..797bf7a2 100644 --- a/src/model/quiz/create.ts +++ b/src/model/quiz/create.ts @@ -26,7 +26,7 @@ export interface CreateQuizRequest { /** true if it is allowed for pause quiz */ pausable: boolean; /** count of questions */ - question_cnt?: number; + questions_count?: number; /** set true if squiz realize group functionality */ super: boolean; /** group of new quiz */ diff --git a/src/model/quiz/edit.ts b/src/model/quiz/edit.ts index 90113d3c..58b36e9e 100644 --- a/src/model/quiz/edit.ts +++ b/src/model/quiz/edit.ts @@ -30,7 +30,7 @@ export interface EditQuizRequest { /** allow to pause quiz to user */ pausable: boolean; /** count of questions */ - question_cnt?: number; + questions_count?: number; /** set true if squiz realize group functionality */ super?: boolean; /** group of new quiz */ @@ -58,7 +58,7 @@ export function quizToEditQuizRequest(quiz: Quiz): EditQuizRequest { due_to: quiz.due_to, time_of_passing: quiz.time_of_passing, pausable: quiz.pausable, - question_cnt: quiz.question_cnt, + questions_count: quiz.questions_count, super: quiz.super, group_id: quiz.group_id, }; diff --git a/src/model/quiz/quiz.ts b/src/model/quiz/quiz.ts index 9fb59db5..de038c01 100644 --- a/src/model/quiz/quiz.ts +++ b/src/model/quiz/quiz.ts @@ -1,4 +1,5 @@ -import { QuizConfig, defaultQuizConfig } from "@model/quizSettings"; +import { QuizConfig } from "@frontend/squzanswerer"; +import { defaultQuizConfig } from "@model/quizSettings"; import { nanoid } from "nanoid"; export interface Quiz { @@ -47,7 +48,7 @@ export interface Quiz { created_at: string; updated_at: string; /** count of questions */ - question_cnt: number; + questions_count: number; /** count passings */ passed_count: number; session_count: number; @@ -55,8 +56,6 @@ export interface Quiz { average_time: number; /** set true if squiz realize group functionality */ super: boolean; - - questions_count: number; /** group of new quiz */ group_id: number; } @@ -106,7 +105,7 @@ export interface RawQuiz { created_at: string; updated_at: string; /** count of questions */ - question_cnt: number; + questions_count: number; /** count passings */ passed_count: number; session_count: number; @@ -124,10 +123,7 @@ export function rawQuizToQuiz(rawQuiz: RawQuiz): Quiz { try { config = JSON.parse(rawQuiz.config); } catch (error) { - console.warn( - "Cannot parse quiz config from string, using default config", - error, - ); + console.warn("Cannot parse quiz config from string, using default config", error); } return { diff --git a/src/model/quizSettings.ts b/src/model/quizSettings.ts index b0118cd3..50050301 100644 --- a/src/model/quizSettings.ts +++ b/src/model/quizSettings.ts @@ -1,3 +1,4 @@ +import { QuizConfig } from "@frontend/squzanswerer"; import ChartPieIcon from "@icons/ChartPieIcon"; import ContactBookIcon from "@icons/ContactBookIcon"; import FlowArrowIcon from "@icons/FlowArrowIcon"; @@ -65,78 +66,12 @@ export type QuizTheme = | "Design9" | "Design10"; -export interface QuizConfig { - spec: undefined | true; - type: QuizType; - noStartPage: boolean; - startpageType: QuizStartpageType; - results: QuizResultsType; - haveRoot: string | null; - theme: QuizTheme; - design: boolean; - resultInfo: { - when: "email" | ""; - share: true | false; - replay: true | false; - theme: string; - reply: string; - replname: string; - showResultForm: "before" | "after"; - }; - startpage: { - description: string; - button: string; - position: QuizStartpageAlignType; - favIcon: string | null; - logo: string | null; - originalLogo: string | null; - background: { - type: null | "image" | "video"; - desktop: string | null; - originalDesktop: string | null; - mobile: string | null; - originalMobile: string | null; - video: string | null; - cycle: boolean; - }; - }; - formContact: { - title: string; - desc: string; - fields: Record; - button: string; - }; - info: { - phonenumber: string; - clickable: boolean; - orgname: string; - site: string; - law?: string; - }; - meta: string; - yandexMetricsNumber: number | undefined; - vkMetricsNumber: number | undefined; -} - export enum QuizMetricType { yandex = "yandexMetricsNumber", vk = "vkMetricsNumber", } -export type FormContactFieldName = - | "name" - | "email" - | "phone" - | "text" - | "address"; - -type FormContactFieldData = { - text: string; - innerText: string; - key: string; - required: boolean; - used: boolean; -}; +export type FormContactFieldName = "name" | "email" | "phone" | "text" | "address"; export type FieldSettingsDrawerState = { field: FormContactFieldName | "all" | ""; @@ -228,6 +163,8 @@ export const defaultQuizConfig: QuizConfig = { button: "", }, meta: "", + antifraud: true, + showfc: true, yandexMetricsNumber: undefined, vkMetricsNumber: undefined, }; diff --git a/src/pages/Analytics/Analytics.tsx b/src/pages/Analytics/Analytics.tsx index 437ea06b..c64912f6 100644 --- a/src/pages/Analytics/Analytics.tsx +++ b/src/pages/Analytics/Analytics.tsx @@ -9,7 +9,7 @@ import { useTheme, } from "@mui/material"; import { redirect } from "react-router-dom"; -import { LocalizationProvider, DatePicker } from "@mui/x-date-pickers"; +import { DatePicker } from "@mui/x-date-pickers"; import { AdapterMoment } from "@mui/x-date-pickers/AdapterMoment"; import HeaderFull from "@ui_kit/Header/HeaderFull"; @@ -53,8 +53,8 @@ export default function Analytics() { }); const resetTime = () => { - setFrom(moment(0)); - setTo(moment(Date.now())); + setFrom(moment(new Date(quiz.created_at))); + setTo(moment().add(1, "days")); }; useEffect(() => { @@ -68,13 +68,16 @@ export default function Analytics() { useEffect(() => { const getData = async (): Promise => { - try { - if (editQuizId !== null) { - const gottenQuizes = await quizApi.getList(); - setQuizes(gottenQuizes); + if (editQuizId !== null) { + const [gottenQuizes, gottenQuizesError] = await quizApi.getList(); + + if (gottenQuizesError) { + console.error("Не удалось получить квизы", gottenQuizesError); + + return; } - } catch (error) { - console.error("Не удалось получить квизы", error); + + setQuizes(gottenQuizes); } }; @@ -116,7 +119,7 @@ export default function Analytics() { }; return ( - + <> Аналитика @@ -179,7 +182,9 @@ export default function Analytics() { }, }} value={from} - onChange={(date) => {setFrom(date ? date.startOf("day") : moment())}} + onChange={(date) => { + setFrom(date ? date.startOf("day") : moment()); + }} /> @@ -222,7 +227,9 @@ export default function Analytics() { }, }} value={to} - onChange={(date) => {setTo(date ? date.endOf("day") : moment())}} + onChange={(date) => { + setTo(date ? date.endOf("day") : moment()); + }} /> @@ -255,6 +262,6 @@ export default function Analytics() { - + ); } diff --git a/src/pages/Analytics/Answers/AnswersStatistics.tsx b/src/pages/Analytics/Answers/AnswersStatistics.tsx index c522891e..1a9ff469 100644 --- a/src/pages/Analytics/Answers/AnswersStatistics.tsx +++ b/src/pages/Analytics/Answers/AnswersStatistics.tsx @@ -3,7 +3,7 @@ import { Box, Typography, useMediaQuery, useTheme } from "@mui/material"; import { Answers } from "./Answers"; import { QuestionsResponse } from "@api/statistic"; import { FC } from "react"; -import { Funnel } from "./Funnel"; +import { Funnel } from "./FunnelAnswers/Funnel"; import { Results } from "./Results"; type AnswersStatisticsProps = { diff --git a/src/pages/Analytics/Answers/Funnel.tsx b/src/pages/Analytics/Answers/Funnel.tsx deleted file mode 100644 index e0d41caa..00000000 --- a/src/pages/Analytics/Answers/Funnel.tsx +++ /dev/null @@ -1,145 +0,0 @@ -import { - Box, - LinearProgress, - Paper, - Typography, - useMediaQuery, - useTheme, -} from "@mui/material"; - -import type { FC } from "react"; - -type FunnelItemProps = { - title: string; - percent: number; - index: number; - funnel: number; -}; - -type FunnelProps = { - data: number[]; - funnelData: number[]; -}; - -const FUNNEL_MOCK: Record = { - "Стартовая страница": 100, - "Воронка квиза": 0, - Заявки: 0, - Результаты: 0, -}; - -const FunnelItem = ({ title, percent, index, funnel }: FunnelItemProps) => { - const theme = useTheme(); - - return ( - - - {title} - - - - {funnel} - - - 100 ? 100 : percent * 100} - sx={{ - width: "100%", - marginRight: "15px", - - height: "12px", - background: theme.palette.background.default, - borderRadius: "6px", - border: - percent >= 100 - ? `1px solid ${theme.palette.brightPurple.main}` - : null, - "& > span": { background: "#D9C0F9" }, - }} - /> - - - {index === 0 ? "100%" : `${(percent * 100).toFixed(1)}%`} - - - - - - ); -}; - -export const Funnel: FC = ({ data, funnelData }) => { - const theme = useTheme(); - const isSmallMonitor = useMediaQuery(theme.breakpoints.down(1150)); - const isMobile = useMediaQuery(theme.breakpoints.down(850)); - - if (!data) - return ( - - нет данных о разделах - - ); - - return ( - - {Object.entries(FUNNEL_MOCK).map(([title, percent], index) => ( - 0 ? data[index - 1] : percent} - funnel={funnelData[index]} - /> - ))} - - ); -}; diff --git a/src/pages/Analytics/Answers/FunnelAnswers/Funnel.tsx b/src/pages/Analytics/Answers/FunnelAnswers/Funnel.tsx new file mode 100644 index 00000000..46d581ed --- /dev/null +++ b/src/pages/Analytics/Answers/FunnelAnswers/Funnel.tsx @@ -0,0 +1,191 @@ +import { + Box, IconButton, + LinearProgress, + Paper, + Typography, + useMediaQuery, + useTheme, +} from "@mui/material"; + +import type { FC } from "react"; +import {ArrowDownIcon} from "@icons/questionsPage/ArrowDownIcon"; +import ExpandLessIcon from "@mui/icons-material/ExpandLess"; +import {useState} from "react"; +import FunnelDetals from "@/pages/Analytics/Answers/FunnelAnswers/funnelDetals"; + +type FunnelItemProps = { + title: string; + percent: number; + index: number; + funnel: number; +}; + +type FunnelProps = { + data: number[]; + funnelData: number[]; +}; + +const FUNNEL_MOCK: Record = { + "Стартовая страница": 100, + "Воронка квиза": 0, + Заявки: 0, + Результаты: 0, +}; + +const FunnelItem = ({ title, percent, index, funnel }: FunnelItemProps) => { + const theme = useTheme(); + const [isExpanded, setIsExpanded] = useState(false); + const expandedHC = (bool: boolean) => { + setIsExpanded(bool); + }; + + return ( + <> + + + {title} + + + {/* expandedHC(!isExpanded)} + > + + */} + + {funnel} + + + 100 ? 100 : percent * 100} + sx={{ + width: "100%", + marginRight: "15px", + + height: "12px", + background: theme.palette.background.default, + borderRadius: "6px", + border: + percent >= 100 + ? `1px solid ${theme.palette.brightPurple.main}` + : null, + "& > span": { background: "#D9C0F9" }, + }} + /> + + + {index === 0 ? "100%" : `${(percent * 100).toFixed(1)}%`} + + + + + + {isExpanded && + <> + + Воронки + Дошли до этапа + + + + + + + } + + + ); +}; + +export const Funnel: FC = ({ data, funnelData }) => { + const theme = useTheme(); + const isSmallMonitor = useMediaQuery(theme.breakpoints.down(1150)); + const isMobile = useMediaQuery(theme.breakpoints.down(850)); + + if (!data) + return ( + + нет данных о разделах + + ); + + return ( + + {Object.entries(FUNNEL_MOCK).map(([title, percent], index) => ( + 0 ? data[index - 1] : percent} + funnel={funnelData[index]} + /> + ))} + + ); +}; diff --git a/src/pages/Analytics/Answers/FunnelAnswers/funnelDetals.tsx b/src/pages/Analytics/Answers/FunnelAnswers/funnelDetals.tsx new file mode 100644 index 00000000..71f18a0c --- /dev/null +++ b/src/pages/Analytics/Answers/FunnelAnswers/funnelDetals.tsx @@ -0,0 +1,87 @@ +import {Box, IconButton, Typography, useTheme} from "@mui/material"; +import {ArrowDownIcon} from "@icons/questionsPage/ArrowDownIcon"; +import {useState} from "react"; + +export default function FunnelDetals () { + const theme = useTheme(); + const [isExpanded, setIsExpanded] = useState(false); + const expandedHC = (bool: boolean) => { + setIsExpanded(bool); + }; + return( + + + Воронка 1 + expandedHC(!isExpanded)} + > + + + + {isExpanded && + + + + + + + } + + + ) +} + +const DetalItem = () => { + return( + + + + 1. + Вы немного опоздали, как поступите? + + 20% + + + + ) +} \ No newline at end of file diff --git a/src/pages/Analytics/General.tsx b/src/pages/Analytics/General.tsx index b4ffd56b..306b1d4e 100644 --- a/src/pages/Analytics/General.tsx +++ b/src/pages/Analytics/General.tsx @@ -9,7 +9,6 @@ type GeneralItemsProps = { title: string; general: Record; color: string; - numberType: "sum" | "percent" | "time"; calculateTime?: boolean; conversionValue?: number; day: boolean; @@ -32,119 +31,34 @@ const GeneralItem = ({ title, general, color, - numberType, - calculateTime = false, - conversionValue, - day, -}: GeneralItemsProps) => { - const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down(700)); - - const numberValue = - numberType === "sum" - ? Object.values(general).reduce((total, item) => total + item, 0) - : title === "Конверсия" - ? conversionValue - : 0; - - if ( - Object.keys(general).length === 0 || - Object.values(general).every((x) => x === 0) - ) { - return ( - {`${title} - нет данных`} - ); - } - - const getCalculatedTime = (time: number) => { - const hours = String(Math.floor(time / 3600)).padStart(2, "0"); - const minutes = String(Math.floor((time % 3600) / 60)).padStart(2, "0"); - const seconds = String(Math.floor((time % 3600) % 60)).padStart(2, "0"); - - return `${hours}:${minutes}:${seconds}`; - }; - - return ( - - {title} - - {numberType === "percent" ? `${numberValue?.toFixed(2)}%` : numberValue} - - - moment.unix(Number(value)).format("DD/MM/YYYY HH") + "ч", - }, - ]} - series={[ - { - data: Object.values(general), - valueFormatter: (value) => - calculateTime - ? getCalculatedTime(value) - : String(value.toFixed(2)), - }, - ]} - height={220} - colors={[color]} - sx={{ - transform: isMobile ? "scale(1.1)" : "scale(1.2)", - "& .MuiChartsAxis-tickContainer": { display: "none" }, - }} - /> - - ); -}; - -const GeneralItemTimeConv = ({ - title, - general, - color, - numberType, calculateTime = false, conversionValue, }: GeneralItemsProps) => { const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down(700)); + const statiscticsResult = Boolean(calculateTime || conversionValue); - const data = Object.entries(general).sort((a, b) => a[0] - b[0]); - - const days = [...data].map((e) => e[0]); - - let buffer = 0; - - const time = [...data].map((e) => { - if (e[1] > 0) { - buffer = e[1]; - } - return buffer; - }); - - console.log("data", data); - console.log( - "time", - time.reduce((a, b) => Number(a) + Number(b), 0), + const data = Object.entries(general).sort( + ([nextValue], [currentValue]) => Number(nextValue) - Number(currentValue), ); - console.log( - "time", - getCalculatedTime(time.reduce((a, b) => Number(a) + Number(b), 0)), + const days = data.map(([value]) => value); + const { time } = data.reduce( + (total, [_, value]) => ({ + defaultValue: value > 0 ? value : total.defaultValue, + time: [...total.time, value > 0 ? value : total.defaultValue], + }), + { defaultValue: 0, time: [] as number[] }, ); - console.log("days", days.length); + const numberValue = calculateTime - ? time.reduce((a, b) => Number(a) + Number(b), 0) / days.length || 0 - : conversionValue; + ? time.reduce((total, value) => total + value, 0) / days.length + : conversionValue + ? conversionValue + : Object.values(general).reduce((total, item) => total + item, 0); if ( Object.keys(general).length === 0 || - Object.values(general).every((x) => x === 0) + Object.values(general).every((item) => item === 0) ) { return ( {`${title} - нет данных`} @@ -162,28 +76,32 @@ const GeneralItemTimeConv = ({ {title} {calculateTime - ? `${getCalculatedTime(numberValue)} с` - : `${numberValue.toFixed(2)}%`} + ? `${getCalculatedTime(numberValue ?? 0)} с` + : `${conversionValue ? numberValue?.toFixed(2) : numberValue} ${conversionValue ? "%" : ""}`} - moment.utc(Number(value) * 1000).format("DD/MM/YYYY"), + moment.unix(Number(value)).format("DD/MM/YYYY HH") + + statiscticsResult + ? "" + : "ч", }, ]} series={[ { - data: Object.values(time), - valueFormatter: (value) => { - console.log("log", value); - return calculateTime + data: Object.values(statiscticsResult ? time : general), + valueFormatter: (value) => + calculateTime ? getCalculatedTime(value) - : String((value * 100).toFixed(2)) + "%"; - }, + : statiscticsResult + ? String((value * 100).toFixed(2)) + "%" + : String(value.toFixed(2)), }, ]} + // dataset={Object.entries(general).map(([, v]) => moment.unix(v).format("ss:mm:HH")).reduce((acc, [k, v]) => ({ ...acc, [k]: v }), {})} height={220} colors={[color]} sx={{ @@ -261,29 +179,25 @@ export const General: FC = ({ data, day }) => { > - - + + { + updateQuiz(quiz.id, (quiz) => { + quiz.config.showfc = e.target.checked; + + }) + }} + /> + + Показывать форму контактов + {!quiz?.config.formContact.fields.name.used && !quiz?.config.formContact.fields.email.used && !quiz?.config.formContact.fields.phone.used && diff --git a/src/pages/DesignPage/DesignFilling.tsx b/src/pages/DesignPage/DesignFilling.tsx index 74e744be..c6cbfd42 100644 --- a/src/pages/DesignPage/DesignFilling.tsx +++ b/src/pages/DesignPage/DesignFilling.tsx @@ -122,7 +122,7 @@ export const DesignFilling = ({ width: "100%", borderRadius: "12px", height: "calc(100vh - 300px)", - mb: "76px" + mb: "76px", }} > ) => { - setChecked([event.target.checked, checked[1]]); - }; - - const handleChange2 = (event: React.ChangeEvent) => { - setChecked([checked[0], event.target.checked]); - }; - - return ( - - - - - {" "} - - - - - - Quiz будет открываться через указанное время - - - - - - Если quiz уже установлен на сайт, и вы что-то здесь изменили, код на - сайте нужно будет поменять. Настройки в этом конструкторе не - сохраняются. - - - - - - - - - - - - {checked[0] ? ( - - - - Показывать через - - - - - - секунд - - - - - - - - ) : ( - <> - )} - - {checked[1] ? ( - - - - - ) : ( - <> - )} - - - ); -} diff --git a/src/pages/InstallQuiz/BannerInstall.tsx b/src/pages/InstallQuiz/BannerInstall.tsx deleted file mode 100644 index 8b8c3910..00000000 --- a/src/pages/InstallQuiz/BannerInstall.tsx +++ /dev/null @@ -1,268 +0,0 @@ -import { - Box, - FormControl, - FormControlLabel, - Link, - Radio, - RadioGroup, - TextField, - Typography, - useTheme, -} from "@mui/material"; -import InstallQzOnSiteParent from "./InstallQzOnSiteParent"; -import React, { useState } from "react"; -import BannerImg from "../../assets/BannerImg.png"; -import CustomTextField from "@ui_kit/CustomTextField"; -import CustomCheckbox from "@ui_kit/CustomCheckbox"; -import RadioCheck from "@ui_kit/RadioCheck"; -import RadioIcon from "@ui_kit/RadioIcon"; -import AccordMy from "@ui_kit/Accordion"; - -type isExpanded = "panel1" | "panel2"; -export default function BannerInstall() { - const theme = useTheme(); - - const [value, setValue] = React.useState("1"); - - const handleChangeRadio = (event: React.ChangeEvent) => { - setValue((event.target as HTMLInputElement).value); - }; - - const [value2, setValue2] = React.useState("1"); - - const handleChangeRadio2 = (event: React.ChangeEvent) => { - setValue2((event.target as HTMLInputElement).value); - }; - - const [isExpanded, setIsExpanded] = useState("panel1"); - - return ( - - - - - Если quiz уже установлен на сайт, и вы что-то здесь изменили, код на - сайте нужно будет поменять. Настройки в этом конструкторе не - сохраняются. - - - - - - Текст-призыв - - - - Заголовок quiz - - - - - Показывать через - - - - - - секунд - - - - - Цвет кнопки - - - {" "} - - - Цвет текста кнопки - - - {" "} - - - - - setIsExpanded("panel1")} - sx={{ display: "flex", flexDirection: "column" }} - > - - Расположение - - - - - } icon={} /> - } - label="Слева сверху" - /> - } icon={} /> - } - label="Справа сверху" - /> - - - } icon={} /> - } - label="Слева снизу" - /> - } icon={} /> - } - label="Справа снизу" - /> - - - - - Параметры - - - - - - - - setIsExpanded("panel2")} - sx={{ display: "flex", flexDirection: "column" }} - > - - Расположение - - - - - } icon={} /> - } - label="Сверху страницы" - /> - } icon={} /> - } - label="Снизу страницы" - /> - - - - - Параметры - - - - - - - - + Автооткрытие quiz - - - - ); -} diff --git a/src/pages/InstallQuiz/InBodyInstall.tsx b/src/pages/InstallQuiz/InBodyInstall.tsx deleted file mode 100644 index abb494f7..00000000 --- a/src/pages/InstallQuiz/InBodyInstall.tsx +++ /dev/null @@ -1,169 +0,0 @@ -import { Box, Button, Link, Typography, useTheme } from "@mui/material"; -import InstallQzOnSiteParent from "./InstallQzOnSiteParent"; -import Dots from "../../assets/dots.png"; -import React from "react"; -import CustomTextField from "@ui_kit/CustomTextField"; -import CustomCheckbox from "@ui_kit/CustomCheckbox"; - -export default function InBodyInstall() { - const theme = useTheme(); - return ( - - - - - {" "} - - - - - - Quiz будет открыть прямо в том месте, где вы установите код на - сайте - - - - - В мобильной версии будет показана кнопка, открывающая quiz в - модальном окне - - - - - Если quiz уже установлен на сайт, и вы что-то здесь изменили, код на - сайте нужно будет поменять. Настройки в этом конструкторе не - сохраняются. - - - - - 1. Задайте размеры (опционально) - - - - Ширина (px) - - - - Радиус (px) - - - - - - Высота (px) - - - - Отступ (px) - - - - - - 2. Настройте кнопку для мобильной версии - - - Цвет кнопки - - - {" "} - - - Цвет текста кнопки - - - {" "} - - - - - - - - - Текст кнопки - - - - + Автооткрытие quiz - - - - - ); -} diff --git a/src/pages/InstallQuiz/InstallQzCode.tsx b/src/pages/InstallQuiz/InstallQzCode.tsx deleted file mode 100644 index 2474d0e5..00000000 --- a/src/pages/InstallQuiz/InstallQzCode.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import InstallQzOnSiteParent from "./InstallQzOnSiteParent"; -import { - Box, - Button, - FormControl, - IconButton, - InputAdornment, - Link, - OutlinedInput, - TextField, - Typography, - useTheme, -} from "@mui/material"; -import CopyIcon from "@icons/CopyIcon"; -import React from "react"; -import InfoIcon from "@icons/InfoIcon"; - -export default function InstallQzCode() { - const theme = useTheme(); - return ( - - - - 1. Код вставки опросника - - Установите код в то место, где должен быть опросник - - - - - - - ), - }} - /> - - - - - - Код нужно вставить один раз. Изменения в самом quiz будут отображаться - автоматически после сохранения. - - - - ); -} diff --git a/src/pages/InstallQuiz/InstallQzOnSiteParent.tsx b/src/pages/InstallQuiz/InstallQzOnSiteParent.tsx deleted file mode 100644 index c0e0df15..00000000 --- a/src/pages/InstallQuiz/InstallQzOnSiteParent.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { - Box, - Button, - SxProps, - Theme, - Typography, - useTheme, -} from "@mui/material"; -import OneIconBorder from "../../assets/icons/OneIconBorder"; -import React from "react"; -import NumberTwo from "@icons/NumberTwo"; -import NumberThree from "@icons/NumberThree"; - -interface Props { - outerContainerSx?: SxProps; - children?: React.ReactNode; -} - -export default function InstallQzOnSiteParent({ - outerContainerSx: sx, - children, -}: Props) { - const theme = useTheme(); - return ( - <> - - - - Установка quiz на сайте - - - - Способ установки - - - Настройка кнопки - - - Вставить код на сайт - - - - - {children} - - - - - - - ); -} diff --git a/src/pages/InstallQuiz/OnButtonInstall.tsx b/src/pages/InstallQuiz/OnButtonInstall.tsx deleted file mode 100644 index 20c2393d..00000000 --- a/src/pages/InstallQuiz/OnButtonInstall.tsx +++ /dev/null @@ -1,162 +0,0 @@ -import { Box, Button, Link, Typography, useTheme } from "@mui/material"; -import React, { useState } from "react"; -import CustomCheckbox from "@ui_kit/CustomCheckbox"; -import CustomTextField from "@ui_kit/CustomTextField"; -import Dots from "../../assets/dots.png"; -import InstallQzOnSiteParent from "./InstallQzOnSiteParent"; -import AccordMy from "@ui_kit/Accordion"; - -type isExpanded = "panel1" | "panel2"; - -export default function OnButtonInstall() { - const theme = useTheme(); - - const [isExpanded, setIsExpanded] = useState("panel1"); - return ( - <> - - - - - {" "} - - - - - - - - Если quiz уже установлен на сайт, и вы что-то здесь изменили, код на - сайте нужно будет поменять. Настройки в этом конструкторе не - сохраняются. - - - - setIsExpanded("panel1")} - sx={{ display: "flex", flexDirection: "column" }} - > - - - Цвет кнопки - - - {" "} - - - Цвет текста кнопки - - - {" "} - - - - - - - - - Текст кнопки - - - - setIsExpanded("panel2")} - sx={{ display: "flex", flexDirection: "column", width: "100%" }} - > - - Ссылка для вашей кнопки - - - - Или событие - - - - - - + Автооткрытие quiz - - - - - ); -} diff --git a/src/pages/InstallQuiz/QuizInstallationCard/InstallationStepButton.tsx b/src/pages/InstallQuiz/QuizInstallationCard/InstallationStepButton.tsx index 53ac5d7e..20736b1d 100644 --- a/src/pages/InstallQuiz/QuizInstallationCard/InstallationStepButton.tsx +++ b/src/pages/InstallQuiz/QuizInstallationCard/InstallationStepButton.tsx @@ -1,31 +1,58 @@ -import { ButtonBase, Typography, useTheme } from "@mui/material"; +import NumberIcon from "@icons/NumberIcon"; +import { Button, useTheme } from "@mui/material"; import { ReactNode } from "react"; interface Props { - active?: boolean; + state: "active" | "done" | "inactive"; onClick?: () => void; - leftIcon?: ReactNode; + index: number; children?: ReactNode; } export default function InstallationStepButton({ - active, - leftIcon, + state = "done", + index, onClick, children, }: Props) { const theme = useTheme(); + const buttonColorByState: Record = { + active: "#FC712F", + done: theme.palette.brightPurple.main, + inactive: theme.palette.grey2.main, + }; + + const color = buttonColorByState[state]; + return ( - - {leftIcon} - - {children} - - + ); } diff --git a/src/pages/InstallQuiz/QuizInstallationCard/QuizInstallationCard.tsx b/src/pages/InstallQuiz/QuizInstallationCard/QuizInstallationCard.tsx index 63796407..8a1c344d 100644 --- a/src/pages/InstallQuiz/QuizInstallationCard/QuizInstallationCard.tsx +++ b/src/pages/InstallQuiz/QuizInstallationCard/QuizInstallationCard.tsx @@ -1,38 +1,49 @@ -import { - Box, - ButtonBase, - IconButton, - InputAdornment, - TextField as MuiTextField, - TextFieldProps, - Typography, - useMediaQuery, - useTheme, -} from "@mui/material"; +import { WidgetType } from "@frontend/squzanswerer"; +import { Box, Button, Typography, useMediaQuery, useTheme } from "@mui/material"; import { useCurrentQuiz } from "@root/quizes/hooks"; -import { useDomainDefine } from "@utils/hooks/useDomainDefine"; -import { FC, useState } from "react"; -import CopyIcon from "../../../assets/icons/CopyIcon"; -import OneIconBorder from "../../../assets/icons/OneIconBorder"; +import { useState } from "react"; import InstallationStepButton from "./InstallationStepButton"; +import BannerWidgetSetup from "./WidgetSetupByType/BannerWidgetSetup/BannerWidgetSetup"; +import ButtonWidgetSetup from "./WidgetSetupByType/ButtonWidgetSetup/ButtonWidgetSetup"; +import ContainerWidgetSetup from "./WidgetSetupByType/ContainerWidgetSetup"; +import PopupWidgetSetup from "./WidgetSetupByType/PopupWidgetSetup"; +import SideWidgetSetup from "./WidgetSetupByType/SideWidgetSetup/SideWidgetSetup"; +import WidgetTypeButton from "./WidgetTypeButton"; +import BannerWidgetPreview from "./previewIcons/BannerWidgetPreview"; +import ButtonWidgetPreview from "./previewIcons/ButtonWidgetPreview"; import ContainerWidgetPreview from "./previewIcons/ContainerWidgetPreview"; +import PopupWidgetPreview from "./previewIcons/PopupWidgetPreview"; +import SideWidgetPreview from "./previewIcons/SideWidgetPreview"; -const TextField = MuiTextField as unknown as FC; +type WidgetSetupSettings = { + step: 1 | 2 | 3; + widgetType: WidgetType; +}; export default function QuizInstallationCard() { const theme = useTheme(); - const quiz = useCurrentQuiz(); - const { isTestServer } = useDomainDefine(); - const [stepState, setStepState] = useState("step1"); const isSmallMonitor = useMediaQuery(theme.breakpoints.down(1065)); + const quiz = useCurrentQuiz(); + const [widgetSetupSettings, setWidgetSetupSettings] = useState({ + step: 1, + widgetType: "container", + }); if (!quiz) return null; + const nextButton = ( + + ); + return ( - - Установка quiz на сайте - - - } - onClick={() => { - setStepState("step1"); + Установка quiz на сайте + - Способ установки - - - } - onClick={() => { - setStepState("step2"); - }} - > - Вставить код на сайт - - - - - {stepState === "step1" ? ( - { - setStepState("step2"); - }} - sx={{ - display: "flex", - flexDirection: "column", - justifyContent: "start", - alignItems: "start", - maxWidth: "205px", - gap: "15px", + setWidgetSetupSettings({ + step: 1, + widgetType: "container", + }); }} > - - В тело сайта - - Задайте свои размеры и встройте в сайт - - - ) : ( - <> - - + {widgetSetupSettings.step !== 1 && ( + <> + { + setWidgetSetupSettings((prev) => ({ + ...prev, + step: 2, + })); }} > - 1. Код вставки quiz - - Установите код в то место, где должен быть quiz - - `} - sx={{ - "& .MuiInputBase-root": { - maxWidth: "520px", - width: "100%", - backgroundColor: theme.palette.background.default, - fontSize: "18px", - alignItems: "flex-start", - }, - }} - InputProps={{ - endAdornment: ( - - navigator.clipboard.writeText( // TODO - document.getElementById( - "outlined-multiline-static" - ).value - )} - > - - - - ), - }} - /> - - - - - Код нужно вставить один раз. Изменения в самом quiz будут - отображаться автоматически после сохранения. - - - Для установки размера добавьте в тег значения высоты и ширины, - например: - - - {`
`} -
-
- - )} + {installationStepButtonTextByWidgetType[widgetSetupSettings.widgetType]} + + { + setWidgetSetupSettings((prev) => ({ + ...prev, + step: 3, + })); + }} + > + Вставить код на сайт + + + )} +
+ {widgetSetupSettings.step === 1 ? ( + + } + text1="По кнопке" + text2="Конструктор кнопки или собственная кнопка" + onClick={() => { + setWidgetSetupSettings({ + step: 2, + widgetType: "button", + }); + }} + /> + } + text1="Баннером" + text2="Сбоку или на всю ширину экрана" + onClick={() => { + setWidgetSetupSettings({ + step: 2, + widgetType: "banner", + }); + }} + /> + } + text1="В тело сайта" + text2="Задайте свои размеры и встройте в сайт" + onClick={() => { + setWidgetSetupSettings({ + step: 2, + widgetType: "container", + }); + }} + /> + } + text1="Автооткрытие" + text2="Автооткрытие поп-ап на сайте" + onClick={() => { + setWidgetSetupSettings({ + step: 2, + widgetType: "popup", + }); + }} + /> + } + text1="Виджет" + text2="Сбоку страницы, как консультант" + onClick={() => { + setWidgetSetupSettings({ + step: 2, + widgetType: "side", + }); + }} + /> + + ) : ( + <> + {widgetSetupSettings.widgetType === "container" && ( + + )} + {widgetSetupSettings.widgetType === "button" && ( + + )} + {widgetSetupSettings.widgetType === "banner" && ( + + )} + {widgetSetupSettings.widgetType === "popup" && ( + + )} + {widgetSetupSettings.widgetType === "side" && ( + + )} + + )}
); } + +const installationStepButtonTextByWidgetType: Record = { + button: "Настройка кнопки", + banner: "Настройка баннера", + container: "Настройка квиза", + popup: "Настройка автооткрытия", + side: "Настройка виджета", +}; diff --git a/src/pages/InstallQuiz/QuizInstallationCard/WidgetScript.tsx b/src/pages/InstallQuiz/QuizInstallationCard/WidgetScript.tsx new file mode 100644 index 00000000..4e3470ec --- /dev/null +++ b/src/pages/InstallQuiz/QuizInstallationCard/WidgetScript.tsx @@ -0,0 +1,65 @@ +import CopyIcon from "@icons/CopyIcon"; +import { + Box, + IconButton, + InputAdornment, + TextField as MuiTextField, + TextFieldProps, + Typography, + useTheme, +} from "@mui/material"; +import { FC } from "react"; + +const TextField = MuiTextField as unknown as FC; + +interface Props { + scriptText: string; + helperText: string; +} + +export default function WidgetScript({ scriptText, helperText }: Props) { + const theme = useTheme(); + + return ( + + {helperText} + + { + navigator.clipboard.writeText(scriptText); + }} + edge="end" + sx={{ marginTop: "22px" }} + > + + + + ), + }} + /> + + ); +} diff --git a/src/pages/InstallQuiz/QuizInstallationCard/WidgetSetupByType/BannerWidgetSetup/BannerWidgetPreviewDesktop.tsx b/src/pages/InstallQuiz/QuizInstallationCard/WidgetSetupByType/BannerWidgetSetup/BannerWidgetPreviewDesktop.tsx new file mode 100644 index 00000000..2d38942a --- /dev/null +++ b/src/pages/InstallQuiz/QuizInstallationCard/WidgetSetupByType/BannerWidgetSetup/BannerWidgetPreviewDesktop.tsx @@ -0,0 +1,157 @@ +import TaskIcon from "@/ui_kit/TaskIcon"; +import CloseIcon from "@mui/icons-material/Close"; +import { Box, Typography } from "@mui/material"; +import RunningStripe from "@ui_kit/RunningStripe"; + +interface Props { + position: "topleft" | "topright" | "bottomleft" | "bottomright"; + bannerFullWidth: boolean; + pulsation: boolean; + rounded: boolean; + withShadow: boolean; + buttonFlash: boolean; + buttonBackgroundColor: string; + buttonTextColor: string; + appealText: string; + quizHeaderText: string; +} + +export default function BannerWidgetPreviewDesktop({ + bannerFullWidth, + buttonBackgroundColor, + buttonFlash, + buttonTextColor, + position, + pulsation, + rounded, + withShadow, + appealText, + quizHeaderText, +}: Props) { + return ( + + + + + + {appealText || "Пройти тест"} + + + {quizHeaderText || "Заголовок теста"} + + + {buttonFlash && } + + + + ); +} diff --git a/src/pages/InstallQuiz/QuizInstallationCard/WidgetSetupByType/BannerWidgetSetup/BannerWidgetPreviewMobile.tsx b/src/pages/InstallQuiz/QuizInstallationCard/WidgetSetupByType/BannerWidgetSetup/BannerWidgetPreviewMobile.tsx new file mode 100644 index 00000000..47a26914 --- /dev/null +++ b/src/pages/InstallQuiz/QuizInstallationCard/WidgetSetupByType/BannerWidgetSetup/BannerWidgetPreviewMobile.tsx @@ -0,0 +1,128 @@ +import TaskIcon from "@/ui_kit/TaskIcon"; +import CloseIcon from "@mui/icons-material/Close"; +import { Box, Typography } from "@mui/material"; +import RunningStripe from "@ui_kit/RunningStripe"; + +interface Props { + position: "topleft" | "topright" | "bottomleft" | "bottomright"; + pulsation: boolean; + withShadow: boolean; + buttonFlash: boolean; + buttonBackgroundColor: string; + buttonTextColor: string; + appealText: string; + quizHeaderText: string; +} + +export default function BannerWidgetPreviewMobile({ + buttonBackgroundColor, + buttonFlash, + buttonTextColor, + position, + pulsation, + withShadow, + appealText, + quizHeaderText, +}: Props) { + return ( + + + + + + {appealText || "Пройти тест"} + + + {quizHeaderText || "Заголовок теста"} + + + {buttonFlash && } + + + + ); +} diff --git a/src/pages/InstallQuiz/QuizInstallationCard/WidgetSetupByType/BannerWidgetSetup/BannerWidgetSetup.tsx b/src/pages/InstallQuiz/QuizInstallationCard/WidgetSetupByType/BannerWidgetSetup/BannerWidgetSetup.tsx new file mode 100644 index 00000000..100677c4 --- /dev/null +++ b/src/pages/InstallQuiz/QuizInstallationCard/WidgetSetupByType/BannerWidgetSetup/BannerWidgetSetup.tsx @@ -0,0 +1,483 @@ +import { BannerWidgetParams } from "@frontend/squzanswerer"; +import { + Box, + Button, + Collapse, + Divider, + FormControl, + FormControlLabel, + Radio, + RadioGroup, + Typography, + useMediaQuery, + useTheme, +} from "@mui/material"; +import { useCurrentQuiz } from "@root/quizes/hooks"; +import CircleColorPicker from "@ui_kit/CircleColorPicker"; +import CustomCheckbox from "@ui_kit/CustomCheckbox"; +import PenaTextField from "@ui_kit/PenaTextField"; +import RadioCheck from "@ui_kit/RadioCheck"; +import RadioIcon from "@ui_kit/RadioIcon"; +import { ReactNode, useState } from "react"; +import widgetPreviewImage from "../../../../../assets/widget-preview.png"; +import WidgetScript from "../../WidgetScript"; +import { createBannerWidgetScriptText } from "../../createWidgetScriptText"; +import { useWidgetUrl } from "../../useWidgetUrl"; +import BannerWidgetPreviewDesktop from "./BannerWidgetPreviewDesktop"; +import BannerWidgetPreviewMobile from "./BannerWidgetPreviewMobile"; + +interface Props { + step: 2 | 3; + nextButton: ReactNode; +} + +export default function BannerWidgetSetup({ step, nextButton }: Props) { + const theme = useTheme(); + const isSmallMonitor = useMediaQuery(theme.breakpoints.down(1065)); + const quiz = useCurrentQuiz(); + const widgetUrl = useWidgetUrl(); + const [widgetWidth, setWidgetWidth] = useState(""); + const [widgetHeight, setWidgetHeight] = useState(""); + const [isAutoopenQuizSettingsOpen, setIsAutoopenQuizSettingsOpen] = useState(false); + const [hideOnMobile, setHideOnMobile] = useState(false); + const [rounded, setRounded] = useState(false); + const [withShadow, setWithShadow] = useState(false); + const [buttonFlash, setButtonFlash] = useState(false); + const [buttonBackgroundColor, setButtonBackgroundColor] = useState(theme.palette.brightPurple.main); + const [buttonTextColor, setButtonTextColor] = useState("#FFFFFF"); + const [autoShowQuiz, setAutoShowQuiz] = useState(false); + const [autoShowQuizTime, setAutoShowQuizTime] = useState(10); + const [openOnLeaveAttempt, setOpenOnLeaveAttempt] = useState(false); + const [position, setPosition] = useState("topleft"); + const [bannerFullWidth, setBannerFullWidth] = useState(false); + const [autoShowWidgetTime, setAutoShowWidgetTime] = useState(10); + const [appealText, setAppealText] = useState(""); + const [quizHeaderText, setQuizHeaderText] = useState(""); + const [pulsation, setPulsation] = useState(false); + const [fullScreen, setFullScreen] = useState(false); + + if (!quiz) return null; + + if (step === 3) { + const scriptText = createBannerWidgetScriptText( + { + quizId: quiz.qid, + position, + hideOnMobile: hideOnMobile || undefined, + rounded: bannerFullWidth ? undefined : rounded || undefined, + withShadow: withShadow || undefined, + buttonFlash: buttonFlash || undefined, + buttonBackgroundColor, + buttonTextColor, + autoShowQuizTime: autoShowQuiz ? autoShowQuizTime : undefined, + openOnLeaveAttempt: openOnLeaveAttempt || undefined, + bannerFullWidth: bannerFullWidth || undefined, + autoShowWidgetTime, + appealText: appealText || undefined, + quizHeaderText: quizHeaderText || undefined, + pulsation: pulsation || undefined, + fullScreen: fullScreen || undefined, + dialogDimensions: + !fullScreen && (widgetWidth || widgetHeight) + ? { + width: widgetWidth ? `${widgetWidth}px` : "100%", + height: widgetHeight ? `${widgetHeight}px` : "100%", + } + : undefined, + }, + widgetUrl + ); + + return ( + + ); + } + + return ( + + + + + + + + + + setFullScreen(e.target.checked)} + /> + + + + Ширина окна (px) + setWidgetWidth(e.target.value)} + FormControlSx={{ maxWidth: "160px" }} + placeholder="auto" + /> + + + Высота окна (px) + setWidgetHeight(e.target.value)} + FormControlSx={{ maxWidth: "160px" }} + placeholder="auto" + /> + + + + Текст-призыв + setAppealText(e.target.value)} + FormControlSx={{ maxWidth: "360px" }} + /> + Заголовок quiz + setQuizHeaderText(e.target.value)} + FormControlSx={{ maxWidth: "360px" }} + /> + + Показывать через + setAutoShowWidgetTime(parseInt(e.target.value))} + FormControlSx={{ + width: "90px", + }} + /> + секунд + + + Цвет баннера + setButtonBackgroundColor(color)} + /> + Цвет текста баннера + setButtonTextColor(color)} + /> + + { + setBannerFullWidth(e.target.checked); + if (e.target.checked) setPosition("topleft"); + }} + /> + Расположение + + { + setPosition(e.target.value as BannerWidgetParams["position"]); + }} + sx={{ + display: "flex", + flexWrap: "wrap", + flexDirection: "row", + paddingLeft: "5px", + justifyContent: "space-between", + }} + > + {bannerFullWidth ? ( + + } + icon={} + /> + } + sx={{ color: theme.palette.grey2.main }} + /> + } + icon={} + /> + } + sx={{ color: theme.palette.grey2.main }} + /> + + ) : ( + <> + + } + icon={} + /> + } + sx={{ color: theme.palette.grey2.main }} + /> + } + icon={} + /> + } + sx={{ color: theme.palette.grey2.main }} + /> + + + } + icon={} + /> + } + sx={{ color: theme.palette.grey2.main }} + /> + } + icon={} + /> + } + sx={{ color: theme.palette.grey2.main }} + /> + + + )} + + + Параметры + {!bannerFullWidth && ( + setRounded(e.target.checked)} + /> + )} + setWithShadow(e.target.checked)} + /> + setButtonFlash(e.target.checked)} + /> + setPulsation(e.target.checked)} + /> + setHideOnMobile(e.target.checked)} + /> + + + + setAutoShowQuiz(e.target.checked)} + /> + setOpenOnLeaveAttempt(e.target.checked)} + sx={{ + mt: "15px", + }} + /> + + + + + Показывать через + setAutoShowQuizTime(parseInt(e.target.value))} + FormControlSx={{ + width: "90px", + }} + /> + секунд + + + + + + + {nextButton} + + + + ); +} diff --git a/src/pages/InstallQuiz/QuizInstallationCard/WidgetSetupByType/ButtonWidgetSetup/ButtonWidgetPreviewDesktop.tsx b/src/pages/InstallQuiz/QuizInstallationCard/WidgetSetupByType/ButtonWidgetSetup/ButtonWidgetPreviewDesktop.tsx new file mode 100644 index 00000000..c1cae171 --- /dev/null +++ b/src/pages/InstallQuiz/QuizInstallationCard/WidgetSetupByType/ButtonWidgetSetup/ButtonWidgetPreviewDesktop.tsx @@ -0,0 +1,74 @@ +import RunningStripe from "@/ui_kit/RunningStripe"; +import TaskIcon from "@/ui_kit/TaskIcon"; +import { Box, Typography } from "@mui/material"; + +interface Props { + buttonBackgroundColor: string; + rounded: boolean; + withShadow: boolean; + fixedSide: "left" | "right"; + buttonTextColor: string; + buttonText: string; + buttonFlash: boolean; +} + +export default function ButtonWidgetPreviewDesktop({ + buttonBackgroundColor, + buttonFlash, + buttonText, + buttonTextColor, + fixedSide, + rounded, + withShadow, +}: Props) { + return ( + + + + {buttonText || "Пройти тест"} + + {buttonFlash && } + + ); +} diff --git a/src/pages/InstallQuiz/QuizInstallationCard/WidgetSetupByType/ButtonWidgetSetup/ButtonWidgetPreviewMobile.tsx b/src/pages/InstallQuiz/QuizInstallationCard/WidgetSetupByType/ButtonWidgetSetup/ButtonWidgetPreviewMobile.tsx new file mode 100644 index 00000000..e70eb7a6 --- /dev/null +++ b/src/pages/InstallQuiz/QuizInstallationCard/WidgetSetupByType/ButtonWidgetSetup/ButtonWidgetPreviewMobile.tsx @@ -0,0 +1,47 @@ +import TaskIcon from "@/ui_kit/TaskIcon"; +import { Box } from "@mui/material"; + +interface Props { + fixedSide: "left" | "right"; + buttonBackgroundColor: string; + buttonTextColor: string; +} + +export default function ButtonWidgetPreviewMobile({ buttonBackgroundColor, fixedSide, buttonTextColor }: Props) { + return ( + + + + ); +} diff --git a/src/pages/InstallQuiz/QuizInstallationCard/WidgetSetupByType/ButtonWidgetSetup/ButtonWidgetSetup.tsx b/src/pages/InstallQuiz/QuizInstallationCard/WidgetSetupByType/ButtonWidgetSetup/ButtonWidgetSetup.tsx new file mode 100644 index 00000000..8b953fce --- /dev/null +++ b/src/pages/InstallQuiz/QuizInstallationCard/WidgetSetupByType/ButtonWidgetSetup/ButtonWidgetSetup.tsx @@ -0,0 +1,459 @@ +import { ButtonWidgetFixedParams, ButtonWidgetParams } from "@frontend/squzanswerer"; +import { + Box, + Button, + Collapse, + Divider, + FormControl, + FormControlLabel, + Radio, + RadioGroup, + Typography, + useMediaQuery, + useTheme, +} from "@mui/material"; +import { useCurrentQuiz } from "@root/quizes/hooks"; +import CircleColorPicker from "@ui_kit/CircleColorPicker"; +import CustomCheckbox from "@ui_kit/CustomCheckbox"; +import PenaTextField from "@ui_kit/PenaTextField"; +import RadioCheck from "@ui_kit/RadioCheck"; +import RadioIcon from "@ui_kit/RadioIcon"; +import RunningStripe from "@ui_kit/RunningStripe"; +import { nanoid } from "nanoid"; +import { ReactNode, useState } from "react"; +import Dots from "../../../../../assets/dots.png"; +import widgetPreviewImage from "../../../../../assets/widget-preview.png"; +import WidgetScript from "../../WidgetScript"; +import { createButtonWidgetScriptText } from "../../createWidgetScriptText"; +import { useWidgetUrl } from "../../useWidgetUrl"; +import ButtonWidgetPreviewDesktop from "./ButtonWidgetPreviewDesktop"; +import ButtonWidgetPreviewMobile from "./ButtonWidgetPreviewMobile"; + +interface Props { + step: 2 | 3; + nextButton: ReactNode; +} + +export default function ButtonWidgetSetup({ step, nextButton }: Props) { + const theme = useTheme(); + const isSmallMonitor = useMediaQuery(theme.breakpoints.down(1065)); + const quiz = useCurrentQuiz(); + const widgetUrl = useWidgetUrl(); + const [widgetWidth, setWidgetWidth] = useState(""); + const [widgetHeight, setWidgetHeight] = useState(""); + const [isAutoopenQuizSettingsOpen, setIsAutoopenQuizSettingsOpen] = useState(false); + const [hideOnMobile, setHideOnMobile] = useState(false); + const [rounded, setRounded] = useState(false); + const [withShadow, setWithShadow] = useState(false); + const [buttonFlash, setButtonFlash] = useState(false); + const [buttonText, setButtonText] = useState(""); + const [buttonBackgroundColor, setButtonBackgroundColor] = useState(theme.palette.brightPurple.main); + const [buttonTextColor, setButtonTextColor] = useState("#FFFFFF"); + const [autoShowQuiz, setAutoShowQuiz] = useState(false); + const [autoShowQuizTime, setAutoShowQuizTime] = useState(10); + const [openOnLeaveAttempt, setOpenOnLeaveAttempt] = useState(false); + const [fixedSide, setFixedSide] = useState(null); + const [fullScreen, setFullScreen] = useState(false); + + if (!quiz) return null; + + if (step === 3) { + const genericParams = { + quizId: quiz.qid, + fullScreen: fullScreen || undefined, + dialogDimensions: + !fullScreen && (widgetWidth || widgetHeight) + ? { + width: widgetWidth ? `${widgetWidth}px` : "100%", + height: widgetHeight ? `${widgetHeight}px` : "100%", + } + : undefined, + hideOnMobile: hideOnMobile || undefined, + rounded: rounded || undefined, + withShadow: withShadow || undefined, + buttonFlash: buttonFlash || undefined, + buttonText: buttonText || undefined, + buttonBackgroundColor, + buttonTextColor, + autoShowQuizTime: autoShowQuiz ? autoShowQuizTime : undefined, + openOnLeaveAttempt: openOnLeaveAttempt || undefined, + }; + + let params: ButtonWidgetParams | ButtonWidgetFixedParams; + if (fixedSide === null) { + params = { + ...genericParams, + selector: `#pena-quiz-button-container-${nanoid(6)}`, + }; + } else { + params = { + ...genericParams, + fixedSide, + }; + } + + const scriptText = createButtonWidgetScriptText(params, widgetUrl); + + return ( + + ); + } + + return ( + + + {fixedSide === null ? ( + + + {" "} + + + + + + + ) : ( + + + + + + {!hideOnMobile && ( + + )} + + + + )} + + + + 1. Задайте размеры окна квиза (опционально) + + setFullScreen(e.target.checked)} + /> + + + + Ширина (px) + setWidgetWidth(e.target.value)} + FormControlSx={{ maxWidth: "160px" }} + placeholder="auto" + /> + + + Высота (px) + setWidgetHeight(e.target.value)} + FormControlSx={{ maxWidth: "160px" }} + placeholder="auto" + /> + + + + + + 2. Конструктор кнопки + + + Цвет кнопки + setButtonBackgroundColor(color)} + /> + Цвет текста кнопки + setButtonTextColor(color)} + /> + + setHideOnMobile(e.target.checked)} + /> + setRounded(e.target.checked)} + /> + setWithShadow(e.target.checked)} + /> + setButtonFlash(e.target.checked)} + /> + setFixedSide(e.target.checked ? "left" : null)} + /> + + + { + setFixedSide(e.target.value as ButtonWidgetFixedParams["fixedSide"]); + }} + > + } + icon={} + /> + } + label="Слева" + sx={{ + color: theme.palette.grey2.main, + }} + /> + } + icon={} + /> + } + label="Справа" + sx={{ + color: theme.palette.grey2.main, + }} + /> + + + + Текст кнопки + setButtonText(e.target.value)} + placeholder="Пройти тест" + FormControlSx={{ maxWidth: "360px" }} + /> + + + + setAutoShowQuiz(e.target.checked)} + /> + setOpenOnLeaveAttempt(e.target.checked)} + sx={{ + mt: "15px", + }} + /> + + + + + Показывать через + setAutoShowQuizTime(parseInt(e.target.value))} + FormControlSx={{ + width: "90px", + }} + /> + секунд + + + + + + + + {nextButton} + + + + ); +} diff --git a/src/pages/InstallQuiz/QuizInstallationCard/WidgetSetupByType/ContainerWidgetSetup.tsx b/src/pages/InstallQuiz/QuizInstallationCard/WidgetSetupByType/ContainerWidgetSetup.tsx new file mode 100644 index 00000000..a9553668 --- /dev/null +++ b/src/pages/InstallQuiz/QuizInstallationCard/WidgetSetupByType/ContainerWidgetSetup.tsx @@ -0,0 +1,299 @@ +import { Box, Button, Collapse, Divider, Typography, useMediaQuery, useTheme } from "@mui/material"; +import { useCurrentQuiz } from "@root/quizes/hooks"; +import CircleColorPicker from "@ui_kit/CircleColorPicker"; +import CustomCheckbox from "@ui_kit/CustomCheckbox"; +import PenaTextField from "@ui_kit/PenaTextField"; +import { nanoid } from "nanoid"; +import { ReactNode, useState } from "react"; +import Dots from "../../../../assets/dots.png"; +import WidgetScript from "../WidgetScript"; +import { createContainerWidgetScriptText } from "../createWidgetScriptText"; +import { useWidgetUrl } from "../useWidgetUrl"; + +interface Props { + step: 2 | 3; + nextButton: ReactNode; +} + +export default function ContainerWidgetSetup({ step, nextButton }: Props) { + const theme = useTheme(); + const isSmallMonitor = useMediaQuery(theme.breakpoints.down(1065)); + const quiz = useCurrentQuiz(); + const widgetUrl = useWidgetUrl(); + const [isAutoopenQuizSettingsOpen, setIsAutoopenQuizSettingsOpen] = useState(false); + const [widgetWidth, setWidgetWidth] = useState(""); + const [widgetHeight, setWidgetHeight] = useState(""); + const [hideOnMobile, setHideOnMobile] = useState(false); + const [showButtonOnMobile, setShowButtonOnMobile] = useState(false); + const [rounded, setRounded] = useState(false); + const [withShadow, setWithShadow] = useState(false); + const [buttonFlash, setButtonFlash] = useState(false); + const [buttonText, setButtonText] = useState(""); + const [buttonBackgroundColor, setButtonBackgroundColor] = useState(theme.palette.brightPurple.main); + const [buttonTextColor, setButtonTextColor] = useState("#FFFFFF"); + const [autoShowQuiz, setAutoShowQuiz] = useState(false); + const [autoShowQuizTime, setAutoShowQuizTime] = useState(10); + const [openOnLeaveAttempt, setOpenOnLeaveAttempt] = useState(false); + + if (!quiz) return null; + + if (step === 3) { + const scriptText = createContainerWidgetScriptText( + { + quizId: quiz.qid, + selector: `#pena-quiz-container-${nanoid(6)}`, + dimensions: + widgetWidth || widgetHeight + ? { + width: widgetWidth ? `${widgetWidth}px` : "100%", + height: widgetHeight ? `${widgetHeight}px` : "100%", + } + : undefined, + hideOnMobile: hideOnMobile || undefined, + showButtonOnMobile: showButtonOnMobile || undefined, + rounded: rounded || undefined, + withShadow: withShadow || undefined, + buttonFlash: buttonFlash || undefined, + buttonText: buttonText || undefined, + buttonBackgroundColor, + buttonTextColor, + autoShowQuizTime: autoShowQuiz ? autoShowQuizTime : undefined, + openOnLeaveAttempt: openOnLeaveAttempt || undefined, + }, + widgetUrl + ); + + return ; + } + + return ( + + + + {" "} + + + + + + Quiz будет открыть прямо в том месте, где вы установите код на сайте + + + + В мобильной версии будет показана кнопка, открывающая quiz в модальном окне + + + + + + 1. Задайте размеры (опционально) + + + + Ширина (px) + setWidgetWidth(e.target.value)} + FormControlSx={{ maxWidth: "160px" }} + placeholder="auto" + /> + + + Высота (px) + setWidgetHeight(e.target.value)} + FormControlSx={{ maxWidth: "160px" }} + placeholder="auto" + /> + + + + + 2. Настройте кнопку для мобильной версии + + + Цвет кнопки + setButtonBackgroundColor(color)} /> + Цвет текста кнопки + setButtonTextColor(color)} /> + + setHideOnMobile(e.target.checked)} + /> + setRounded(e.target.checked)} /> + setWithShadow(e.target.checked)} /> + setButtonFlash(e.target.checked)} + /> + setShowButtonOnMobile(e.target.checked)} + /> + Текст кнопки + setButtonText(e.target.value)} + placeholder="Пройти тест" + FormControlSx={{ maxWidth: "360px" }} + /> + + + + setAutoShowQuiz(e.target.checked)} + /> + setOpenOnLeaveAttempt(e.target.checked)} + sx={{ + mt: "15px", + }} + /> + + + + + Показывать через + setAutoShowQuizTime(parseInt(e.target.value))} + FormControlSx={{ + width: "90px", + }} + /> + секунд + + + + + + + + {nextButton} + + + + ); +} diff --git a/src/pages/InstallQuiz/QuizInstallationCard/WidgetSetupByType/PopupWidgetSetup.tsx b/src/pages/InstallQuiz/QuizInstallationCard/WidgetSetupByType/PopupWidgetSetup.tsx new file mode 100644 index 00000000..6a4f3009 --- /dev/null +++ b/src/pages/InstallQuiz/QuizInstallationCard/WidgetSetupByType/PopupWidgetSetup.tsx @@ -0,0 +1,211 @@ +import { Box, Collapse, Typography, useMediaQuery, useTheme } from "@mui/material"; +import { useCurrentQuiz } from "@root/quizes/hooks"; +import CustomCheckbox from "@ui_kit/CustomCheckbox"; +import PenaTextField from "@ui_kit/PenaTextField"; +import { ReactNode, useState } from "react"; +import Dots from "../../../../assets/dots.png"; +import WidgetScript from "../WidgetScript"; +import { createPopupWidgetScriptText } from "../createWidgetScriptText"; +import { useWidgetUrl } from "../useWidgetUrl"; + +interface Props { + step: 2 | 3; + nextButton: ReactNode; +} + +export default function PopupWidgetSetup({ step, nextButton }: Props) { + const theme = useTheme(); + const isSmallMonitor = useMediaQuery(theme.breakpoints.down(1065)); + const quiz = useCurrentQuiz(); + const widgetUrl = useWidgetUrl(); + const [widgetWidth, setWidgetWidth] = useState(""); + const [widgetHeight, setWidgetHeight] = useState(""); + const [hideOnMobile, setHideOnMobile] = useState(false); + const [autoShowQuizTime, setAutoShowQuizTime] = useState(10); + const [openOnLeaveAttempt, setOpenOnLeaveAttempt] = useState(false); + const [fullScreen, setFullScreen] = useState(false); + + if (!quiz) return null; + + if (step === 3) { + const scriptText = createPopupWidgetScriptText( + { + quizId: quiz.qid, + hideOnMobile: hideOnMobile || undefined, + autoShowQuizTime: autoShowQuizTime, + openOnLeaveAttempt: openOnLeaveAttempt || undefined, + fullScreen: fullScreen || undefined, + dialogDimensions: + !fullScreen && (widgetWidth || widgetHeight) + ? { + width: widgetWidth ? `${widgetWidth}px` : "100%", + height: widgetHeight ? `${widgetHeight}px` : "100%", + } + : undefined, + }, + widgetUrl + ); + + return ; + } + + return ( + + + + + {" "} + + + + + + Quiz будет открываться через указанное время + + + + + + + setFullScreen(e.target.checked)} + /> + + + + Ширина окна (px) + setWidgetWidth(e.target.value)} + FormControlSx={{ maxWidth: "160px" }} + placeholder="auto" + /> + + + Высота окна (px) + setWidgetHeight(e.target.value)} + FormControlSx={{ maxWidth: "160px" }} + placeholder="auto" + /> + + + + + Показывать через + setAutoShowQuizTime(parseInt(e.target.value))} + FormControlSx={{ + width: "90px", + }} + /> + секунд + + setOpenOnLeaveAttempt(e.target.checked)} + /> + setHideOnMobile(e.target.checked)} + /> + + {nextButton} + + + + ); +} diff --git a/src/pages/InstallQuiz/QuizInstallationCard/WidgetSetupByType/SideWidgetSetup/SideWidgetPositionButton.tsx b/src/pages/InstallQuiz/QuizInstallationCard/WidgetSetupByType/SideWidgetSetup/SideWidgetPositionButton.tsx new file mode 100644 index 00000000..c125d810 --- /dev/null +++ b/src/pages/InstallQuiz/QuizInstallationCard/WidgetSetupByType/SideWidgetSetup/SideWidgetPositionButton.tsx @@ -0,0 +1,61 @@ +import { Button } from "@mui/material"; + +interface Props { + isSelected: boolean; + position: "left" | "right"; + onClick: () => void; +} + +export default function SideWidgetPositionButton({ + isSelected, + position, + onClick, +}: Props) { + return ( + + ); +} diff --git a/src/pages/InstallQuiz/QuizInstallationCard/WidgetSetupByType/SideWidgetSetup/SideWidgetPreviewDesktop.tsx b/src/pages/InstallQuiz/QuizInstallationCard/WidgetSetupByType/SideWidgetSetup/SideWidgetPreviewDesktop.tsx new file mode 100644 index 00000000..742f158c --- /dev/null +++ b/src/pages/InstallQuiz/QuizInstallationCard/WidgetSetupByType/SideWidgetSetup/SideWidgetPreviewDesktop.tsx @@ -0,0 +1,55 @@ +import { Box, Typography } from "@mui/material"; +import RunningStripe from "@ui_kit/RunningStripe"; + +interface Props { + position: "left" | "right"; + buttonFlash: boolean; +} + +export default function SideWidgetPreviewDesktop({ buttonFlash, position }: Props) { + return ( + + theme.palette.brightPurple.main, + containerType: "size", + gap: "calc(5 / 196 * 100%)", + borderRadius: "4px", + }} + > + + Пройти квиз + + {buttonFlash && } + + + ); +} diff --git a/src/pages/InstallQuiz/QuizInstallationCard/WidgetSetupByType/SideWidgetSetup/SideWidgetPreviewMobile.tsx b/src/pages/InstallQuiz/QuizInstallationCard/WidgetSetupByType/SideWidgetSetup/SideWidgetPreviewMobile.tsx new file mode 100644 index 00000000..ccaebc6e --- /dev/null +++ b/src/pages/InstallQuiz/QuizInstallationCard/WidgetSetupByType/SideWidgetSetup/SideWidgetPreviewMobile.tsx @@ -0,0 +1,47 @@ +import { Box, Typography } from "@mui/material"; +import RunningStripe from "@ui_kit/RunningStripe"; + +interface Props { + buttonFlash: boolean; +} + +export default function SideWidgetPreviewMobile({ buttonFlash }: Props) { + return ( + + theme.palette.brightPurple.main, + containerType: "size", + gap: "calc(5 / 196 * 100%)", + }} + > + + Пройти квиз + + {buttonFlash && } + + + ); +} diff --git a/src/pages/InstallQuiz/QuizInstallationCard/WidgetSetupByType/SideWidgetSetup/SideWidgetSetup.tsx b/src/pages/InstallQuiz/QuizInstallationCard/WidgetSetupByType/SideWidgetSetup/SideWidgetSetup.tsx new file mode 100644 index 00000000..11700ba5 --- /dev/null +++ b/src/pages/InstallQuiz/QuizInstallationCard/WidgetSetupByType/SideWidgetSetup/SideWidgetSetup.tsx @@ -0,0 +1,294 @@ +import { SideWidgetParams } from "@frontend/squzanswerer"; +import { Box, Collapse, Typography, useMediaQuery, useTheme } from "@mui/material"; +import { useCurrentQuiz } from "@root/quizes/hooks"; +import CircleColorPicker from "@ui_kit/CircleColorPicker"; +import CustomCheckbox from "@ui_kit/CustomCheckbox"; +import PenaTextField from "@ui_kit/PenaTextField"; +import { ReactNode, useState } from "react"; +import widgetPreviewImage from "../../../../../assets/widget-preview.png"; +import WidgetScript from "../../WidgetScript"; +import { createSideWidgetScriptText } from "../../createWidgetScriptText"; +import { useWidgetUrl } from "../../useWidgetUrl"; +import SideWidgetPositionButton from "./SideWidgetPositionButton"; +import SideWidgetPreviewDesktop from "./SideWidgetPreviewDesktop"; +import SideWidgetPreviewMobile from "./SideWidgetPreviewMobile"; + +interface Props { + step: 2 | 3; + nextButton: ReactNode; +} + +export default function SideWidgetSetup({ step, nextButton }: Props) { + const theme = useTheme(); + const isSmallMonitor = useMediaQuery(theme.breakpoints.down(1065)); + const quiz = useCurrentQuiz(); + const widgetUrl = useWidgetUrl(); + const [widgetWidth, setWidgetWidth] = useState(""); + const [widgetHeight, setWidgetHeight] = useState(""); + const [hideOnMobile, setHideOnMobile] = useState(false); + const [autoShowQuiz, setAutoShowQuiz] = useState(false); + const [autoShowQuizTime, setAutoShowQuizTime] = useState(10); + const [autoShowWidgetTime, setAutoShowWidgetTime] = useState(10); + const [position, setPosition] = useState("left"); + const [fullScreen, setFullScreen] = useState(false); + const [buttonFlash, setButtonFlash] = useState(false); + const [buttonBackgroundColor, setButtonBackgroundColor] = useState(theme.palette.brightPurple.main); + const [buttonTextColor, setButtonTextColor] = useState("#FFFFFF"); + + if (!quiz) return null; + + if (step === 3) { + const scriptText = createSideWidgetScriptText( + { + quizId: quiz.qid, + position: position, + hideOnMobile: hideOnMobile || undefined, + autoShowQuizTime: autoShowQuiz ? autoShowQuizTime : undefined, + autoShowWidgetTime: autoShowWidgetTime || undefined, + fullScreen: fullScreen || undefined, + buttonFlash: buttonFlash || undefined, + buttonTextColor, + buttonBackgroundColor, + dialogDimensions: + !fullScreen && (widgetWidth || widgetHeight) + ? { + width: widgetWidth ? `${widgetWidth}px` : "100%", + height: widgetHeight ? `${widgetHeight}px` : "100%", + } + : undefined, + }, + widgetUrl + ); + + return ( + + ); + } + + return ( + + + + + + + + + + Расположение + + + setPosition("left")} + /> + Слева снизу + + + setPosition("right")} + /> + Справа снизу + + + setFullScreen(e.target.checked)} + /> + + + + Ширина окна (px) + setWidgetWidth(e.target.value)} + FormControlSx={{ maxWidth: "160px" }} + placeholder="auto" + /> + + + Высота окна (px) + setWidgetHeight(e.target.value)} + FormControlSx={{ maxWidth: "160px" }} + placeholder="auto" + /> + + + + + Цвет кнопки + setButtonBackgroundColor(color)} + /> + Цвет текста кнопки + setButtonTextColor(color)} + /> + + + Показывать кнопку через + setAutoShowWidgetTime(parseInt(e.target.value))} + FormControlSx={{ + width: "90px", + }} + /> + секунд + + setButtonFlash(e.target.checked)} + /> + + setAutoShowQuiz(e.target.checked)} + /> + + + Показывать через + setAutoShowQuizTime(parseInt(e.target.value))} + FormControlSx={{ + width: "90px", + }} + /> + секунд + + + Время, через которое квиз автоматически откроется + + + + setHideOnMobile(e.target.checked)} + /> + + {nextButton} + + + + ); +} diff --git a/src/pages/InstallQuiz/QuizInstallationCard/WidgetTypeButton.tsx b/src/pages/InstallQuiz/QuizInstallationCard/WidgetTypeButton.tsx new file mode 100644 index 00000000..5ff5c778 --- /dev/null +++ b/src/pages/InstallQuiz/QuizInstallationCard/WidgetTypeButton.tsx @@ -0,0 +1,63 @@ +import { + Box, + Button, + ButtonBase, + Typography, + useMediaQuery, + useTheme, +} from "@mui/material"; +import { ReactNode } from "react"; + +interface Props { + onClick?: () => void; + image: ReactNode; + text1: string; + text2: string; +} + +export default function WidgetTypeButton({ + onClick, + image, + text1, + text2, +}: Props) { + const theme = useTheme(); + const isSmallMonitor = useMediaQuery(theme.breakpoints.down(1065)); + + return ( + + ); +} diff --git a/src/pages/InstallQuiz/QuizInstallationCard/createWidgetScriptText.ts b/src/pages/InstallQuiz/QuizInstallationCard/createWidgetScriptText.ts new file mode 100644 index 00000000..d251d074 --- /dev/null +++ b/src/pages/InstallQuiz/QuizInstallationCard/createWidgetScriptText.ts @@ -0,0 +1,51 @@ +import { + BannerWidgetParams, + ButtonWidgetFixedParams, + ButtonWidgetParams, + ContainerWidgetParams, + PopupWidgetParams, + SideWidgetParams, +} from "@frontend/squzanswerer"; + +export function createContainerWidgetScriptText(params: ContainerWidgetParams, widgetUrl: string) { + return `
+`; +} + +export function createButtonWidgetScriptText(params: ButtonWidgetParams | ButtonWidgetFixedParams, widgetUrl: string) { + const widgetClassName = "fixedSide" in params ? "ButtonWidgetFixed" : "ButtonWidget"; + + return `${"selector" in params ? `
\n` : ""}`; +} + +export function createPopupWidgetScriptText(params: PopupWidgetParams, widgetUrl: string) { + return ``; +} + +export function createSideWidgetScriptText(params: SideWidgetParams, widgetUrl: string) { + return ``; +} + +export function createBannerWidgetScriptText(params: BannerWidgetParams, widgetUrl: string) { + return ``; +} diff --git a/src/pages/InstallQuiz/QuizInstallationCard/previewIcons/BannerWidgetPreview.tsx b/src/pages/InstallQuiz/QuizInstallationCard/previewIcons/BannerWidgetPreview.tsx index d3825aca..b1e2c678 100644 --- a/src/pages/InstallQuiz/QuizInstallationCard/previewIcons/BannerWidgetPreview.tsx +++ b/src/pages/InstallQuiz/QuizInstallationCard/previewIcons/BannerWidgetPreview.tsx @@ -1,49 +1,34 @@ -import { Box } from "@mui/material"; +import WidgetPreviewContainer from "./WidgetPreviewContainer"; export default function BannerWidgetPreview() { return ( - - - - - - - - - - + + + + + + + + ); } diff --git a/src/pages/InstallQuiz/QuizInstallationCard/previewIcons/ButtonWidgetPreview.tsx b/src/pages/InstallQuiz/QuizInstallationCard/previewIcons/ButtonWidgetPreview.tsx index efa49eb9..07ce6678 100644 --- a/src/pages/InstallQuiz/QuizInstallationCard/previewIcons/ButtonWidgetPreview.tsx +++ b/src/pages/InstallQuiz/QuizInstallationCard/previewIcons/ButtonWidgetPreview.tsx @@ -1,50 +1,35 @@ -import { Box } from "@mui/material"; +import WidgetPreviewContainer from "./WidgetPreviewContainer"; export default function ButtonWidgetPreview() { return ( - - - - - - - - - - + + + + + + + + ); } diff --git a/src/pages/InstallQuiz/QuizInstallationCard/previewIcons/ContainerWidgetPreview.tsx b/src/pages/InstallQuiz/QuizInstallationCard/previewIcons/ContainerWidgetPreview.tsx index 1c8653d8..1709638b 100644 --- a/src/pages/InstallQuiz/QuizInstallationCard/previewIcons/ContainerWidgetPreview.tsx +++ b/src/pages/InstallQuiz/QuizInstallationCard/previewIcons/ContainerWidgetPreview.tsx @@ -1,50 +1,35 @@ -import { Box } from "@mui/material"; +import WidgetPreviewContainer from "./WidgetPreviewContainer"; export default function ContainerWidgetPreview() { return ( - - - - - - - - - - + + + + + + + + ); } diff --git a/src/pages/InstallQuiz/QuizInstallationCard/previewIcons/PopupWidgetPreview.tsx b/src/pages/InstallQuiz/QuizInstallationCard/previewIcons/PopupWidgetPreview.tsx index 9469e502..a5b37797 100644 --- a/src/pages/InstallQuiz/QuizInstallationCard/previewIcons/PopupWidgetPreview.tsx +++ b/src/pages/InstallQuiz/QuizInstallationCard/previewIcons/PopupWidgetPreview.tsx @@ -1,54 +1,39 @@ -import { Box } from "@mui/material"; +import WidgetPreviewContainer from "./WidgetPreviewContainer"; export default function PopupWidgetPreview() { return ( - - - - - - - - - - - + + + + + + + + + ); } diff --git a/src/pages/InstallQuiz/QuizInstallationCard/previewIcons/SideWidgetPreview.tsx b/src/pages/InstallQuiz/QuizInstallationCard/previewIcons/SideWidgetPreview.tsx index bf9abb95..06aefe35 100644 --- a/src/pages/InstallQuiz/QuizInstallationCard/previewIcons/SideWidgetPreview.tsx +++ b/src/pages/InstallQuiz/QuizInstallationCard/previewIcons/SideWidgetPreview.tsx @@ -1,50 +1,35 @@ -import { Box } from "@mui/material"; +import WidgetPreviewContainer from "./WidgetPreviewContainer"; export default function SideWidgetPreview() { return ( - - - - - - - - - - + + + + + + + + ); } diff --git a/src/pages/InstallQuiz/QuizInstallationCard/previewIcons/WidgetPreviewContainer.tsx b/src/pages/InstallQuiz/QuizInstallationCard/previewIcons/WidgetPreviewContainer.tsx new file mode 100644 index 00000000..ecd9879e --- /dev/null +++ b/src/pages/InstallQuiz/QuizInstallationCard/previewIcons/WidgetPreviewContainer.tsx @@ -0,0 +1,33 @@ +import { Box, useMediaQuery, useTheme } from "@mui/material"; +import { ReactNode } from "react"; + +interface Props { + children?: ReactNode; +} + +export default function WidgetPreviewContainer({ children }: Props) { + const theme = useTheme(); + const isSmallMonitor = useMediaQuery(theme.breakpoints.down(1065)); + + return ( + + + {children} + + + ); +} diff --git a/src/pages/InstallQuiz/QuizInstallationCard/useWidgetUrl.ts b/src/pages/InstallQuiz/QuizInstallationCard/useWidgetUrl.ts new file mode 100644 index 00000000..b1a6027f --- /dev/null +++ b/src/pages/InstallQuiz/QuizInstallationCard/useWidgetUrl.ts @@ -0,0 +1,7 @@ +import { useDomainDefine } from "@/utils/hooks/useDomainDefine"; + +export function useWidgetUrl() { + const { isTestServer } = useDomainDefine(); + + return `https://${isTestServer ? "s." : ""}hbpn.link/export/pub.js`; +} diff --git a/src/pages/InstallQuiz/VidjetInstall.tsx b/src/pages/InstallQuiz/VidjetInstall.tsx deleted file mode 100644 index c9da07fb..00000000 --- a/src/pages/InstallQuiz/VidjetInstall.tsx +++ /dev/null @@ -1,219 +0,0 @@ -import { - Box, - Button, - FormControl, - FormControlLabel, - Radio, - RadioGroup, - TextField, - Typography, - useTheme, -} from "@mui/material"; -import React, { useState } from "react"; -import InstallQzOnSiteParent from "./InstallQzOnSiteParent"; -import CustomCheckbox from "@ui_kit/CustomCheckbox"; -import VidjetImg from "../../assets/VidjetImg.png"; -import LDownButton from "@icons/InstallQuizIcon/LDownButton"; -import RDownButton from "@icons/InstallQuizIcon/RDownButton"; -import RadioCheck from "@ui_kit/RadioCheck"; -import RadioIcon from "@ui_kit/RadioIcon"; - -export default function VidjetInstall() { - const [value, setValue] = React.useState("1"); - - const handleChangeRadio = (event: React.ChangeEvent) => { - setValue((event.target as HTMLInputElement).value); - }; - - const [position, setPosition] = useState<"left" | "right">("left"); - - const theme = useTheme(); - return ( - - - - - Если quiz уже установлен на сайт, и вы что-то здесь изменили, код на - сайте нужно будет поменять. Настройки в этом конструкторе не - сохраняются. - - - - - Расположение - - - setPosition("left")} - isActive={position === "left"} - Icon={LDownButton} - /> - setPosition("right")} - isActive={position === "right"} - Icon={RDownButton} - /> - - - - - Показывать виджет - - - - - - секунд - - - - Автооткрытие виджета при входе на сайт - - - - } icon={} /> - } - label="Да" - /> - } icon={} /> - } - label="Нет" - /> - - - {value === "1" ? ( - - - - Показывать через - - - - - - секунд - - - - Время, через которое quiz автоматически откроется - - - ) : ( - <> - )} - - - - - - ); -} - -interface Props { - Icon: React.ElementType; - isActive?: boolean; - onClick: () => void; -} - -function SelectButtonPosition({ Icon, isActive = false, onClick }: Props) { - const theme = useTheme(); - return ( - + + + { + isCurrentFields ? + e.entity === activeScope)} + currentField={selectedField} + currentQuestion={selectedItemId} + setCurrentField={setSelectedField} + setCurrentQuestion={setSelectedQuestion} + /> + : + + } + + + { + onLargeBtnClick() + }} + largeBtnText={"Добавить"} + onSmallBtnClick={onSmallBtnClick} + smallBtnText={"Отменить"} + /> + + + ) +} \ No newline at end of file diff --git a/src/pages/IntegrationsPage/IntegrationsModal/AmoQuestions/Item/AnswerItem/AnswerItem.tsx b/src/pages/IntegrationsPage/IntegrationsModal/AmoQuestions/Item/AnswerItem/AnswerItem.tsx new file mode 100644 index 00000000..b2e7545d --- /dev/null +++ b/src/pages/IntegrationsPage/IntegrationsModal/AmoQuestions/Item/AnswerItem/AnswerItem.tsx @@ -0,0 +1,69 @@ +import { Box, IconButton, Typography, useTheme } from "@mui/material"; +import { FC } from "react"; +import Trash from "@icons/trash"; + +type AnswerItemProps = { + fieldName: string; + fieldValue: string; + deleteHC: () => void; +}; + +export const AnswerItem: FC = ({ fieldName, fieldValue, deleteHC }) => { + const theme = useTheme(); + return ( + + + + {fieldName} + + + + {fieldValue} + + + + + + + ); +}; diff --git a/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep6/Item/IconBtnAdd/IconBtnAdd.tsx b/src/pages/IntegrationsPage/IntegrationsModal/AmoQuestions/Item/IconBtnAdd/IconBtnAdd.tsx similarity index 100% rename from src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep6/Item/IconBtnAdd/IconBtnAdd.tsx rename to src/pages/IntegrationsPage/IntegrationsModal/AmoQuestions/Item/IconBtnAdd/IconBtnAdd.tsx diff --git a/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep6/Item/Item.tsx b/src/pages/IntegrationsPage/IntegrationsModal/AmoQuestions/Item/Item.tsx similarity index 55% rename from src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep6/Item/Item.tsx rename to src/pages/IntegrationsPage/IntegrationsModal/AmoQuestions/Item/Item.tsx index 55c746f9..dd7e748c 100644 --- a/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep6/Item/Item.tsx +++ b/src/pages/IntegrationsPage/IntegrationsModal/AmoQuestions/Item/Item.tsx @@ -1,30 +1,24 @@ -import { Box, Typography, useTheme } from "@mui/material"; +import { Box, IconButton, Typography, useTheme } from "@mui/material"; import { FC } from "react"; import { IconBtnAdd } from "./IconBtnAdd/IconBtnAdd"; import { AnswerItem } from "./AnswerItem/AnswerItem"; -import { - TagKeys, - TitleKeys, - TQuestionEntity, - TTags, -} from "../../IntegrationsModal"; +import { QuestionKeys, SelectedQuestions, TagKeys, SelectedTags, MinifiedData } from "../../types"; type ItemProps = { - title: TitleKeys | TagKeys; + items: MinifiedData[]; + title: QuestionKeys | TagKeys; onAddBtnClick: () => void; - data: TQuestionEntity | TTags; + data: SelectedTags | SelectedQuestions; + deleteHC: (id: string, scope: QuestionKeys | TagKeys) => void; }; -export const Item: FC = ({ title, onAddBtnClick, data }) => { +export const Item: FC = ({ items, title, onAddBtnClick, data, deleteHC }) => { const theme = useTheme(); const titleDictionary = { - contact: "Контакт", - company: "Компания", - deal: "Сделка", - buyer: "Покупатель", - contacts: "Контакты", - users: "Пользователи", - buyers: "Покупатели", + Company: "Компания", + Lead: "Сделка", + Contact: "Контакты", + Customer: "Покупатели", }; const translatedTitle = titleDictionary[title]; @@ -50,16 +44,15 @@ export const Item: FC = ({ title, onAddBtnClick, data }) => { height: "40px", }} > - - {translatedTitle} - + {translatedTitle} {selectedOptions && - selectedOptions.map((text, index) => ( + selectedOptions.map((id, index) => ( e.id === id)?.title || id} + deleteHC={() => deleteHC(selectedOptions[index], title)} /> ))} diff --git a/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep6/ItemDetailsView/ItemDetailsView.tsx b/src/pages/IntegrationsPage/IntegrationsModal/AmoQuestions/ItemDetailsView/ItemDetailsView.tsx similarity index 52% rename from src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep6/ItemDetailsView/ItemDetailsView.tsx rename to src/pages/IntegrationsPage/IntegrationsModal/AmoQuestions/ItemDetailsView/ItemDetailsView.tsx index a22eec6e..dbb25e3f 100644 --- a/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep6/ItemDetailsView/ItemDetailsView.tsx +++ b/src/pages/IntegrationsPage/IntegrationsModal/AmoQuestions/ItemDetailsView/ItemDetailsView.tsx @@ -1,27 +1,36 @@ import { Box, useTheme } from "@mui/material"; -import { Item } from "../Item/Item"; +import { ItemForQuestions } from "../ItemForQuestions"; import { StepButtonsBlock } from "../../StepButtonsBlock/StepButtonsBlock"; import { FC } from "react"; -import { TQuestionEntity } from "../../IntegrationsModal"; +import { MinifiedData, QuestionKeys, SelectedQuestions } from "../../types"; -type TitleKeys = "contacts" | "company" | "deal" | "users" | "buyers"; +type TitleKeys = "Contact" | "Company" | "Lead" | "Customer"; type ItemDetailsViewProps = { + items: MinifiedData[]; setIsSelection: (value: boolean) => void; - handlePrevStep: () => void; - handleNextStep: () => void; - questionEntity: TQuestionEntity; - setActiveItem: (value: string | null) => void; + handleSmallBtn: () => void; + handleLargeBtn: () => void; + selectedQuestions: SelectedQuestions; + setActiveScope: (value: QuestionKeys | null) => void; + deleteHC: (id: string, scope: QuestionKeys) => void; }; export const ItemDetailsView: FC = ({ - handlePrevStep, - handleNextStep, - questionEntity, - setActiveItem, + items, + handleSmallBtn, + handleLargeBtn, + selectedQuestions, setIsSelection, + setActiveScope, + deleteHC, }) => { + console.log(selectedQuestions) const theme = useTheme(); + console.log("items") + console.log(items) + console.log("selectedQuestions") + console.log(selectedQuestions) return ( = ({ justifyContent: "start", }} > - {questionEntity && - Object.keys(questionEntity).map((item) => ( - ( + { setIsSelection(true); - setActiveItem(item); + setActiveScope(item as QuestionKeys); }} - data={questionEntity} + items={items} + data={selectedQuestions} + deleteHC={deleteHC} /> ))} @@ -69,8 +80,9 @@ export const ItemDetailsView: FC = ({ }} > diff --git a/src/pages/IntegrationsPage/IntegrationsModal/AmoQuestions/ItemForQuestions.tsx b/src/pages/IntegrationsPage/IntegrationsModal/AmoQuestions/ItemForQuestions.tsx new file mode 100644 index 00000000..f2483f39 --- /dev/null +++ b/src/pages/IntegrationsPage/IntegrationsModal/AmoQuestions/ItemForQuestions.tsx @@ -0,0 +1,62 @@ +import { Box, IconButton, Typography, useTheme } from "@mui/material"; +import { FC } from "react"; +import { IconBtnAdd } from "./Item/IconBtnAdd/IconBtnAdd"; +import { AnswerItem } from "./Item/AnswerItem/AnswerItem"; +import { QuestionKeys, SelectedQuestions, TagKeys, SelectedTags, MinifiedData } from "../../types"; + +type ItemProps = { + items: MinifiedData[]; + title: QuestionKeys | TagKeys; + onAddBtnClick: () => void; + data: MinifiedData[]; + deleteHC: (id: string, scope: QuestionKeys | TagKeys) => void; +}; +export const ItemForQuestions: FC = ({ items, title, onAddBtnClick, data, deleteHC }) => { + const theme = useTheme(); + + const titleDictionary = { + Company: "Компания", + Lead: "Сделка", + Contact: "Контакты", + Customer: "Покупатели", + }; + + const translatedTitle = titleDictionary[title]; + const selectedOptions = data[title]; + return ( + + + {translatedTitle} + + {selectedOptions && + selectedOptions.map((minifiedData, index) => ( + deleteHC(selectedOptions[index].id, title)} + /> + ))} + + + + ); +}; diff --git a/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep6/ItemsSelectionView/ItemsSelectionView.tsx b/src/pages/IntegrationsPage/IntegrationsModal/AmoQuestions/ItemsSelectionView/ItemsSelectionView.tsx similarity index 72% rename from src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep6/ItemsSelectionView/ItemsSelectionView.tsx rename to src/pages/IntegrationsPage/IntegrationsModal/AmoQuestions/ItemsSelectionView/ItemsSelectionView.tsx index 3523352f..a19a6aed 100644 --- a/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep6/ItemsSelectionView/ItemsSelectionView.tsx +++ b/src/pages/IntegrationsPage/IntegrationsModal/AmoQuestions/ItemsSelectionView/ItemsSelectionView.tsx @@ -2,21 +2,26 @@ import { Box } from "@mui/material"; import { CustomRadioGroup } from "../../../../../components/CustomRadioGroup/CustomRadioGroup"; import { StepButtonsBlock } from "../../StepButtonsBlock/StepButtonsBlock"; import { FC } from "react"; +import { MinifiedData, TagKeys } from "../../types"; type ItemsSelectionViewProps = { - items: string[]; - selectedValue: string | null; - setSelectedValue: (value: string | null) => void; + items: MinifiedData[] | []; + selectedItemId?: string | null; + setSelectedItem: (value: string | null) => void; + handleScroll?: () => void; onLargeBtnClick: () => void; onSmallBtnClick: () => void; + activeScope: TagKeys; }; export const ItemsSelectionView: FC = ({ items, - selectedValue, - setSelectedValue, + selectedItemId, + setSelectedItem, + handleScroll, onLargeBtnClick, onSmallBtnClick, + activeScope, }) => { return ( = ({ > void; + setCurrentQuestion: (value: string) => void; +} +export const NewFields = ({ + items, + currentQuestion, + setCurrentQuestion, +}: Props) => { + + return ( + + + Выберите вопрос для поля. Название поля настроится автоматически + { }} + activeScope={undefined} + /> + + + ) +} \ No newline at end of file diff --git a/src/pages/IntegrationsPage/IntegrationsModal/AmoRemoveAccount/AmoDeleteTagQuestion.tsx b/src/pages/IntegrationsPage/IntegrationsModal/AmoRemoveAccount/AmoDeleteTagQuestion.tsx new file mode 100644 index 00000000..315251e0 --- /dev/null +++ b/src/pages/IntegrationsPage/IntegrationsModal/AmoRemoveAccount/AmoDeleteTagQuestion.tsx @@ -0,0 +1,50 @@ +import { FC } from "react"; +import { Button, Typography, useTheme, Box } from "@mui/material"; + +interface Props { + deleteItem: () => void; + close: () => void; +} + +export const AmoDeleteTagQuestion: FC = ({ close, deleteItem }) => { + const theme = useTheme(); + + return ( + + Вы хотите удалить элемент? + + + + + + ); +}; diff --git a/src/pages/IntegrationsPage/IntegrationsModal/AmoRemoveAccount/AmoRemoveAccount.tsx b/src/pages/IntegrationsPage/IntegrationsModal/AmoRemoveAccount/AmoRemoveAccount.tsx new file mode 100644 index 00000000..381a43b9 --- /dev/null +++ b/src/pages/IntegrationsPage/IntegrationsModal/AmoRemoveAccount/AmoRemoveAccount.tsx @@ -0,0 +1,65 @@ +import { FC } from "react" +import { Button, Typography, useTheme, Box } from "@mui/material" +import { removeAmoAccount } from "@/api/integration"; +import { enqueueSnackbar } from "notistack"; + + +interface Props { + stopThisPage: () => void; + handleCloseModal: () => void; + +} + +export const AmoRemoveAccount: FC = ({ + stopThisPage, + handleCloseModal, + +}: Props) => { + const theme = useTheme(); + const removeAccount = async () => { + const [, error] = await removeAmoAccount() + + if (error) { + enqueueSnackbar(error) + } else { + handleCloseModal() + } + }; + + return ( + + + Вы хотите сменить аккаунт? + + + + + + + ) +} \ No newline at end of file diff --git a/src/pages/IntegrationsPage/IntegrationsModal/AmoTags/AmoTags.tsx b/src/pages/IntegrationsPage/IntegrationsModal/AmoTags/AmoTags.tsx new file mode 100644 index 00000000..14b5e969 --- /dev/null +++ b/src/pages/IntegrationsPage/IntegrationsModal/AmoTags/AmoTags.tsx @@ -0,0 +1,84 @@ +import { FC, useState } from "react"; +import { Box } from "@mui/material"; +import { ItemsSelectionView } from "../AmoQuestions/ItemsSelectionView/ItemsSelectionView"; +import { TagsDetailsView } from "./TagsDetailsView/TagsDetailsView"; +import { MinifiedData, QuestionKeys, SelectedTags, TagKeys, TagQuestionHC } from "../types"; + +type Props = { + tagsItems: MinifiedData[] | []; + selectedTags: SelectedTags; + handleAddTag: (scope: QuestionKeys | TagKeys, id: string, type: "question" | "tag") => void; + openDelete: (data: TagQuestionHC) => void; + handleScroll: () => void; + handlePrevStep: () => void; + handleNextStep: () => void; +}; + +export const AmoTags: FC = ({ + tagsItems, + selectedTags, + handleAddTag, + openDelete, + handleScroll, + handlePrevStep, + handleNextStep, +}) => { + const [isSelection, setIsSelection] = useState(false); + const [activeScope, setActiveScope] = useState(null); + const [selectedTag, setSelectedTag] = useState(null); + + const handleAdd = () => { + if (activeScope === null || selectedTag === null) return; + setActiveScope(null); + handleAddTag(activeScope, selectedTag, "tag"); + }; + const handleDelete = (id: string, scope: TagKeys) => { + openDelete({ + id, + scope, + type: "tag", + }); + }; + + return ( + + {isSelection && activeScope !== null ? ( + // Здесь выбираем элемент в табличку + { + setActiveScope(null); + setIsSelection(false); + }} + onLargeBtnClick={() => { + handleAdd(); + setActiveScope(null); + setIsSelection(false); + }} + /> + ) : ( + // Табличка + + )} + + ); +}; diff --git a/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep7/TagsDetailsView/TagsDetailsView.tsx b/src/pages/IntegrationsPage/IntegrationsModal/AmoTags/TagsDetailsView/TagsDetailsView.tsx similarity index 66% rename from src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep7/TagsDetailsView/TagsDetailsView.tsx rename to src/pages/IntegrationsPage/IntegrationsModal/AmoTags/TagsDetailsView/TagsDetailsView.tsx index 43b050f7..386be0ec 100644 --- a/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep7/TagsDetailsView/TagsDetailsView.tsx +++ b/src/pages/IntegrationsPage/IntegrationsModal/AmoTags/TagsDetailsView/TagsDetailsView.tsx @@ -1,23 +1,27 @@ import { Box, Typography, useTheme } from "@mui/material"; import { StepButtonsBlock } from "../../StepButtonsBlock/StepButtonsBlock"; import { FC } from "react"; -import { TagKeys, TTags } from "../../IntegrationsModal"; -import { Item } from "../../IntegrationStep6/Item/Item"; +import { Item } from "../../AmoQuestions/Item/Item"; +import { MinifiedData, SelectedTags, TagKeys } from "../../types"; type TagsDetailsViewProps = { + items: MinifiedData[]; setIsSelection: (value: boolean) => void; - handleSmallBtn: () => void; - handleLargeBtn: () => void; - tags: TTags; - setActiveItem: (value: string | null) => void; + handlePrevStep: () => void; + handleNextStep: () => void; + setActiveScope: (value: TagKeys | null) => void; + selectedTags: SelectedTags; + deleteHC: (id: string, scope: TagKeys) => void; }; export const TagsDetailsView: FC = ({ - handleSmallBtn, - handleLargeBtn, - tags, - setActiveItem, + items, + setActiveScope, + selectedTags, setIsSelection, + handlePrevStep, + handleNextStep, + deleteHC, }) => { const theme = useTheme(); @@ -52,11 +56,7 @@ export const TagsDetailsView: FC = ({ alignItems: "center", }} > - - Результат - + Результат = ({ justifyContent: "start", }} > - {tags && - Object.keys(tags).map((item) => ( + {selectedTags && + Object.keys(selectedTags).map((item) => ( { setIsSelection(true); - setActiveItem(item); + setActiveScope(item as TagKeys); }} - data={tags} + data={selectedTags} + deleteHC={deleteHC} /> ))} @@ -89,9 +91,8 @@ export const TagsDetailsView: FC = ({ }} > diff --git a/src/pages/IntegrationsPage/IntegrationsModal/AmoTokenExpiredDialog.tsx b/src/pages/IntegrationsPage/IntegrationsModal/AmoTokenExpiredDialog.tsx new file mode 100644 index 00000000..3287341f --- /dev/null +++ b/src/pages/IntegrationsPage/IntegrationsModal/AmoTokenExpiredDialog.tsx @@ -0,0 +1,114 @@ +import { connectAmo } from "@/api/integration"; +import { setTryShowAmoTokenExpiredDialog } from "@/stores/uiTools/actions"; +import { useUiTools } from "@/stores/uiTools/store"; +import CustomCheckbox from "@/ui_kit/CustomCheckbox"; +import { Box, Button, Dialog, Typography, useTheme } from "@mui/material"; +import { useEffect, useState } from "react"; +import { useLocation } from "react-router-dom"; + +const HIDE_DIALOG_EXPIRATION_PERIOD = 24 * 60 * 60 * 1000; + +interface Props { + isAmoTokenExpired: boolean; +} + +export default function AmoTokenExpiredDialog({ isAmoTokenExpired }: Props) { + const theme = useTheme(); + const tryShowAmoTokenExpiredDialog = useUiTools((state) => state.tryShowAmoTokenExpiredDialog); + const [isHideDialogForADayChecked, setIsHideDialogForADayChecked] = useState(false); + const location = useLocation(); + + const onAmoClick = async () => { + const [url, error] = await connectAmo(); + if (url && !error) { + window.open(url, "_blank"); + } + }; + + function handleDialogClose() { + if (isHideDialogForADayChecked) { + const expirationDate = Date.now() + HIDE_DIALOG_EXPIRATION_PERIOD; + localStorage.setItem("hideAmoTokenExpiredDialogExpirationTime", expirationDate.toString()); + } + + setTryShowAmoTokenExpiredDialog(false); + } + + useEffect(() => { + setTryShowAmoTokenExpiredDialog(true); + }, [location]); + + return ( + + + + Ваш amo-токен не работает + + + + + Amo отозвал ваш токен. Зайдите заново в свой аккаунт, чтобы вам снова начали приходить сделки. + + + + + + setIsHideDialogForADayChecked(target.checked)} + /> + + + ); +} diff --git a/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep4/IntegrationStep4.tsx b/src/pages/IntegrationsPage/IntegrationsModal/DealPerformers/DealPerformers.tsx similarity index 80% rename from src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep4/IntegrationStep4.tsx rename to src/pages/IntegrationsPage/IntegrationsModal/DealPerformers/DealPerformers.tsx index 88009b2e..6e90ddc6 100644 --- a/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep4/IntegrationStep4.tsx +++ b/src/pages/IntegrationsPage/IntegrationsModal/DealPerformers/DealPerformers.tsx @@ -2,21 +2,22 @@ import { Box, useMediaQuery, useTheme } from "@mui/material"; import { FC } from "react"; import { StepButtonsBlock } from "../StepButtonsBlock/StepButtonsBlock"; import { CustomSelect } from "../../../../components/CustomSelect/CustomSelect"; +import { MinifiedData } from "../types"; -type IntegrationStep4Props = { +type Props = { + users: MinifiedData[]; handlePrevStep: () => void; handleNextStep: () => void; - selectedDealPerformer: string | null; + selectedDealUser: string | null; setSelectedDealPerformer: (value: string | null) => void; - performers: string[]; }; -export const IntegrationStep4: FC = ({ +export const DealPerformers: FC = ({ + users, handlePrevStep, handleNextStep, - selectedDealPerformer, + selectedDealUser, setSelectedDealPerformer, - performers, }) => { const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down(600)); @@ -34,9 +35,10 @@ export const IntegrationStep4: FC = ({ > {}} /> void; -}; - -interface Values { - login: string; - password: string; -} - -const initialValues: Values = { - login: "", - password: "", -}; - -const validationSchema = object({ - login: string().required("Поле обязательно"), - password: string().required("Поле обязательно").min(8, "Минимум 8 символов"), -}); - -export const IntegrationStep1: FC = ({ - handleNextStep, -}) => { - const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down(600)); - - const formik = useFormik({ - initialValues, - validationSchema, - onSubmit: async (values, formikHelpers) => { - const loginTrimmed = values.login.trim(); - const passwordTrimmed = values.password.trim(); - try { - // Simulate a network request - await new Promise((resolve) => setTimeout(resolve, 2000)); - handleNextStep(); - } catch (error) { - formikHelpers.setSubmitting(false); - if (error instanceof Error) { - formikHelpers.setErrors({ - login: error.message, - password: error.message, - }); - } - } - }, - }); - - return ( - - - - - - - - Инструкция - - - Повседневная практика показывает, что постоянный количественный рост и - сфера нашей активности способствует подготовки и реализации систем - массового участия - - - - - ); -}; diff --git a/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep5/CustomFileUploader/CustomFileUploader.tsx b/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep5/CustomFileUploader/CustomFileUploader.tsx deleted file mode 100644 index 8d055829..00000000 --- a/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep5/CustomFileUploader/CustomFileUploader.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { - Box, - ButtonBase, - Typography, - useMediaQuery, - useTheme, -} from "@mui/material"; -import UploadIcon from "@icons/UploadIcon"; -import { type DragEvent, FC, useRef, useState } from "react"; - -type TextFormat = "txt" | "docx"; - -interface CustomFileUploaderProps { - description?: string; - accept?: TextFormat[]; - handleImageChange: (file: File) => void; -} - -export const CustomFileUploader: FC = ({ - accept, - description, - handleImageChange, -}) => { - const theme = useTheme(); - const dropZone = useRef(null); - const [ready, setReady] = useState(false); - const isMobile = useMediaQuery(theme.breakpoints.down(700)); - - const handleDragEnter = (event: DragEvent) => { - event.preventDefault(); - setReady(true); - }; - - const handleDrop = (event: DragEvent) => { - event.preventDefault(); - event.stopPropagation(); - - const file = event.dataTransfer.files[0]; - if (!file) return; - - handleImageChange(file); - }; - - const acceptedFormats = accept - ? accept.map((format) => "." + format).join(", ") - : ""; - - return ( - - { - const file = event.target.files?.[0]; - if (file) handleImageChange(file); - }} - hidden - accept={acceptedFormats || ".jpg, .jpeg, .png , .gif"} - multiple - type="file" - data-cy="upload-image-input" - /> - ) => - event.preventDefault() - } - onDrop={handleDrop} - ref={dropZone} - sx={{ - width: isMobile ? "100%" : "580px", - padding: isMobile ? "33px" : "33px 10px 33px 55px", - display: "flex", - alignItems: "center", - backgroundColor: theme.palette.background.default, - border: `1px solid ${ready ? "red" : theme.palette.grey2.main}`, - borderRadius: "8px", - gap: "55px", - flexDirection: isMobile ? "column" : "row", - }} - onDragEnter={handleDragEnter} - > - - - - Добавить файл - - - {description || "Принимает JPG, PNG, и GIF формат — максимум 5mb"} - - - - - ); -}; diff --git a/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep5/FileBlock/FileBlock.tsx b/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep5/FileBlock/FileBlock.tsx deleted file mode 100644 index 89d63fcf..00000000 --- a/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep5/FileBlock/FileBlock.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import React, { FC } from "react"; -import Box from "@mui/material/Box"; -import { IconButton, Typography, useTheme } from "@mui/material"; -import CloseIcon from "@mui/icons-material/Close"; - -type FileBlockProps = { - file: File | null; - setFile?: (file: File | null) => void; -}; - -export const FileBlock: FC = ({ setFile, file }) => { - const theme = useTheme(); - return ( - - - Вы загрузили: - - - - {file?.name} - - {setFile && ( - setFile(null)} - sx={{ - backgroundColor: "#864BD9", - borderRadius: "50%", - width: "24px", - height: "24px", - color: "white", - }} - > - - - )} - - - ); -}; diff --git a/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep5/IntegrationStep5.tsx b/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep5/IntegrationStep5.tsx deleted file mode 100644 index dd5a60c1..00000000 --- a/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep5/IntegrationStep5.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { Box, useMediaQuery, useTheme } from "@mui/material"; -import React, { FC } from "react"; -import { StepButtonsBlock } from "../StepButtonsBlock/StepButtonsBlock"; -import File from "@ui_kit/QuizPreview/QuizPreviewQuestionTypes/File"; -import { FileBlock } from "./FileBlock/FileBlock"; -import { CustomFileUploader } from "./CustomFileUploader/CustomFileUploader"; - -type IntegrationStep5Props = { - handlePrevStep: () => void; - handleNextStep: () => void; - setUtmFile: (file: File | null) => void; - utmFile: File | null; -}; - -export const IntegrationStep5: FC = ({ - handlePrevStep, - handleNextStep, - utmFile, - setUtmFile, -}) => { - const theme = useTheme(); - const upMd = useMediaQuery(theme.breakpoints.up("md")); - - return ( - - - {utmFile ? ( - - ) : ( - - )} - - - - ); -}; diff --git a/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep6/IntegrationStep6.tsx b/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep6/IntegrationStep6.tsx deleted file mode 100644 index fb7f7a1b..00000000 --- a/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep6/IntegrationStep6.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { useTheme } from "@mui/material"; -import { - Dispatch, - FC, - SetStateAction, - useCallback, - useMemo, - useState, -} from "react"; -import { ItemsSelectionView } from "./ItemsSelectionView/ItemsSelectionView"; -import { ItemDetailsView } from "./ItemDetailsView/ItemDetailsView"; -import { TitleKeys, TQuestionEntity } from "../IntegrationsModal"; -import Box from "@mui/material/Box"; - -type IntegrationStep6Props = { - handlePrevStep: () => void; - handleNextStep: () => void; - questionEntity: TQuestionEntity; - setQuestionEntity: Dispatch>; -}; - -export const IntegrationStep6: FC = ({ - handlePrevStep, - handleNextStep, - questionEntity, - setQuestionEntity, -}) => { - const theme = useTheme(); - const [isSelection, setIsSelection] = useState(false); - const [activeItem, setActiveItem] = useState(null); - const [selectedValue, setSelectedValue] = useState(null); - - const handleAdd = useCallback(() => { - if (!activeItem || !selectedValue) return; - - setQuestionEntity((prevState) => ({ - ...prevState, - [activeItem]: [...prevState[activeItem as TitleKeys], selectedValue], - })); - }, [activeItem, setQuestionEntity, selectedValue]); - - const items = useMemo( - () => ["Город", "Имя", "Фамилия", "Отчество", "Контрагент"], - [], - ); - - return ( - - {isSelection ? ( - { - setActiveItem(null); - setIsSelection(false); - }} - onLargeBtnClick={() => { - handleAdd(); - setActiveItem(null); - setIsSelection(false); - }} - /> - ) : ( - - )} - - ); -}; diff --git a/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep6/Item/AnswerItem/AnswerItem.tsx b/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep6/Item/AnswerItem/AnswerItem.tsx deleted file mode 100644 index f4082034..00000000 --- a/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep6/Item/AnswerItem/AnswerItem.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { Box, Typography, useTheme } from "@mui/material"; -import { FC } from "react"; - -type AnswerItemProps = { - fieldName: string; - fieldValue: string; -}; - -export const AnswerItem: FC = ({ fieldName, fieldValue }) => { - const theme = useTheme(); - return ( - - - {fieldName} - - - {fieldValue} - - - ); -}; diff --git a/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep7/IntegrationStep7.tsx b/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep7/IntegrationStep7.tsx deleted file mode 100644 index 80607d8b..00000000 --- a/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep7/IntegrationStep7.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { useTheme } from "@mui/material"; -import { - Dispatch, - FC, - SetStateAction, - useCallback, - useMemo, - useState, -} from "react"; - -import { TagKeys, TTags } from "../IntegrationsModal"; -import Box from "@mui/material/Box"; -import { ItemsSelectionView } from "../IntegrationStep6/ItemsSelectionView/ItemsSelectionView"; -import { TagsDetailsView } from "./TagsDetailsView/TagsDetailsView"; - -type IntegrationStep7Props = { - handleSmallBtn: () => void; - handleLargeBtn: () => void; - tags: TTags; - setTags: Dispatch>; -}; - -export const IntegrationStep7: FC = ({ - handleSmallBtn, - handleLargeBtn, - tags, - setTags, -}) => { - const theme = useTheme(); - const [isSelection, setIsSelection] = useState(false); - const [activeItem, setActiveItem] = useState(null); - const [selectedValue, setSelectedValue] = useState(null); - - const handleAdd = useCallback(() => { - if (!activeItem || !selectedValue) return; - - setTags((prevState) => ({ - ...prevState, - [activeItem]: [...prevState[activeItem as TagKeys], selectedValue], - })); - }, [activeItem, setTags, selectedValue]); - - const items = useMemo( - () => ["#тег с результатом 1", "#еще один тег с результатом 2", "#тег"], - [], - ); - - return ( - - {isSelection ? ( - { - setActiveItem(null); - setIsSelection(false); - }} - onLargeBtnClick={() => { - handleAdd(); - setActiveItem(null); - setIsSelection(false); - }} - /> - ) : ( - - )} - - ); -}; diff --git a/src/pages/IntegrationsPage/IntegrationsModal/IntegrationsModal.tsx b/src/pages/IntegrationsPage/IntegrationsModal/IntegrationsModal.tsx deleted file mode 100644 index e78f1d71..00000000 --- a/src/pages/IntegrationsPage/IntegrationsModal/IntegrationsModal.tsx +++ /dev/null @@ -1,270 +0,0 @@ -import { - Dialog, - IconButton, - Typography, - useMediaQuery, - useTheme, -} from "@mui/material"; -import React, { FC, useMemo, useState } from "react"; -import Box from "@mui/material/Box"; -import CloseIcon from "@mui/icons-material/Close"; -import { IntegrationStep1 } from "./IntegrationStep1/IntegrationStep1"; -import { IntegrationStep2 } from "./IntegrationStep2/IntegrationStep2"; -import { IntegrationStep3 } from "./IntegrationStep3/IntegrationStep3"; -import { IntegrationStep4 } from "./IntegrationStep4/IntegrationStep4"; -import { IntegrationStep5 } from "./IntegrationStep5/IntegrationStep5"; -import { IntegrationStep6 } from "./IntegrationStep6/IntegrationStep6"; -import { funnelsMock, performersMock, stagesMock } from "../mocks/MockData"; -import File from "@ui_kit/QuizPreview/QuizPreviewQuestionTypes/File"; -import { IntegrationsModalTitle } from "./IntegrationsModalTitle/IntegrationsModalTitle"; -import { SettingsBlock } from "./SettingsBlock/SettingsBlock"; -import { IntegrationStep7 } from "./IntegrationStep7/IntegrationStep7"; - -export type TitleKeys = "contacts" | "company" | "deal" | "users" | "buyers"; - -export type TQuestionEntity = Record; -type IntegrationsModalProps = { - isModalOpen: boolean; - handleCloseModal: () => void; - companyName: string | null; -}; - -export type TagKeys = "contact" | "company" | "deal" | "buyer"; -export type TTags = Record; - -export const IntegrationsModal: FC = ({ - isModalOpen, - handleCloseModal, - companyName, -}) => { - const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down(600)); - const isTablet = useMediaQuery(theme.breakpoints.down(1000)); - - const [step, setStep] = useState(0); - const [isSettingsBlock, setIsSettingsBlock] = useState(false); - const [selectedFunnelPerformer, setSelectedFunnelPerformer] = useState< - string | null - >(null); - const [selectedFunnel, setSelectedFunnel] = useState(null); - const [selectedStagePerformer, setSelectedStagePerformer] = useState< - string | null - >(null); - const [selectedStage, setSelectedStage] = useState(null); - const [selectedDealPerformer, setSelectedDealPerformer] = useState< - string | null - >(null); - const [utmFile, setUtmFile] = useState(null); - const [questionEntity, setQuestionEntity] = useState({ - contacts: [], - company: [], - deal: [], - users: [], - buyers: [], - }); - const [tags, setTags] = useState({ - deal: [], - contact: [], - company: [], - buyer: [], - }); - - const handleNextStep = () => { - setStep((prevState) => prevState + 1); - }; - const handlePrevStep = () => { - setStep((prevState) => prevState - 1); - }; - const handleSave = () => { - handleCloseModal(); - setStep(1); - }; - - const steps = useMemo( - () => [ - { - title: "Авторизация в аккаунте", - isSettingsAvailable: false, - component: , - }, - { - title: "Выбор воронки", - isSettingsAvailable: true, - component: ( - - ), - }, - { - title: "Выбор этапа воронки", - isSettingsAvailable: true, - component: ( - - ), - }, - { - title: "Сделка", - isSettingsAvailable: true, - component: ( - - ), - }, - { - title: "Добавление utm-меток", - isSettingsAvailable: false, - component: ( - - ), - }, - { - title: "Соотнесение вопросов и сущностей", - isSettingsAvailable: true, - component: ( - - ), - }, - { - title: "Добавление тегов", - isSettingsAvailable: true, - component: ( - - ), - }, - ], - [ - questionEntity, - utmFile, - selectedFunnelPerformer, - selectedFunnel, - selectedStagePerformer, - selectedStage, - selectedDealPerformer, - tags, - ], - ); - - const stepTitles = steps.map((step) => step.title); - - return ( - - - - Интеграция с {companyName ? companyName : "партнером"} - - - - - - - - {isSettingsBlock ? ( - - - - ) : ( - {steps[step].component} - )} - - - ); -}; diff --git a/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep3/IntegrationStep3.tsx b/src/pages/IntegrationsPage/IntegrationsModal/PipelineSteps/PipelineSteps.tsx similarity index 64% rename from src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep3/IntegrationStep3.tsx rename to src/pages/IntegrationsPage/IntegrationsModal/PipelineSteps/PipelineSteps.tsx index 75d77f10..cc009a46 100644 --- a/src/pages/IntegrationsPage/IntegrationsModal/IntegrationStep3/IntegrationStep3.tsx +++ b/src/pages/IntegrationsPage/IntegrationsModal/PipelineSteps/PipelineSteps.tsx @@ -3,27 +3,30 @@ import { FC } from "react"; import { StepButtonsBlock } from "../StepButtonsBlock/StepButtonsBlock"; import { CustomSelect } from "../../../../components/CustomSelect/CustomSelect"; import { CustomRadioGroup } from "../../../../components/CustomRadioGroup/CustomRadioGroup"; +import { MinifiedData } from "../types"; -type IntegrationStep3Props = { +type Props = { + users: MinifiedData[]; + steps: MinifiedData[]; handlePrevStep: () => void; handleNextStep: () => void; - selectedStagePerformer: string | null; - setSelectedStagePerformer: (value: string | null) => void; - selectedStage: string | null; - setSelectedStage: (value: string | null) => void; - performers: string[]; - stages: string[]; + selectedDealUser: string | null; + setSelectedDealPerformer: (value: string | null) => void; + selectedStep: string | null; + setSelectedStep: (value: string | null) => void; }; -export const IntegrationStep3: FC = ({ +export const PipelineSteps: FC = ({ + users, + selectedDealUser, + setSelectedDealPerformer, + + steps, + selectedStep, + setSelectedStep, + handlePrevStep, handleNextStep, - selectedStagePerformer, - setSelectedStagePerformer, - selectedStage, - setSelectedStage, - performers, - stages, }) => { const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down(600)); @@ -41,9 +44,10 @@ export const IntegrationStep3: FC = ({ > {}} /> = ({ }} > {}} /> void; handleNextStep: () => void; - selectedFunnelPerformer: string | null; - setSelectedFunnelPerformer: (value: string | null) => void; - selectedFunnel: string | null; - setSelectedFunnel: (value: string | null) => void; - performers: string[]; - funnels: string[]; + selectedDealUser: string | null; + setSelectedDealPerformer: (value: string | null) => void; + selectedPipeline: string | null; + setSelectedPipeline: (value: string | null) => void; }; -export const IntegrationStep2: FC = ({ +export const Pipelines: FC = ({ + pipelines, + selectedPipeline, + setSelectedPipeline, + + users, + selectedDealUser, + setSelectedDealPerformer, + handlePrevStep, handleNextStep, - selectedFunnelPerformer, - setSelectedFunnelPerformer, - selectedFunnel, - setSelectedFunnel, - performers, - funnels, }) => { const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down(600)); @@ -40,9 +43,10 @@ export const IntegrationStep2: FC = ({ > {}} /> = ({ }} > {}} /> void; setIsSettingsBlock: (value: boolean) => void; - selectedFunnelPerformer: string | null; selectedFunnel: string | null; - selectedStagePerformer: string | null; selectedStage: string | null; - selectedDealPerformer: string | null; - utmFile: File | null; - questionEntity: TQuestionEntity; - tags: TTags; + selectedDealUser: string | null; + selectedQuestions: SelectedQuestions; + selectedTags: SelectedTags; }; -export const SettingsBlock: FC = ({ +export const AmoSettingsBlock: FC = ({ stepTitles, setStep, setIsSettingsBlock, - selectedFunnelPerformer, selectedFunnel, - selectedStagePerformer, - selectedDealPerformer, + selectedDealUser, selectedStage, - utmFile, - questionEntity, - tags, + selectedQuestions, + selectedTags, }) => { const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down(600)); @@ -63,14 +57,11 @@ export const SettingsBlock: FC = ({ title={title} setIsSettingsBlock={setIsSettingsBlock} setStep={setStep} - selectedFunnelPerformer={selectedFunnelPerformer} + selectedDealUser={selectedDealUser} selectedFunnel={selectedFunnel} - selectedStagePerformer={selectedStagePerformer} - selectedDealPerformer={selectedDealPerformer} selectedStage={selectedStage} - utmFile={utmFile} - questionEntity={questionEntity} - tags={tags} + selectedQuestions={selectedQuestions} + selectedTags={selectedTags} /> ))} diff --git a/src/pages/IntegrationsPage/IntegrationsModal/SettingsBlock/SettingItem/SettingItem.tsx b/src/pages/IntegrationsPage/IntegrationsModal/SettingsBlock/SettingItem/SettingItem.tsx index 80ace601..ee1614be 100644 --- a/src/pages/IntegrationsPage/IntegrationsModal/SettingsBlock/SettingItem/SettingItem.tsx +++ b/src/pages/IntegrationsPage/IntegrationsModal/SettingsBlock/SettingItem/SettingItem.tsx @@ -4,8 +4,7 @@ import { Typography, useMediaQuery, useTheme } from "@mui/material"; import { SettingItemHeader } from "./SettingItemHeader/SettingItemHeader"; import { ResponsiblePerson } from "./ResponsiblePerson/ResponsiblePerson"; import { SelectedParameter } from "./SelectedParameter/SelectedParameter"; -import { FileBlock } from "../../IntegrationStep5/FileBlock/FileBlock"; -import { TQuestionEntity, TTags } from "../../IntegrationsModal"; +import { SelectedQuestions, SelectedTags } from "../../types"; type SettingItemProps = { step: number; @@ -15,11 +14,10 @@ type SettingItemProps = { selectedFunnelPerformer: string | null; selectedFunnel: string | null; selectedStagePerformer: string | null; - selectedDealPerformer: string | null; + selectedDealUser: string | null; selectedStage: string | null; - utmFile: File | null; - questionEntity: TQuestionEntity; - tags: TTags; + selectedQuestions: SelectedQuestions; + selectedTags: SelectedTags; }; export const SettingItem: FC = ({ @@ -30,15 +28,13 @@ export const SettingItem: FC = ({ selectedFunnelPerformer, selectedFunnel, selectedStagePerformer, - selectedDealPerformer, + selectedDealUser, selectedStage, - utmFile, - questionEntity, - tags, + selectedQuestions, + selectedTags, }) => { const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down(600)); - if (step === 0) { return; } @@ -47,7 +43,7 @@ export const SettingItem: FC = ({ if (step === 1) { return ( <> - + ); @@ -55,7 +51,7 @@ export const SettingItem: FC = ({ if (step === 2) { return ( <> - + ); @@ -63,25 +59,12 @@ export const SettingItem: FC = ({ if (step === 3) { return ( <> - + ); } if (step === 4) { - return ( - - {utmFile ? ( - - ) : ( - Файл не загружен - )} - - ); - } - if (step === 5) { - const isFilled = Object.values(questionEntity).some( - (array) => array.length > 0, - ); + const isFilled = Object.values(selectedTags).some((array) => array.length > 0); const status = isFilled ? "Заполнено" : "Не заполнено"; return ( @@ -110,8 +93,8 @@ export const SettingItem: FC = ({ ); } - if (step === 6) { - const isFilled = Object.values(tags).some((array) => array.length > 0); + if (step === 5) { + const isFilled = Object.values(selectedQuestions).some((array) => array.length > 0); const status = isFilled ? "Заполнено" : "Не заполнено"; return ( @@ -146,11 +129,10 @@ export const SettingItem: FC = ({ selectedFunnelPerformer, selectedFunnel, selectedStagePerformer, - selectedDealPerformer, + selectedDealUser, selectedStage, - utmFile, - questionEntity, - tags, + selectedQuestions, + selectedTags, ]); return ( diff --git a/src/pages/IntegrationsPage/IntegrationsModal/types.ts b/src/pages/IntegrationsPage/IntegrationsModal/types.ts new file mode 100644 index 00000000..dca381e4 --- /dev/null +++ b/src/pages/IntegrationsPage/IntegrationsModal/types.ts @@ -0,0 +1,19 @@ +export type TagKeys = "Company" | "Lead" | "Customer" | "Contact"; +export type SelectedTags = Record; + +export type QuestionKeys = "Company" | "Lead" | "Customer" | "Contact"; +export type SelectedQuestions = Record; + +export type MinifiedData = { + id: string; + title: string; + subTitle?: string; + entity?: TagKeys; + amoId?: string; +}; + +export type TagQuestionHC = { + scope: QuestionKeys | TagKeys; + id: string; + type: "question" | "tag"; +}; diff --git a/src/pages/IntegrationsPage/IntegrationsModal/useAmoIntegration.ts b/src/pages/IntegrationsPage/IntegrationsModal/useAmoIntegration.ts new file mode 100644 index 00000000..b48e5b4d --- /dev/null +++ b/src/pages/IntegrationsPage/IntegrationsModal/useAmoIntegration.ts @@ -0,0 +1,295 @@ +import { useEffect, useState } from "react"; +import { enqueueSnackbar } from "notistack"; +import type { TagKeys, SelectedTags, QuestionKeys, SelectedQuestions, MinifiedData } from "./types"; +import { + AccountResponse, + getIntegrationRules, + getPipelines, + getSteps, + getTags, + getUsers, + getAccount, + FieldsRule, + getFields, +} from "@/api/integration"; +import { AnyTypedQuizQuestion } from "@frontend/squzanswerer"; + +const SIZE = 175; + +interface Props { + isModalOpen: boolean; + isTryRemoveAccount: boolean; + quizID: number; + questions: AnyTypedQuizQuestion +} + +const FCTranslate = { + "name": "имя", + "email": "почта", + "phone": "телефон", + "text": "номер", + "address": "адрес", +} +export const useAmoIntegration = ({ isModalOpen, isTryRemoveAccount, quizID, questions }: Props) => { + const [isLoadingPage, setIsLoadingPage] = useState(true); + const [firstRules, setFirstRules] = useState(false); + const [accountInfo, setAccountInfo] = useState(null); + + const [arrayOfPipelines, setArrayOfPipelines] = useState([]); + const [arrayOfPipelinesSteps, setArrayOfPipelinesSteps] = useState([]); + const [arrayOfUsers, setArrayOfUsers] = useState([]); + const [arrayOfTags, setArrayOfTags] = useState([]); + const [arrayOfFields, setArrayOfFields] = useState([]); + + const [selectedPipeline, setSelectedPipeline] = useState(null); + const [selectedPipelineStep, setSelectedPipelineStep] = useState(null); + const [selectedDealUser, setSelectedDealPerformer] = useState(null); + const [selectedCurrentFields, setSelectedCurrentFields] = useState([]); + + const [questionsBackend, setQuestionsBackend] = useState({} as FieldsRule); + const [selectedTags, setSelectedTags] = useState({ + Lead: [], + Contact: [], + Company: [], + Customer: [], + }); + const [selectedQuestions, setSelectedQuestions] = useState({ + Lead: [], + Company: [], + Customer: [], + Contact: [] + }); + + const [pageOfPipelines, setPageOfPipelines] = useState(1); + const [pageOfPipelinesSteps, setPageOfPipelinesSteps] = useState(1); + const [pageOfUsers, setPageOfUsers] = useState(1); + const [pageOfTags, setPageOfTags] = useState(1); + const [pageOfFields, setPageOfFields] = useState(1); + + useEffect(() => { + const fetchAccountRules = async () => { + setIsLoadingPage(true); + const [account, accountError] = await getAccount(); + + if (accountError) { + if (!accountError.includes("Not Found")) enqueueSnackbar(accountError); + setAccountInfo(null); + } + if (account) { + setAccountInfo(account); + } + const [settingsResponse, rulesError] = await getIntegrationRules(quizID.toString()); + + if (rulesError) { + if (rulesError === "first") setFirstRules(true); + if (!rulesError.includes("Not Found") && !rulesError.includes("first")) enqueueSnackbar(rulesError); + } + if (settingsResponse) { + if (settingsResponse.PipelineID) setSelectedPipeline(settingsResponse.PipelineID.toString()); + if (settingsResponse.StepID) setSelectedPipelineStep(settingsResponse.StepID.toString()); + if (settingsResponse.PerformerID) setSelectedDealPerformer(settingsResponse.PerformerID.toString()); + + if (Boolean(settingsResponse.FieldsRule) && Object.keys(settingsResponse?.FieldsRule).length > 0) { + const gottenQuestions = { ...selectedQuestions }; + setQuestionsBackend(settingsResponse.FieldsRule); + + for (let key in settingsResponse.FieldsRule) { + if ( + settingsResponse.FieldsRule[key as QuestionKeys] !== null + ) { + const gottenList = settingsResponse.FieldsRule[key as QuestionKeys]; + + if (gottenList !== null) { + Object.keys(gottenList.QuestionID).forEach((qId) => { + console.log(qId) + console.log(questions) + const q = questions.find(e=>e.backendId === Number(qId)) || {} + gottenQuestions[key as QuestionKeys].push({ + id: qId, + title: q.title, + entity: key, + + }) + }) + + } + + if (key === "Contact") { + const MAP = settingsResponse.FieldsRule[key as QuestionKeys].ContactRuleMap + + const list = [] + for (let key in MAP) { + list.push({ + id: key, + title: FCTranslate[key], + entity: "Contact", + amoId: MAP[key].toString(), + }) + } + console.log(list) + setSelectedCurrentFields(list) + } + } + } + setSelectedQuestions(gottenQuestions); + } + + if (Boolean(settingsResponse.TagsToAdd) && Object.keys(settingsResponse.TagsToAdd).length > 0) { + const gottenTags = { ...selectedTags }; + + for (let key in settingsResponse.TagsToAdd) { + const gottenList = settingsResponse.TagsToAdd[key as TagKeys]; + if (gottenList !== null && Array.isArray(gottenList)) { + gottenTags[key as TagKeys] = gottenList.map((e) => e.toString()); + } + } + setSelectedTags(gottenTags); + } + setFirstRules(false); + } + setIsLoadingPage(false); + }; + + fetchAccountRules(); + + }, [isModalOpen, isTryRemoveAccount]); + + useEffect(() => { + getPipelines({ + page: pageOfPipelines, + size: SIZE, + }).then(([response]) => { + if (response && response.items !== null) { + const minifiedPipelines: MinifiedData[] = []; + + response.items.forEach((step) => { + minifiedPipelines.push({ + id: step.AmoID.toString(), + title: step.Name, + }); + }); + setArrayOfPipelines((prevItems) => [...prevItems, ...minifiedPipelines]); + setPageOfPipelinesSteps(1); + } + }); + }, [pageOfPipelines]); + useEffect(() => { + const oldData = pageOfPipelinesSteps === 1 ? [] : arrayOfPipelinesSteps; + if (selectedPipeline !== null) + getSteps({ + page: pageOfPipelinesSteps, + size: SIZE, + pipelineId: Number(selectedPipeline), + }).then(([response]) => { + if (response && response.items !== null) { + const minifiedSteps: MinifiedData[] = []; + + response.items.forEach((step) => { + minifiedSteps.push({ + id: step.AmoID.toString(), + title: step.Name, + }); + }); + setArrayOfPipelinesSteps([...oldData, ...minifiedSteps]); + } + }); + }, [selectedPipeline, pageOfPipelinesSteps]); + useEffect(() => { + getUsers({ + page: pageOfUsers, + size: SIZE, + }).then(([response]) => { + if (response && response.items !== null) { + const minifiedUsers: MinifiedData[] = []; + + response.items.forEach((step) => { + minifiedUsers.push({ + id: step.amoUserID.toString(), + title: step.name, + }); + }); + setArrayOfUsers((prevItems) => [...prevItems, ...minifiedUsers]); + } + }); + }, [pageOfUsers]); + useEffect(() => { + getTags({ + page: pageOfTags, + size: SIZE, + }).then(([response]) => { + if (response && response.items !== null) { + const minifiedTags: MinifiedData[] = []; + + response.items.forEach((step) => { + minifiedTags.push({ + id: step.AmoID.toString(), + title: step.Name, + entity: + step.Entity === "leads" + ? "Lead" + : step.Entity === "contacts" + ? "Contact" + : step.Entity === "companies" + ? "Company" + : "Customer", + }); + }); + setArrayOfTags((prevItems) => [...prevItems, ...minifiedTags]); + } + }); + }, [pageOfTags]); + useEffect(() => { + getFields({ + page: pageOfTags, + size: SIZE, + }).then(([response]) => { + if (response && response.items !== null) { + const minifiedTags: MinifiedData[] = []; + + response.items.forEach((field) => { + minifiedTags.push({ + id: field.AmoID.toString(), + title: field.Name, + entity: + field.Entity === "leads" + ? "Lead" + : field.Entity === "contacts" + ? "Contact" + : field.Entity === "companies" + ? "Company" + : "Customer", + }); + }); + setArrayOfFields((prevItems) => [...prevItems, ...minifiedTags]); + } + }); + }, [pageOfFields]); + + return { + isLoadingPage, + firstRules, + accountInfo, + arrayOfPipelines, + arrayOfPipelinesSteps, + arrayOfUsers, + arrayOfTags, + arrayOfFields, + selectedPipeline, + setSelectedPipeline, + selectedCurrentFields, + selectedPipelineStep, + setSelectedPipelineStep, + selectedDealUser, + setSelectedDealPerformer, + questionsBackend, + selectedTags, + setSelectedTags, + selectedQuestions, + setSelectedQuestions, + setPageOfPipelines, + setPageOfPipelinesSteps, + setPageOfUsers, + setPageOfTags, + setSelectedCurrentFields, + }; +}; diff --git a/src/pages/IntegrationsPage/IntegrationsPage.tsx b/src/pages/IntegrationsPage/IntegrationsPage.tsx index 7357dca1..b74e3650 100644 --- a/src/pages/IntegrationsPage/IntegrationsPage.tsx +++ b/src/pages/IntegrationsPage/IntegrationsPage.tsx @@ -5,7 +5,6 @@ import { useCurrentQuiz } from "@root/quizes/hooks"; import { useQuizStore } from "@root/quizes/store"; import { useNavigate } from "react-router-dom"; import { PartnersBoard } from "./PartnersBoard/PartnersBoard"; -import { partnersMock } from "./mocks/MockData"; import { QuizMetricType } from "@model/quizSettings"; interface IntegrationsPageProps { @@ -26,6 +25,9 @@ export const IntegrationsPage = ({ const [companyName, setCompanyName] = useState< keyof typeof QuizMetricType | null >(null); + + const [isAmoCrmModalOpen, setIsAmoCrmModalOpen] = useState(false); + useEffect(() => { if (editQuizId === null) navigate("/list"); }, [navigate, editQuizId]); @@ -38,9 +40,9 @@ export const IntegrationsPage = ({ const handleCloseModal = () => { setIsModalOpen(false); - // setTimeout(() => { - // setCompanyName(null); - // }, 300); + }; + const handleCloseAmoSRMModal = () => { + setIsAmoCrmModalOpen(false); }; return ( @@ -63,18 +65,15 @@ export const IntegrationsPage = ({ Интеграции - {/**/} ); diff --git a/src/pages/IntegrationsPage/PartnersBoard/AnalyticsModal/AnalyticsModal.tsx b/src/pages/IntegrationsPage/PartnersBoard/AnalyticsModal/AnalyticsModal.tsx index 677c117b..4ad0f815 100644 --- a/src/pages/IntegrationsPage/PartnersBoard/AnalyticsModal/AnalyticsModal.tsx +++ b/src/pages/IntegrationsPage/PartnersBoard/AnalyticsModal/AnalyticsModal.tsx @@ -25,11 +25,11 @@ interface Props { companyName: keyof typeof QuizMetricType; } -export default function AnalyticsModal({ +export const AnalyticsModal = ({ isModalOpen, handleCloseModal, companyName, -}: Props) { +}: Props) => { const theme = useTheme(); const quiz = useCurrentQuiz(); const isMobile = useMediaQuery(theme.breakpoints.down(600)); @@ -260,4 +260,4 @@ export default function AnalyticsModal({ ); -} +}; diff --git a/src/pages/IntegrationsPage/PartnersBoard/AnalyticsModal/IntsructionsBlock/Instructions/VKInstruction.tsx b/src/pages/IntegrationsPage/PartnersBoard/AnalyticsModal/IntsructionsBlock/Instructions/VKInstruction.tsx index c899bdb4..f9db1a6d 100644 --- a/src/pages/IntegrationsPage/PartnersBoard/AnalyticsModal/IntsructionsBlock/Instructions/VKInstruction.tsx +++ b/src/pages/IntegrationsPage/PartnersBoard/AnalyticsModal/IntsructionsBlock/Instructions/VKInstruction.tsx @@ -136,8 +136,7 @@ export const VKPixelInstruction = () => { • Посетитель отправил заявку с заполненным полем Х: - penaquiz-formfield-X, где X — одно из полей. - Например, + penaquiz-formfield-X, где X — одно из полей. Например, @@ -163,7 +162,8 @@ export const VKPixelInstruction = () => { - penaquiz-formfield-text (это будет кастомное поле, которое вы настроили сами) + penaquiz-formfield-text (это будет кастомное поле, + которое вы настроили сами) diff --git a/src/pages/IntegrationsPage/PartnersBoard/AnalyticsModal/IntsructionsBlock/Instructions/YandexInstruction.tsx b/src/pages/IntegrationsPage/PartnersBoard/AnalyticsModal/IntsructionsBlock/Instructions/YandexInstruction.tsx index b7ba8d52..310e520f 100644 --- a/src/pages/IntegrationsPage/PartnersBoard/AnalyticsModal/IntsructionsBlock/Instructions/YandexInstruction.tsx +++ b/src/pages/IntegrationsPage/PartnersBoard/AnalyticsModal/IntsructionsBlock/Instructions/YandexInstruction.tsx @@ -132,8 +132,7 @@ export const YandexInstruction = () => { • Посетитель отправил заявку с заполненным полем Х: - penaquiz-formfield-X, где X — одно из полей. - Например, + penaquiz-formfield-X, где X — одно из полей. Например, @@ -159,7 +158,8 @@ export const YandexInstruction = () => { - penaquiz-formfield-text (это будет кастомное поле, которое вы настроили сами) + penaquiz-formfield-text (это будет кастомное поле, + которое вы настроили сами) diff --git a/src/pages/IntegrationsPage/PartnersBoard/PartnerItem/PartnerItem.tsx b/src/pages/IntegrationsPage/PartnersBoard/PartnerItem/PartnerItem.tsx deleted file mode 100644 index b780ed4e..00000000 --- a/src/pages/IntegrationsPage/PartnersBoard/PartnerItem/PartnerItem.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { Box, Typography, useTheme } from "@mui/material"; -import { FC } from "react"; -import { Partner } from "../PartnersBoard"; - -type PartnerItemProps = { - partner: Partner; - setIsModalOpen: (value: boolean) => void; - setCompanyName: (value: string) => void; -}; - -export const PartnerItem: FC = ({ - partner, - setIsModalOpen, - setCompanyName, -}) => { - const theme = useTheme(); - - const handleClick = () => { - setCompanyName(partner.name); - setIsModalOpen(true); - }; - - return ( - <> - {partner && ( - - {partner.logo ? ( - {partner.name} - ) : ( - {partner.name} - )} - - )} - - ); -}; diff --git a/src/pages/IntegrationsPage/PartnersBoard/PartnersBoard.tsx b/src/pages/IntegrationsPage/PartnersBoard/PartnersBoard.tsx index e39d98e8..da684628 100644 --- a/src/pages/IntegrationsPage/PartnersBoard/PartnersBoard.tsx +++ b/src/pages/IntegrationsPage/PartnersBoard/PartnersBoard.tsx @@ -1,43 +1,53 @@ -import { Box, Typography, useTheme } from "@mui/material"; -import { FC } from "react"; +import { Box, Typography, useMediaQuery, useTheme } from "@mui/material"; +import React, { FC, lazy, Suspense } from "react"; import { ServiceButton } from "./ServiceButton/ServiceButton"; import { YandexMetricaLogo } from "../mocks/YandexMetricaLogo"; -import AnalyticsModal from "./AnalyticsModal/AnalyticsModal"; +// import AnalyticsModal from "./AnalyticsModal/AnalyticsModal"; import { VKPixelLogo } from "../mocks/VKPixelLogo"; import { QuizMetricType } from "@model/quizSettings"; +import { AmoCRMLogo } from "../mocks/AmoCRMLogo"; +import { useCurrentQuiz } from "@/stores/quizes/hooks"; +import { useUserStore } from "@/stores/user"; -export type Partner = { - name: string; - logo?: string; - category: string; -}; +const AnalyticsModal = lazy(() => + import("./AnalyticsModal/AnalyticsModal").then((module) => ({ + default: module.AnalyticsModal, + })) +); + +const AmoCRMModal = lazy(() => + import("../IntegrationsModal/AmoCRMModal").then((module) => ({ + default: module.AmoCRMModal, + })) +); type PartnersBoardProps = { - partners: Partner[]; setIsModalOpen: (value: boolean) => void; companyName: keyof typeof QuizMetricType | null; setCompanyName: (value: keyof typeof QuizMetricType) => void; isModalOpen: boolean; handleCloseModal: () => void; + setIsAmoCrmModalOpen: (value: boolean) => void; + isAmoCrmModalOpen: boolean; + handleCloseAmoSRMModal: () => void; }; export const PartnersBoard: FC = ({ - partners, setIsModalOpen, isModalOpen, handleCloseModal, companyName, setCompanyName, + setIsAmoCrmModalOpen, + isAmoCrmModalOpen, + handleCloseAmoSRMModal, }) => { const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down(600)); - // const partnersByCategory = partners.reduce( - // (acc, partner) => { - // (acc[partner.category] = acc[partner.category] || []).push(partner); - // return acc; - // }, - // {} as Record, - // ); + const quiz = useCurrentQuiz(); + + const user = useUserStore(); return ( = ({ justifyContent: { xs: "center", sm: "center", md: "start" }, }} > - {/*{Object.entries(partnersByCategory).map(([category, partners]) => (*/} - {/* */} - {/* */} - {/* {category}*/} - {/* */} - {/* */} - {/* {partners.map((partner) => (*/} - {/* */} - {/* ))}*/} - - {/* */} - {/* */} - {/*))}*/} - + { + user.user._id === "6692e068983ee77f8e1e682e" && + <> + + CRM + + + } + setIsModalOpen={setIsAmoCrmModalOpen} + setCompanyName={setCompanyName} + name={"amoCRM"} + /> + + + } + = ({ {companyName && ( - + + + + )} + {companyName && isAmoCrmModalOpen && ( + + + )} ); diff --git a/src/pages/IntegrationsPage/mocks/AmoCRMLogo.tsx b/src/pages/IntegrationsPage/mocks/AmoCRMLogo.tsx new file mode 100644 index 00000000..be6b1460 --- /dev/null +++ b/src/pages/IntegrationsPage/mocks/AmoCRMLogo.tsx @@ -0,0 +1,6 @@ +import React from "react"; +import { ReactComponent as AmoLogo } from "./amoCRMLogo.svg"; + +export const AmoCRMLogo = () => { + return ; +}; diff --git a/src/pages/IntegrationsPage/mocks/MockData.ts b/src/pages/IntegrationsPage/mocks/MockData.ts index 22094d4f..e38462fc 100644 --- a/src/pages/IntegrationsPage/mocks/MockData.ts +++ b/src/pages/IntegrationsPage/mocks/MockData.ts @@ -1,9 +1,3 @@ -import amoCrmLogo from "./amoCrmLogo.png"; - -export const partnersMock = [ - { category: "CRM", name: "amoCRM", logo: amoCrmLogo }, -]; - export const performersMock = [ "Ангелина Полякова", "Петр Иванов", diff --git a/src/pages/IntegrationsPage/mocks/amoCRMLogo.svg b/src/pages/IntegrationsPage/mocks/amoCRMLogo.svg new file mode 100644 index 00000000..c531cc0b --- /dev/null +++ b/src/pages/IntegrationsPage/mocks/amoCRMLogo.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/pages/IntegrationsPage/mocks/amoCrmLogo.png b/src/pages/IntegrationsPage/mocks/amoCrmLogo.png deleted file mode 100644 index 07d03d78..00000000 Binary files a/src/pages/IntegrationsPage/mocks/amoCrmLogo.png and /dev/null differ diff --git a/src/pages/Landing/HowItWorks.tsx b/src/pages/Landing/HowItWorks.tsx index 34accaba..d7cc6452 100644 --- a/src/pages/Landing/HowItWorks.tsx +++ b/src/pages/Landing/HowItWorks.tsx @@ -2,10 +2,6 @@ import React from "react"; import Box from "@mui/material/Box"; import { Typography, useMediaQuery, useTheme } from "@mui/material"; import SectionStyled from "./SectionStyled"; -import OneIconBorder from "@icons/OneIconBorder"; -import TwoIcon from "./images/icons/IconNumber2"; -import ThreeIcon from "./images/icons/IconNumber3"; -import FourIcon from "./images/icons/IconNumber4"; import Firstblock1 from "./images/firstblock1.png"; import Firstblock2 from "./images/firstblock2.png"; import Firstblock3 from "./images/firstblock3.png"; @@ -28,6 +24,7 @@ import Icon16 from "./images/icons/Group149"; import Icon17 from "./images/icons/Group151"; import Icon19 from "./images/icons/Group153"; import Icon21 from "./images/icons/Network"; +import NumberIcon from "@icons/NumberIcon"; export default function HowItWorks() { const theme = useTheme(); @@ -88,19 +85,7 @@ export default function HowItWorks() { boxSizing: "border-box", }} > - - - + - - {" "} - на сайте - + на сайте - - - - + - - - + - - - + + {/* */} diff --git a/src/pages/Landing/Tariffs/TariffCard/TariffCard.tsx b/src/pages/Landing/Tariffs/TariffCard/TariffCard.tsx new file mode 100644 index 00000000..c9c94739 --- /dev/null +++ b/src/pages/Landing/Tariffs/TariffCard/TariffCard.tsx @@ -0,0 +1,111 @@ +import { Box, Button, Typography, useMediaQuery, useTheme } from "@mui/material"; +import { FC } from "react"; +import { DurationTariffIcon } from "@/pages/Landing/Tariffs/TariffCard/icons/DurationTariffIcon"; +import { TrialTariffIcon } from "@/pages/Landing/Tariffs/TariffCard/icons/TrialTariffIcon"; +import { RequestsTariffIcon } from "@/pages/Landing/Tariffs/TariffCard/icons/RequestsTariffIcon"; +import { Link, useLocation } from "react-router-dom"; + +type TariffCardProps = { + type: "requests" | "duration" | "trial"; + name: string; + description: string; + actualPrice: string; + oldPrice?: string; + discount?: string; +}; + +const icons = { + requests: , + duration: , + trial: , +}; + +export const TariffCard: FC = ({ type, name, actualPrice, oldPrice, discount, description }) => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down(600)); + const isTablet = useMediaQuery(theme.breakpoints.down(1140)); + + const location = useLocation(); + + return ( + + + + {icons[type]} + + {discount && -{discount}%} + + + + {oldPrice && ( + + {oldPrice} ₽ + + )} + {actualPrice && ( + + {actualPrice} ₽ + + )} + + + + {name && {name}} + {description && {description}} + + + + + + ); +}; diff --git a/src/pages/Landing/Tariffs/TariffCard/icons/DurationTariffIcon.tsx b/src/pages/Landing/Tariffs/TariffCard/icons/DurationTariffIcon.tsx new file mode 100644 index 00000000..64d6b65a --- /dev/null +++ b/src/pages/Landing/Tariffs/TariffCard/icons/DurationTariffIcon.tsx @@ -0,0 +1,18 @@ +import { Box } from "@mui/material"; + +export const DurationTariffIcon = () => { + return ( + + + + + + + + ); +}; diff --git a/src/pages/Landing/Tariffs/TariffCard/icons/RequestsTariffIcon.tsx b/src/pages/Landing/Tariffs/TariffCard/icons/RequestsTariffIcon.tsx new file mode 100644 index 00000000..d39b29c8 --- /dev/null +++ b/src/pages/Landing/Tariffs/TariffCard/icons/RequestsTariffIcon.tsx @@ -0,0 +1,48 @@ +import { Box } from "@mui/material"; + +export const RequestsTariffIcon = () => { + return ( + + + + + + + + + + + + ); +}; diff --git a/src/pages/Landing/Tariffs/TariffCard/icons/TrialTariffIcon.tsx b/src/pages/Landing/Tariffs/TariffCard/icons/TrialTariffIcon.tsx new file mode 100644 index 00000000..b4862af7 --- /dev/null +++ b/src/pages/Landing/Tariffs/TariffCard/icons/TrialTariffIcon.tsx @@ -0,0 +1,27 @@ +import { Box } from "@mui/material"; + +export const TrialTariffIcon = () => { + return ( + + + + + + + + + + ); +}; diff --git a/src/pages/Landing/Tariffs/Tariffs.tsx b/src/pages/Landing/Tariffs/Tariffs.tsx new file mode 100644 index 00000000..8628e66c --- /dev/null +++ b/src/pages/Landing/Tariffs/Tariffs.tsx @@ -0,0 +1,173 @@ +import Box from "@mui/material/Box"; +import { CssBaseline, Typography, useMediaQuery, useTheme } from "@mui/material"; +import { Swiper, SwiperSlide } from "swiper/react"; +import { Navigation, Pagination } from "swiper/modules"; +import SectionStyled from "@/pages/Landing/SectionStyled"; +import React, { useMemo } from "react"; +import { TariffCard } from "@/pages/Landing/Tariffs/TariffCard/TariffCard"; +import "swiper/css"; +import "swiper/css/pagination"; +import "swiper/css/navigation"; + +export type Tariff = { + name: string; + type: "requests" | "duration" | "trial"; + description: string; + actualPrice: string; + oldPrice?: string; + discount?: string; +}; + +const swiperStyles = ` +.mySwiper .swiper-pagination-bullet { + background: #9A9AAF; + margin-top: 40px; +} +.mySwiper .swiper-pagination { + position: relative; + margin-top: 30px; +} +.mySwiper .swiper-button-next, .mySwiper .swiper-button-prev { + width: 37.5px; + height: 37.5px; + background: white; + border-radius: 50%; + margin-top: -40px; +} +.mySwiper .swiper-button-next:after, .mySwiper .swiper-button-prev:after { + color: black; + font-size: 20px; +} +`; + +export const Tariffs = () => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down(600)); + const isTablet = useMediaQuery(theme.breakpoints.down(1000)); + + const tariffs: Tariff[] = useMemo( + () => [ + { + name: "Бесплатно 14 дней", + type: "trial", + description: + "Каждому пользователю все наши продукты в первые 14 дней доступны совершенно бесплатно (кроме доп.услуг)", + actualPrice: "0", + }, + { + name: "Месяц", + type: "duration", + description: "30 дней безлимитного пользования сервисом", + actualPrice: "969", + oldPrice: "1024", + discount: "5", + }, + { + name: "3 месяца", + type: "duration", + description: "90 дней безлимитного пользования сервисом", + actualPrice: " 2 325,6", + oldPrice: "3060", + discount: "24", + }, + { + name: "Год", + type: "duration", + description: "365 дней безлимитного пользования сервисом", + actualPrice: " 7 501,84", + oldPrice: "12 410", + discount: "40", + }, + { + name: "3 года", + type: "duration", + description: "1095 дней безлимитного пользования сервисом", + actualPrice: "16 939,65", + oldPrice: "37 230", + discount: "55", + }, + { + name: "100 Заявок", + type: "requests", + description: "Полное прохождение 100 опросов респондентом", + actualPrice: "1 900", + oldPrice: "2 000", + discount: "5", + }, + { + name: "1 000 Заявок", + type: "requests", + description: "Полное прохождение 1000 опросов респондентом", + actualPrice: "12 740", + oldPrice: "2 000", + discount: "36", + }, + { + name: "10 000 Заявок", + type: "requests", + description: "Полное прохождение 10000 опросов респондентом", + actualPrice: "89 000", + oldPrice: "200 000", + discount: "56", + }, + ], + [] + ); + + return ( + + + + + + + Наши тарифы + + + + + {tariffs.map((tariff) => { + return ( + + + + ); + })} + + + + ); +}; diff --git a/src/pages/Landing/WhatTheFeatures.tsx b/src/pages/Landing/WhatTheFeatures.tsx index 0ddce921..b0d12961 100644 --- a/src/pages/Landing/WhatTheFeatures.tsx +++ b/src/pages/Landing/WhatTheFeatures.tsx @@ -321,7 +321,6 @@ export default function Component() { > Аналитика - (в разработке) - - - - - - - ); -} diff --git a/src/pages/Landing/images/icons/IconNumber3.tsx b/src/pages/Landing/images/icons/IconNumber3.tsx deleted file mode 100644 index afc7a355..00000000 --- a/src/pages/Landing/images/icons/IconNumber3.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { Box } from "@mui/material"; - -interface Props { - color?: string; -} - -export default function OneIconBorder({ color }: Props) { - return ( - - - - - - - - ); -} diff --git a/src/pages/Landing/images/icons/IconNumber4.tsx b/src/pages/Landing/images/icons/IconNumber4.tsx deleted file mode 100644 index d8cef30e..00000000 --- a/src/pages/Landing/images/icons/IconNumber4.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { Box } from "@mui/material"; - -interface Props { - color?: string; -} - -export default function OneIconBorder({ color }: Props) { - return ( - - - - - - - - - ); -} diff --git a/src/pages/Questions/AnswerDraggableList/AnswerItem.tsx b/src/pages/Questions/AnswerDraggableList/AnswerItem.tsx index 40794d5f..1b22b6aa 100644 --- a/src/pages/Questions/AnswerDraggableList/AnswerItem.tsx +++ b/src/pages/Questions/AnswerDraggableList/AnswerItem.tsx @@ -10,21 +10,11 @@ import { useMediaQuery, useTheme, } from "@mui/material"; -import { - addQuestionVariant, - deleteQuestionVariant, - setQuestionVariantField, -} from "@root/questions/actions"; +import { addQuestionVariant, deleteQuestionVariant, setQuestionVariantField } from "@root/questions/actions"; import { enqueueSnackbar } from "notistack"; -import { - memo, - type ChangeEvent, - type FC, - type KeyboardEvent, - type ReactNode, -} from "react"; +import { memo, type ChangeEvent, type FC, type KeyboardEvent, type ReactNode } from "react"; import { Draggable } from "react-beautiful-dnd"; -import type { QuestionVariant } from "../../../model/questionTypes/shared"; +import type { QuestionVariant } from "@frontend/squzanswerer"; const TextField = MuiTextField as unknown as FC; @@ -39,22 +29,20 @@ type AnswerItemProps = { }; const AnswerItem = memo( - ({ - index, - variant, - questionId, - largeCheck = false, - additionalContent, - additionalMobile, - disableKeyDown, - }) => { + ({ index, variant, questionId, largeCheck = false, additionalContent, additionalMobile, disableKeyDown }) => { const theme = useTheme(); const isTablet = useMediaQuery(theme.breakpoints.down(790)); return ( - + {(provided) => ( - + ( multiline={largeCheck} onChange={({ target }: ChangeEvent) => { if (target.value.length <= 1000) { - setQuestionVariantField( - questionId, - variant.id, - "answer", - target.value || " ", - ); + setQuestionVariantField(questionId, variant.id, "answer", target.value || " "); } }} onKeyDown={(event: KeyboardEvent) => { @@ -96,9 +79,7 @@ const AnswerItem = memo( {...provided.dragHandleProps} position="start" > - + {additionalContent} @@ -107,9 +88,7 @@ const AnswerItem = memo( - deleteQuestionVariant(questionId, variant.id) - } + onClick={() => deleteQuestionVariant(questionId, variant.id)} > ( )} ); - }, + } ); AnswerItem.displayName = "AnswerItem"; diff --git a/src/pages/Questions/BranchingMap/CsComponent.tsx b/src/pages/Questions/BranchingMap/CsComponent.tsx index a9b45764..5adc11db 100644 --- a/src/pages/Questions/BranchingMap/CsComponent.tsx +++ b/src/pages/Questions/BranchingMap/CsComponent.tsx @@ -1,6 +1,6 @@ import { devlog } from "@frontend/kitui"; -import { AnyTypedQuizQuestion } from "@model/questionTypes/shared"; -import { Box, Button } from "@mui/material"; +import { AnyTypedQuizQuestion } from "@frontend/squzanswerer"; +import { Box, useMediaQuery, useTheme } from "@mui/material"; import { clearRuleForAll } from "@root/questions/actions"; import { useQuestionsStore } from "@root/questions/store"; import { updateRootContentId } from "@root/quizes/actions"; @@ -9,42 +9,38 @@ import { cleardragQuestionContentId, setModalQuestionParentContentId, setModalQuestionTargetContentId, - updateModalInfoWhyCantCreate, updateOpenedModalSettingsId, } from "@root/uiTools/actions"; import { useUiTools } from "@root/uiTools/store"; -import { ProblemIcon } from "@ui_kit/ProblemIcon"; import type { Core } from "cytoscape"; import { enqueueSnackbar } from "notistack"; -import { useEffect, useLayoutEffect, useMemo, useRef } from "react"; +import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import CytoscapeComponent from "react-cytoscapejs"; import { withErrorBoundary } from "react-error-boundary"; import { DeleteNodeModal } from "../DeleteNodeModal"; import CsNodeButtons from "./CsNodeButtons"; +import { InfoBanner } from "./InfoBanner/InfoBanner"; +import { PositionControl } from "./PositionControl/PositionControl"; +import { ZoomControl } from "./ZoomControl/ZoomControl"; import { addNode, layoutOptions, storeToNodes } from "./helper"; import { useRemoveNode } from "./hooks/useRemoveNode"; import "./style/styles.css"; import { stylesheet } from "./style/stylesheet"; function CsComponent() { - const desireToOpenABranchingModal = useUiTools( - (state) => state.desireToOpenABranchingModal, - ); - const canCreatePublic = useUiTools((state) => state.canCreatePublic); - const modalQuestionParentContentId = useUiTools( - (state) => state.modalQuestionParentContentId, - ); - const modalQuestionTargetContentId = useUiTools( - (state) => state.modalQuestionTargetContentId, - ); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down(650)); + const [isBannerVisible, setBannerVisible] = useState(true); + const desireToOpenABranchingModal = useUiTools((state) => state.desireToOpenABranchingModal); + const modalQuestionParentContentId = useUiTools((state) => state.modalQuestionParentContentId); + const modalQuestionTargetContentId = useUiTools((state) => state.modalQuestionTargetContentId); const trashQuestions = useQuestionsStore((state) => state.questions); const cyRef = useRef(null); const { removeNode } = useRemoveNode({ cyRef }); const csElements = useMemo(() => { const questions = trashQuestions.filter( - (question): question is AnyTypedQuizQuestion => - question.type !== null && question.type !== "result", + (question): question is AnyTypedQuizQuestion => question.type !== null && question.type !== "result" ); return storeToNodes(questions); @@ -54,9 +50,7 @@ function CsComponent() { const cy = cyRef?.current; if (desireToOpenABranchingModal) { setTimeout(() => { - cy - ?.getElementById(desireToOpenABranchingModal) - ?.data("eroticeyeblink", true); + cy?.getElementById(desireToOpenABranchingModal)?.data("eroticeyeblink", true); }, 250); } else { cy?.elements().data("eroticeyeblink", false); @@ -64,10 +58,7 @@ function CsComponent() { }, [desireToOpenABranchingModal]); useEffect(() => { - if ( - modalQuestionTargetContentId.length !== 0 && - modalQuestionParentContentId.length !== 0 - ) { + if (modalQuestionTargetContentId.length !== 0 && modalQuestionParentContentId.length !== 0) { if (!cyRef.current) return; addNode({ @@ -93,47 +84,45 @@ function CsComponent() { cyRef.current?.layout(layoutOptions).run(); cyRef.current?.fit(undefined, 70); }, - [csElements], + [csElements] ); return ( - <> - - - - - { - cyRef.current = cy; - }} - autoungrabify={true} - autounselectify={true} - boxSelectionEnabled={false} + + + + {isBannerVisible && } + + + { + cyRef.current = cy; + }} + autoungrabify={true} + autounselectify={true} + boxSelectionEnabled={false} + /> + - + ); } diff --git a/src/pages/Questions/BranchingMap/CsNodeButtons.tsx b/src/pages/Questions/BranchingMap/CsNodeButtons.tsx index a01b3d23..75d0dc4b 100644 --- a/src/pages/Questions/BranchingMap/CsNodeButtons.tsx +++ b/src/pages/Questions/BranchingMap/CsNodeButtons.tsx @@ -9,15 +9,15 @@ import { } from "@root/uiTools/actions"; import { Core, EventObject, NodeSingular } from "cytoscape"; import { - MutableRefObject, forwardRef, + MutableRefObject, useEffect, useMemo, useRef, } from "react"; import { createPortal } from "react-dom"; import { - addNode, + canAddChildToQuestion, isElementANode, isNodeInViewport, isQuestionProhibited, @@ -56,6 +56,7 @@ export default function CsNodeButtons({ csElements, cyRef }: Props) { left: 0, width: "100%", height: "100%", + borderRadius: "12px", }} > {nodeElements.flatMap((csElement) => [ @@ -75,9 +76,9 @@ export default function CsNodeButtons({ csElements, cyRef }: Props) { const buttonData = buttonRefsById.current[csElement.data.id]; if (buttonData) buttonData.add = r; }} - onPointerUp={() => { - addNode({ parentNodeContentId: csElement.data.id }); - cleardragQuestionContentId(); + onClick={() => { + setModalQuestionParentContentId(csElement.data.id); + setOpenedModalQuestions(canAddChildToQuestion(csElement.data.id)); }} />, !csElement.data.isRoot && @@ -102,12 +103,7 @@ export default function CsNodeButtons({ csElements, cyRef }: Props) { }} onClick={() => { setModalQuestionParentContentId(csElement.data.id); - setOpenedModalQuestions( - !( - isQuestionProhibited(csElement.data.type) && - csElement.data.children > 0 - ), - ); + setOpenedModalQuestions(canAddChildToQuestion(csElement.data.id)); }} />, ])} @@ -181,7 +177,8 @@ const applyButtonStyleByType: Record< }, add(button, node, zoom) { const nodePosition = node.renderedPosition(); - const shiftX = node.renderedWidth() / 2 + (ADD_BUTTON_WIDTH / 2) * zoom; + const shiftX = + node.renderedWidth() / 2 + (ADD_BUTTON_WIDTH / 2 - 10) * zoom; if (!isNodeInViewport(node, 100)) { return button.style.setProperty("display", "none"); @@ -268,9 +265,9 @@ const ADD_BUTTON_HEIGHT = ADD_BUTTON_WIDTH; const CsAddButton = forwardRef< HTMLButtonElement, { - onPointerUp: () => void; + onClick: () => void; } ->(({ onPointerUp }, ref) => ( +>(({ onClick }, ref) => ( event.stopPropagation()} onTouchStartCapture={(event) => event.stopPropagation()} > @@ -295,8 +292,8 @@ const CsAddButton = forwardRef< )); -const SETTINGS_BUTTON_WIDTH = 70; -const SETTINGS_BUTTON_HEIGHT = 60; +const SETTINGS_BUTTON_WIDTH = 50; +const SETTINGS_BUTTON_HEIGHT = 40; const CsSettingsButton = forwardRef< HTMLButtonElement, @@ -364,7 +361,8 @@ const CsSelectButton = forwardRef< width: SELECT_BUTTON_WIDTH, height: SELECT_BUTTON_HEIGHT, backgroundColor: "rgb(0 0 0 / 0)", - borderRadius: "8px", + border: "1px solid #9A9AAF", + borderRadius: "6px", p: 0, zIndex: 0, }} diff --git a/src/pages/Questions/BranchingMap/FirstNodeField.tsx b/src/pages/Questions/BranchingMap/FirstNodeField.tsx index 0fd2c8e2..083c4ab2 100644 --- a/src/pages/Questions/BranchingMap/FirstNodeField.tsx +++ b/src/pages/Questions/BranchingMap/FirstNodeField.tsx @@ -1,4 +1,10 @@ -import { Box } from "@mui/material"; +import { + Box, + Button, + Typography, + useMediaQuery, + useTheme, +} from "@mui/material"; import { clearRuleForAll, createResult, @@ -14,8 +20,11 @@ import { import { useUiTools } from "@root/uiTools/store"; import { enqueueSnackbar } from "notistack"; import { useEffect, useLayoutEffect, useRef } from "react"; +import { GrayPlus } from "@icons/questionsPage/GrayPlus"; export const FirstNodeField = () => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down(650)); const quiz = useCurrentQuiz(); const modalQuestionTargetContentId = useUiTools( (state) => state.modalQuestionTargetContentId, @@ -80,17 +89,49 @@ export const FirstNodeField = () => { - + + + + Добавьте созданные вопросы и настройте связи между ними + + + ); }; diff --git a/src/pages/Questions/BranchingMap/InfoBanner/InfoBanner.tsx b/src/pages/Questions/BranchingMap/InfoBanner/InfoBanner.tsx new file mode 100644 index 00000000..44c7d4e4 --- /dev/null +++ b/src/pages/Questions/BranchingMap/InfoBanner/InfoBanner.tsx @@ -0,0 +1,99 @@ +import { + Box, + IconButton, + Typography, + useMediaQuery, + useTheme, +} from "@mui/material"; +import { Add } from "@mui/icons-material"; +import { EditIcon } from "@icons/questionsPage/EditIcon"; +import CloseIcon from "@mui/icons-material/Close"; +import { Dispatch, FC, SetStateAction } from "react"; + +type InfoBannerProps = { + setBannerVisible: Dispatch>; +}; + +export const InfoBanner: FC = ({ setBannerVisible }) => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down(650)); + return ( + + + + Добавьте больше вопросов кнопкой + + + + + + + Настраивайте условия их отображения в квизе с помощью + + + + + setBannerVisible(false)} + > + + + + ); +}; diff --git a/src/pages/Questions/BranchingMap/PositionControl/PositionControl.tsx b/src/pages/Questions/BranchingMap/PositionControl/PositionControl.tsx new file mode 100644 index 00000000..c49d1f09 --- /dev/null +++ b/src/pages/Questions/BranchingMap/PositionControl/PositionControl.tsx @@ -0,0 +1,64 @@ +import { Box, Button, useMediaQuery, useTheme } from "@mui/material"; +import { ExpandIcon } from "@icons/questionsPage/ExpandIcon"; +import { AlignIcon } from "@icons/questionsPage/AlignIcon"; +import { FC, MutableRefObject } from "react"; +import { Core } from "cytoscape"; + +type PositionControlProps = { + cyRef: MutableRefObject; +}; + +export const PositionControl: FC = ({ cyRef }) => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down(650)); + + return ( + + + + + ); +}; diff --git a/src/pages/Questions/BranchingMap/ZoomControl/ZoomControl.tsx b/src/pages/Questions/BranchingMap/ZoomControl/ZoomControl.tsx new file mode 100644 index 00000000..0450d483 --- /dev/null +++ b/src/pages/Questions/BranchingMap/ZoomControl/ZoomControl.tsx @@ -0,0 +1,72 @@ +import { Box, Button, useMediaQuery, useTheme } from "@mui/material"; +import { FC, MutableRefObject } from "react"; +import AddIcon from "@mui/icons-material/Add"; +import RemoveIcon from "@mui/icons-material/Remove"; +import { Core } from "cytoscape"; + +type PositionControlProps = { + cyRef: MutableRefObject; +}; + +export const ZoomControl: FC = ({ cyRef }) => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down(650)); + + return ( + + + + + ); +}; diff --git a/src/pages/Questions/BranchingMap/helper.ts b/src/pages/Questions/BranchingMap/helper.ts index de8f5ba1..cf3d53e1 100644 --- a/src/pages/Questions/BranchingMap/helper.ts +++ b/src/pages/Questions/BranchingMap/helper.ts @@ -1,16 +1,7 @@ -import { QuestionType } from "@model/question/question"; -import { QuizQuestionResult } from "@model/questionTypes/result"; -import { - AnyTypedQuizQuestion, - QuestionBranchingRule, - QuestionBranchingRuleMain, - UntypedQuizQuestion, -} from "@model/questionTypes/shared"; -import { - createResult, - getQuestionByContentId, - updateQuestion, -} from "@root/questions/actions"; +import { AnyTypedQuizQuestion, QuestionBranchingRule, QuestionBranchingRuleMain } from "@frontend/squzanswerer"; +import { QuizQuestionResult } from "@frontend/squzanswerer"; +import { UntypedQuizQuestion } from "@model/questionTypes/shared"; +import { createResult, getQuestionByContentId, updateQuestion } from "@root/questions/actions"; import { useQuestionsStore } from "@root/questions/store"; import { useQuizStore } from "@root/quizes/store"; import { updateOpenedModalSettingsId } from "@root/uiTools/actions"; @@ -62,21 +53,16 @@ export const storeToNodes = (questions: AnyTypedQuizQuestion[]) => { const parentQuestion = { ...getQuestionByContentId(question.content.rule.parentId), } as AnyTypedQuizQuestion; - let label = - question.title === "" || question.title === " " - ? "noname" - : question.title; - if (label.length > 10) label = label.slice(0, 10).toLowerCase() + "…"; + let label = question.title === "" || question.title === " " ? "noname" : question.title; + label = label.trim().replace(/\s+/g, " "); + if (label.length > 20) label = label.slice(0, 20).toLowerCase() + "…"; nodes.push({ data: { isRoot: question.content.rule.parentId === "root", id: question.content.id, label, - qtype: - question.content.rule.parentId === "root" - ? "root" - : parentQuestion.type, + qtype: question.content.rule.parentId === "root" ? "root" : parentQuestion.type, type: question.type, children: question.content.rule.children.length, }, @@ -130,15 +116,10 @@ export function clearDataAfterAddNode({ //смотрим не добавлен ли родителю result. Если да - делаем его неактивным. Веточкам result не нужен useQuestionsStore .getState() - .questions.filter( - (question): question is QuizQuestionResult => question.type === "result", - ) + .questions.filter((question): question is QuizQuestionResult => question.type === "result") .forEach((targetQuestion) => { if (targetQuestion.content.rule.parentId === parentQuestion.content.id) { - updateQuestion( - targetQuestion.id, - (q) => (q.content.usage = false), - ); + updateQuestion(targetQuestion.id, (q) => (q.content.usage = false)); } }); @@ -156,14 +137,9 @@ export function clearDataAfterAddNode({ //предупреждаем родителя о новом потомке (если он ещё не знает о нём) if (!parentQuestion.content.rule.children.includes(targetQuestion.content.id)) updateQuestion(parentNodeContentId, (question) => { - question.content.rule.children = [ - ...question.content.rule.children, - targetQuestion.content.id, - ]; + question.content.rule.children = [...question.content.rule.children, targetQuestion.content.id]; //единственному ребёнку даём дефолт по-умолчанию - question.content.rule.default = noChild - ? targetQuestion.content.id - : question.content.rule.default; + question.content.rule.default = noChild ? targetQuestion.content.id : question.content.rule.default; }); if (!noChild) { @@ -194,9 +170,7 @@ export function clearDataAfterRemoveNode({ //Делаем результат родителя активным const parentResult = trashQuestions.find( - (q): q is QuizQuestionResult => - q.type === "result" && - q.content.rule.parentId === parentQuestionContentId, + (q): q is QuizQuestionResult => q.type === "result" && q.content.rule.parentId === parentQuestionContentId ); if (parentResult) { updateQuestion(parentResult.content.id, (q) => { @@ -212,21 +186,14 @@ export function clearDataAfterRemoveNode({ } const newChildren = [...parentQuestion.content.rule.children]; - newChildren.splice( - parentQuestion.content.rule.children.indexOf(targetQuestionContentId), - 1, - ); + newChildren.splice(parentQuestion.content.rule.children.indexOf(targetQuestionContentId), 1); const newRule: QuestionBranchingRule = { children: newChildren, - default: - parentQuestion.content.rule.default === targetQuestionContentId - ? "" - : parentQuestion.content.rule.default, + default: parentQuestion.content.rule.default === targetQuestionContentId ? "" : parentQuestion.content.rule.default, //удаляем условия перехода от родителя к этому вопросу, main: parentQuestion.content.rule.main.filter( - (data: QuestionBranchingRuleMain) => - data.next !== targetQuestionContentId, + (data: QuestionBranchingRuleMain) => data.next !== targetQuestionContentId ), parentId: parentQuestion.content.rule.parentId, }; @@ -243,8 +210,7 @@ export function calcNodePosition(node: any) { node.removeData("lastChild"); if (incomming.length === 0) { - if (node.cy().data("firstNode") === undefined) - node.cy().data("firstNode", "root"); + if (node.cy().data("firstNode") === undefined) node.cy().data("firstNode", "root"); node.data("root", true); const children = node.cy().edges(`[source="${id}"]`).targets(); node.data("layer", layer); @@ -257,15 +223,10 @@ export function calcNodePosition(node: any) { const task = queue.pop(); task.task.data("layer", task.layer); task.task.removeData("subtreeWidth"); - const children = node - .cy() - .edges(`[source="${task.task.id()}"]`) - .targets(); + const children = node.cy().edges(`[source="${task.task.id()}"]`).targets(); task.task.data("children", children.length); if (children.length !== 0) { - children.forEach((n: any) => - queue.push({ task: n, layer: task.layer + 1 }), - ); + children.forEach((n: any) => queue.push({ task: n, layer: task.layer + 1 })); } } queue.push({ parent: node, children: children }); @@ -291,7 +252,7 @@ export function calcNodePosition(node: any) { task?.parent.data( "subtreeWidth", - task.children.reduce((p: any, n: any) => p + n.data("subtreeWidth"), 0), + task.children.reduce((p: any, n: any) => p + n.data("subtreeWidth"), 0) ); } @@ -307,7 +268,7 @@ export function calcNodePosition(node: any) { const width = n.data("subtreeWidth"); n.data("oldPos", { - x: 250 * n.data("layer"), + x: 350 * n.data("layer"), y: yoffset + width / 2, }); yoffset += width; @@ -326,6 +287,21 @@ export function calcNodePosition(node: any) { } } +export const canAddChildToQuestion = (parentNodeContentId: string) => { + const parentQuestion = { + ...getQuestionByContentId(parentNodeContentId), + } as AnyTypedQuizQuestion; + if ( + parentQuestion.type !== undefined && + isQuestionProhibited(parentQuestion.type) && + parentQuestion.content.rule.children.length > 0 + ) { + enqueueSnackbar("У вопроса этого типа может быть только 1 потомок"); + return false; + } + return true; +}; + export const addNode = ({ parentNodeContentId, targetNodeContentId, @@ -351,23 +327,16 @@ export const addNode = ({ //если есть инфо о выбранном вопросе из модалки - берём родителя из инфо модалки. Иначе из значения дропа const targetQuestion = { - ...getQuestionByContentId( - targetNodeContentId || useUiTools.getState().dragQuestionContentId, - ), + ...getQuestionByContentId(targetNodeContentId || useUiTools.getState().dragQuestionContentId), } as AnyTypedQuizQuestion; if (Object.keys(targetQuestion).length !== 0 && parentNodeContentId) { clearDataAfterAddNode({ parentNodeContentId, targetQuestion }); createResult(useQuizStore.getState().editQuizId, targetQuestion.content.id); } else { - enqueueSnackbar( - "Добавляемый вопрос не найден. Перетащите вопрос из списка", - ); + enqueueSnackbar("Добавляемый вопрос не найден. Перетащите вопрос из списка"); } }; export const isQuestionProhibited = (parentQType: string) => - parentQType === "text" || - parentQType === "date" || - parentQType === "number" || - parentQType === "page"; + parentQType === "text" || parentQType === "date" || parentQType === "number" || parentQType === "page"; diff --git a/src/pages/Questions/BranchingMap/hooks/useRemoveNode.ts b/src/pages/Questions/BranchingMap/hooks/useRemoveNode.ts index 697ae87f..c2071fba 100644 --- a/src/pages/Questions/BranchingMap/hooks/useRemoveNode.ts +++ b/src/pages/Questions/BranchingMap/hooks/useRemoveNode.ts @@ -1,18 +1,10 @@ import { devlog } from "@frontend/kitui"; -import { QuizQuestionResult } from "@model/questionTypes/result"; -import { - clearRuleForAll, - getQuestionByContentId, - updateQuestion, -} from "@root/questions/actions"; +import { QuizQuestionResult } from "@frontend/squzanswerer"; +import { clearRuleForAll, getQuestionByContentId, updateQuestion } from "@root/questions/actions"; import { useQuestionsStore } from "@root/questions/store"; import { updateRootContentId } from "@root/quizes/actions"; import { useCurrentQuiz } from "@root/quizes/hooks"; -import type { - CollectionReturnValue, - Core, - SingularElementArgument, -} from "cytoscape"; +import type { CollectionReturnValue, Core } from "cytoscape"; import type { MutableRefObject } from "react"; import { clearDataAfterRemoveNode } from "../helper"; @@ -52,11 +44,7 @@ export const useRemoveNode = ({ cyRef }: UseRemoveNodeArgs) => { const targetQuestion = getQuestionByContentId(targetNodeContentId); - if ( - targetQuestion?.type && - targetQuestion.content.rule.parentId === "root" && - quiz - ) { + if (targetQuestion?.type && targetQuestion.content.rule.parentId === "root" && quiz) { updateRootContentId(quiz.id, ""); updateQuestion(targetNodeContentId, (question) => { question.content.rule.parentId = ""; @@ -71,10 +59,7 @@ export const useRemoveNode = ({ cyRef }: UseRemoveNodeArgs) => { ?.toArray()?.[0] ?.data()?.source; if (targetNodeContentId && parentQuestionContentId) { - if ( - quiz && - cy?.edges(`[source="${parentQuestionContentId}"]`).length === 0 - ) { + if (quiz && cy?.edges(`[source="${parentQuestionContentId}"]`).length === 0) { devlog(parentQuestionContentId); //createFrontResult(quiz.backendId, parentQuestionContentId); } @@ -103,8 +88,7 @@ export const useRemoveNode = ({ cyRef }: UseRemoveNodeArgs) => { if ( qr.type === "result" && (deleteNodes.includes(qr.content.rule.parentId || "") || - (targetQuestion?.type && - qr.content.rule.parentId === targetQuestion.content.id)) + (targetQuestion?.type && qr.content.rule.parentId === targetQuestion.content.id)) ) { updateQuestion(qr.content.id, (q) => { q.content.usage = false; diff --git a/src/pages/Questions/BranchingMap/index.tsx b/src/pages/Questions/BranchingMap/index.tsx index 2cd57941..bce067a9 100644 --- a/src/pages/Questions/BranchingMap/index.tsx +++ b/src/pages/Questions/BranchingMap/index.tsx @@ -1,4 +1,4 @@ -import { Box } from "@mui/material"; +import { Box, useMediaQuery, useTheme } from "@mui/material"; import { useCurrentQuiz } from "@root/quizes/hooks"; import { useUiTools } from "@root/uiTools/store"; import { BranchingQuestionsModal } from "../BranchingQuestionsModal"; @@ -6,23 +6,24 @@ import CsComponent from "./CsComponent"; import { FirstNodeField } from "./FirstNodeField"; export const BranchingMap = () => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down(650)); const quiz = useCurrentQuiz(); - const dragQuestionContentId = useUiTools( - (state) => state.dragQuestionContentId, - ); + const dragQuestionContentId = useUiTools((state) => state.dragQuestionContentId); return ( {quiz?.config.haveRoot ? : } diff --git a/src/pages/Questions/BranchingMap/style/stylesheet.ts b/src/pages/Questions/BranchingMap/style/stylesheet.ts index 6fc23eb9..513235f0 100644 --- a/src/pages/Questions/BranchingMap/style/stylesheet.ts +++ b/src/pages/Questions/BranchingMap/style/stylesheet.ts @@ -9,13 +9,14 @@ export const stylesheet: Stylesheet[] = [ height: 130, backgroundColor: "#FFFFFF", label: "data(label)", - "font-size": "16", + "font-size": "12", color: "#4D4D4D", - "text-halign": "center", + "text-halign": "right", "text-valign": "center", "text-wrap": "wrap", "text-max-width": "130px", "text-overflow-wrap": "whitespace", + "text-margin-x": -115, }, }, { @@ -40,7 +41,7 @@ export const stylesheet: Stylesheet[] = [ "line-color": "#DEDFE7", "curve-style": "taxi", "taxi-direction": "horizontal", - "taxi-turn": 60, + "taxi-turn": 100, }, }, { diff --git a/src/pages/Questions/BranchingModal/BranchingQuestionsModal.tsx b/src/pages/Questions/BranchingModal/BranchingQuestionsModal.tsx index b07b75d9..834608c5 100644 --- a/src/pages/Questions/BranchingModal/BranchingQuestionsModal.tsx +++ b/src/pages/Questions/BranchingModal/BranchingQuestionsModal.tsx @@ -1,55 +1,38 @@ -import { useState, useRef, useEffect, useLayoutEffect } from "react"; +import { useLayoutEffect, useState } from "react"; import { Box, Button, - FormControl, + Checkbox, FormControlLabel, Link, Modal, - Radio, - RadioGroup, Tooltip, Typography, - useTheme, - Checkbox, useMediaQuery, + useTheme, } from "@mui/material"; -import { - AnyTypedQuizQuestion, - createBranchingRuleMain, -} from "../../../model/questionTypes/shared"; -import { Select } from "../Select"; - -import RadioCheck from "@ui_kit/RadioCheck"; -import RadioIcon from "@ui_kit/RadioIcon"; +import { createBranchingRuleMain } from "../../../model/questionTypes/shared"; import InfoIcon from "@icons/Info"; -import { DeleteIcon } from "@icons/questionsPage/deleteIcon"; -import { TypeSwitch, BlockRule } from "./Settings"; -import { - getQuestionById, - getQuestionByContentId, - updateQuestion, -} from "@root/questions/actions"; +import { TypeSwitch } from "./Settings"; +import { getQuestionByContentId, getQuestionById, updateQuestion } from "@root/questions/actions"; import { updateOpenedModalSettingsId } from "@root/uiTools/actions"; import { useUiTools } from "@root/uiTools/store"; import { enqueueSnackbar } from "notistack"; import TooltipClickInfo from "@ui_kit/Toolbars/TooltipClickInfo"; +import { AnyTypedQuizQuestion } from "@frontend/squzanswerer"; export default function BranchingQuestions() { const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down(650)); const isSmallMobile = useMediaQuery(theme.breakpoints.down(400)); const { openedModalSettingsId } = useUiTools(); - const [targetQuestion, setTargetQuestion] = - useState( - getQuestionById(openedModalSettingsId) || - getQuestionByContentId(openedModalSettingsId), - ); - const [parentQuestion, setParentQuestion] = - useState( - getQuestionByContentId(targetQuestion?.content.rule.parentId), - ); + const [targetQuestion, setTargetQuestion] = useState( + getQuestionById(openedModalSettingsId) || getQuestionByContentId(openedModalSettingsId) + ); + const [parentQuestion, setParentQuestion] = useState( + getQuestionByContentId(targetQuestion?.content.rule.parentId) + ); useLayoutEffect(() => { if (parentQuestion === null) return; @@ -78,10 +61,7 @@ export default function BranchingQuestions() { const saveData = () => { if (parentQuestion !== null) { - updateQuestion( - parentQuestion.content.id, - (question) => (question.content = parentQuestion.content), - ); + updateQuestion(parentQuestion.content.id, (question) => (question.content = parentQuestion.content)); } handleClose(); }; @@ -92,7 +72,10 @@ export default function BranchingQuestions() { return ( <> - + - - - {targetQuestion.title} - - + + {targetQuestion.title} + {isMobile ? ( - + ) : ( - + )} @@ -205,10 +180,7 @@ export default function BranchingQuestions() { onClick={() => { const mutate = JSON.parse(JSON.stringify(parentQuestion)); mutate.content.rule.main.push( - createBranchingRuleMain( - targetQuestion.content.id, - parentQuestion.content.id, - ), + createBranchingRuleMain(targetQuestion.content.id, parentQuestion.content.id) ); setParentQuestion(mutate); }} @@ -222,10 +194,7 @@ export default function BranchingQuestions() { sx={{ margin: 0, }} - checked={ - parentQuestion.content.rule.default === - targetQuestion.content.id - } + checked={parentQuestion.content.rule.default === targetQuestion.content.id} onClick={() => { let mutate = JSON.parse(JSON.stringify(parentQuestion)); @@ -235,8 +204,7 @@ export default function BranchingQuestions() { } else { //Изменять чекбокс можно только если много потомков mutate.content.rule.default = - parentQuestion.content.rule.default === - targetQuestion.content.id + parentQuestion.content.rule.default === targetQuestion.content.id ? "" : targetQuestion.content.id; } diff --git a/src/pages/Questions/BranchingModal/Settings.tsx b/src/pages/Questions/BranchingModal/Settings.tsx index d4fa576b..817f75ba 100644 --- a/src/pages/Questions/BranchingModal/Settings.tsx +++ b/src/pages/Questions/BranchingModal/Settings.tsx @@ -1,40 +1,31 @@ +import CalendarIcon from "@icons/CalendarIcon"; +import { DeleteIcon } from "@icons/questionsPage/deleteIcon"; import { Box, - MenuItem, - FormControl, Checkbox, + FormControl, FormControlLabel, + IconButton, + MenuItem, Radio, RadioGroup, - Typography, - useTheme, Select, - useMediaQuery, - IconButton, TextField, + Typography, + useMediaQuery, + useTheme, } from "@mui/material"; +import { SelectChangeEvent } from "@mui/material/Select"; +import { DatePicker } from "@mui/x-date-pickers"; +import { TimePicker } from "@mui/x-date-pickers/TimePicker"; import RadioCheck from "@ui_kit/RadioCheck"; import RadioIcon from "@ui_kit/RadioIcon"; -import { QuizQuestionBase } from "model/questionTypes/shared"; -import { useState, useRef, useEffect } from "react"; +import moment from "moment"; +import { useEffect, useState } from "react"; import { useParams } from "react-router-dom"; -import { useQuestionsStore } from "@root/questions/store"; -import { updateQuestion, getQuestionById } from "@root/questions/actions"; -import { SelectChangeEvent } from "@mui/material/Select"; -import CalendarIcon from "@icons/CalendarIcon"; -import { DatePicker } from "@mui/x-date-pickers"; -import dayjs from "dayjs"; -import { TimePicker } from "@mui/x-date-pickers/TimePicker"; -import InfoIcon from "@icons/Info"; -import { DeleteIcon } from "@icons/questionsPage/deleteIcon"; +import type { AnyTypedQuizQuestion, QuizQuestionNumber } from "@frontend/squzanswerer"; -import type { AnyTypedQuizQuestion } from "../../../model/questionTypes/shared"; -import type { QuizQuestionNumber } from "../../../model/questionTypes/number"; - -const CONDITIONS = [ - "Все условия обязательны", - "Обязательно хотя бы одно условие", -]; +const CONDITIONS = ["Все условия обязательны", "Обязательно хотя бы одно условие"]; interface Props { parentQuestion: AnyTypedQuizQuestion; targetQuestion: AnyTypedQuizQuestion; @@ -43,12 +34,7 @@ interface Props { } // Этот компонент вызывается 1 раз на каждое условие родителя для перехода к этому вопросу. Поэтому для изменения стора мы знаем индекс -export const TypeSwitch = ({ - parentQuestion, - targetQuestion, - ruleIndex, - setParentQuestion, -}: Props) => { +export const TypeSwitch = ({ parentQuestion, targetQuestion, ruleIndex, setParentQuestion }: Props) => { switch (parentQuestion.type) { case "variant": case "images": @@ -91,9 +77,7 @@ export const TypeSwitch = ({ break; case "page": - return ( - - ); + return ; break; case "text": @@ -147,12 +131,7 @@ export const BlockRule = ({ text }: { text: string }) => { ); }; -const SelectorType = ({ - parentQuestion, - targetQuestion, - ruleIndex, - setParentQuestion, -}: Props) => { +const SelectorType = ({ parentQuestion, targetQuestion, ruleIndex, setParentQuestion }: Props) => { const theme = useTheme(); const quizId = Number(useParams().quizId); return ( @@ -174,9 +153,7 @@ const SelectorType = ({ pb: "5px", }} > - - Новое условие - + Новое условие { @@ -196,23 +173,15 @@ const SelectorType = ({ pb: "10px", }} > - - Дан ответ - - - (Укажите один или несколько вариантов) - + Дан ответ + (Укажите один или несколько вариантов) ; + return ( + + ); case "select": return ( - + ); case "date": - return ; + return ( + + ); case "number": - return ; + return ( + + ); case "file": return ( - + ); case "page": - return ; + return ( + + ); case "rating": return ( - + ); default: return null; diff --git a/src/pages/Questions/OptionsAndPicture/OptionsAndPicture.tsx b/src/pages/Questions/OptionsAndPicture/OptionsAndPicture.tsx index 314947fa..3d5516a8 100644 --- a/src/pages/Questions/OptionsAndPicture/OptionsAndPicture.tsx +++ b/src/pages/Questions/OptionsAndPicture/OptionsAndPicture.tsx @@ -1,14 +1,10 @@ import { Box, Link, Typography, useMediaQuery, useTheme } from "@mui/material"; -import { - addQuestionVariant, - clearQuestionImages, - uploadQuestionImage, -} from "@root/questions/actions"; +import { addQuestionVariant, clearQuestionImages, uploadQuestionImage } from "@root/questions/actions"; import { useCurrentQuiz } from "@root/quizes/hooks"; import { CropModal, useCropModalState } from "@ui_kit/Modal/CropModal"; import { useEffect, useState } from "react"; import { EnterIcon } from "../../../assets/icons/questionsPage/enterIcon"; -import type { QuizQuestionVarImg } from "../../../model/questionTypes/varimg"; +import type { QuizQuestionVarImg } from "@frontend/squzanswerer"; import { useDisclosure } from "../../../utils/useDisclosure"; import { AnswerDraggableList } from "../AnswerDraggableList"; import ImageEditAnswerItem from "../AnswerDraggableList/ImageEditAnswerItem"; @@ -22,50 +18,31 @@ interface Props { setOpenBranchingPage: (a: boolean) => void; } -export default function OptionsAndPicture({ - question, - setOpenBranchingPage, -}: Props) { +export default function OptionsAndPicture({ question, setOpenBranchingPage }: Props) { const [switchState, setSwitchState] = useState("setting"); const [pictureUploding, setPictureUploading] = useState(false); - const [selectedVariantId, setSelectedVariantId] = useState( - null, - ); + const [selectedVariantId, setSelectedVariantId] = useState(null); const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down(790)); const quizQid = useCurrentQuiz()?.qid; - const [isImageUploadOpen, openImageUploadModal, closeImageUploadModal] = - useDisclosure(); - const { - isCropModalOpen, - openCropModal, - closeCropModal, - imageBlob, - originalImageUrl, - setCropModalImageBlob, - } = useCropModalState(); + const [isImageUploadOpen, openImageUploadModal, closeImageUploadModal] = useDisclosure(); + const { isCropModalOpen, openCropModal, closeCropModal, imageBlob, originalImageUrl, setCropModalImageBlob } = + useCropModalState(); const handleImageUpload = async (file: File) => { if (!selectedVariantId) return; setPictureUploading(true); - const url = await uploadQuestionImage( - question.id, - quizQid, - file, - (question, url) => { - if (!("variants" in question.content)) return; + const url = await uploadQuestionImage(question.id, quizQid, file, (question, url) => { + if (!("variants" in question.content)) return; - const variant = question.content.variants.find( - (variant) => variant.id === selectedVariantId, - ); - if (!variant) return; + const variant = question.content.variants.find((variant) => variant.id === selectedVariantId); + if (!variant) return; - variant.extendedText = url; - variant.originalImageUrl = url; - }, - ); + variant.extendedText = url; + variant.originalImageUrl = url; + }); closeImageUploadModal(); openCropModal(file, url); @@ -78,9 +55,7 @@ export default function OptionsAndPicture({ uploadQuestionImage(question.id, quizQid, imageBlob, (question, url) => { if (!("variants" in question.content)) return; - const variant = question.content.variants.find( - (variant) => variant.id === selectedVariantId, - ); + const variant = question.content.variants.find((variant) => variant.id === selectedVariantId); if (!variant) return; variant.extendedText = url; @@ -127,10 +102,9 @@ export default function OptionsAndPicture({ onClose={closeCropModal} onSaveImageClick={handleCropModalSaveClick} onDeleteClick={() => { - if (selectedVariantId) - clearQuestionImages(question.id, selectedVariantId); + if (selectedVariantId) clearQuestionImages(question.id, selectedVariantId); }} - cropAspectRatio={{ width: 452, height: 300 }} + cropAspectRatio={{ width: 300, height: 300 }} /> - + ); } diff --git a/src/pages/Questions/OptionsAndPicture/SettingOptionsAndPict.tsx b/src/pages/Questions/OptionsAndPicture/SettingOptionsAndPict.tsx index d910360f..f7584458 100644 --- a/src/pages/Questions/OptionsAndPicture/SettingOptionsAndPict.tsx +++ b/src/pages/Questions/OptionsAndPicture/SettingOptionsAndPict.tsx @@ -1,24 +1,22 @@ +import type { QuizQuestionVarImg, QuizQuestionVariant } from "@frontend/squzanswerer"; import { Box, Typography, useMediaQuery, useTheme } from "@mui/material"; import { updateQuestion } from "@root/questions/actions"; import CustomCheckbox from "@ui_kit/CustomCheckbox"; import CustomTextField from "@ui_kit/CustomTextField"; -import type { QuizQuestionVarImg } from "../../../model/questionTypes/varimg"; import { memo } from "react"; type SettingOptionsAndPictProps = { questionId: string; replText: string; isRequired: boolean; + isOwn: boolean; }; -const SettingOptionsAndPict = memo(function ({ - questionId, - replText, - isRequired, -}) { +const SettingOptionsAndPict = memo(function ({ questionId, replText, isRequired, isOwn }) { const theme = useTheme(); const isWrappColumn = useMediaQuery(theme.breakpoints.down(980)); const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990)); + const isTablet = useMediaQuery(theme.breakpoints.down(985)); const isMobile = useMediaQuery(theme.breakpoints.down(680)); const setReplText = (replText: string) => { @@ -36,13 +34,14 @@ const SettingOptionsAndPict = memo(function ({ display: "flex", justifyContent: "space-between", flexDirection: isWrappColumn ? "column" : "none", + pb: "20px", + pl: "20px", + pt: isTablet ? "5px" : "0px", }} > (function ({ width: "100%", }} > - (function ({ }} > Настройки ответов - + */} + {/* { + updateQuestion(questionId, (question) => { + question.content.own = target.checked; + }); + }} + /> */} {!isWrappColumn && ( (function ({ (function ({ } /> {isWrappColumn && ( - <> + (function ({ maxLength={60} onChange={({ target }) => setReplText(target.value)} /> - + )} diff --git a/src/pages/Questions/OptionsAndPicture/switchOptionsAndPict.tsx b/src/pages/Questions/OptionsAndPicture/switchOptionsAndPict.tsx index 018fb0a9..4b2e40e1 100644 --- a/src/pages/Questions/OptionsAndPicture/switchOptionsAndPict.tsx +++ b/src/pages/Questions/OptionsAndPicture/switchOptionsAndPict.tsx @@ -1,4 +1,4 @@ -import { QuizQuestionVarImg } from "@model/questionTypes/varimg"; +import { QuizQuestionVarImg } from "@frontend/squzanswerer"; import UploadImage from "../UploadImage"; import HelpQuestions from "../helpQuestions"; import SettingOptionsAndPict from "./SettingOptionsAndPict"; @@ -8,10 +8,7 @@ interface Props { question: QuizQuestionVarImg; } -export default function SwitchOptionsAndPict({ - switchState = "setting", - question, -}: Props) { +export default function SwitchOptionsAndPict({ switchState = "setting", question }: Props) { switch (switchState) { case "setting": return ( @@ -19,6 +16,7 @@ export default function SwitchOptionsAndPict({ questionId={question.id} replText={question.content.replText} isRequired={question.content.required} + isOwn={question.content.own} /> ); case "help": diff --git a/src/pages/Questions/OptionsPicture/OptionsPicture.tsx b/src/pages/Questions/OptionsPicture/OptionsPicture.tsx index 1c148795..cef09aea 100644 --- a/src/pages/Questions/OptionsPicture/OptionsPicture.tsx +++ b/src/pages/Questions/OptionsPicture/OptionsPicture.tsx @@ -1,13 +1,10 @@ import { Box, Link, Typography, useMediaQuery, useTheme } from "@mui/material"; -import { - clearQuestionImages, - uploadQuestionImage, -} from "@root/questions/actions"; +import { clearQuestionImages, uploadQuestionImage } from "@root/questions/actions"; import { useCurrentQuiz } from "@root/quizes/hooks"; import { CropModal, useCropModalState } from "@ui_kit/Modal/CropModal"; import { useState } from "react"; import { EnterIcon } from "../../../assets/icons/questionsPage/enterIcon"; -import type { QuizQuestionImages } from "../../../model/questionTypes/images"; +import type { QuizQuestionImages } from "@frontend/squzanswerer"; import { useAddAnswer } from "../../../utils/hooks/useAddAnswer"; import { useDisclosure } from "../../../utils/useDisclosure"; import { AnswerDraggableList } from "../AnswerDraggableList"; @@ -22,52 +19,32 @@ interface Props { setOpenBranchingPage: (a: boolean) => void; } -export default function OptionsPicture({ - question, - openBranchingPage, - setOpenBranchingPage, -}: Props) { +export default function OptionsPicture({ question, openBranchingPage, setOpenBranchingPage }: Props) { const theme = useTheme(); const onClickAddAnAnswer = useAddAnswer(); const quizQid = useCurrentQuiz()?.qid; const [pictureUploding, setPictureUploading] = useState(false); - const [selectedVariantId, setSelectedVariantId] = useState( - null, - ); + const [selectedVariantId, setSelectedVariantId] = useState(null); const [switchState, setSwitchState] = useState("setting"); const isMobile = useMediaQuery(theme.breakpoints.down(790)); - const [isImageUploadOpen, openImageUploadModal, closeImageUploadModal] = - useDisclosure(); - const { - isCropModalOpen, - openCropModal, - closeCropModal, - imageBlob, - originalImageUrl, - setCropModalImageBlob, - } = useCropModalState(); + const [isImageUploadOpen, openImageUploadModal, closeImageUploadModal] = useDisclosure(); + const { isCropModalOpen, openCropModal, closeCropModal, imageBlob, originalImageUrl, setCropModalImageBlob } = + useCropModalState(); const handleImageUpload = async (file: File) => { if (!selectedVariantId) return; setPictureUploading(true); - const url = await uploadQuestionImage( - question.id, - quizQid, - file, - (question, url) => { - if (!("variants" in question.content)) return; + const url = await uploadQuestionImage(question.id, quizQid, file, (question, url) => { + if (!("variants" in question.content)) return; - const variant = question.content.variants.find( - (variant) => variant.id === selectedVariantId, - ); - if (!variant) return; + const variant = question.content.variants.find((variant) => variant.id === selectedVariantId); + if (!variant) return; - variant.extendedText = url; - variant.originalImageUrl = url; - }, - ); + variant.extendedText = url; + variant.originalImageUrl = url; + }); closeImageUploadModal(); openCropModal(file, url); @@ -81,9 +58,7 @@ export default function OptionsPicture({ uploadQuestionImage(question.id, quizQid, imageBlob, (question, url) => { if (!("variants" in question.content)) return; - const variant = question.content.variants.find( - (variant) => variant.id === selectedVariantId, - ); + const variant = question.content.variants.find((variant) => variant.id === selectedVariantId); if (!variant) return; variant.extendedText = url; @@ -124,8 +99,7 @@ export default function OptionsPicture({ onClose={closeCropModal} onSaveImageClick={handleCropModalSaveClick} onDeleteClick={() => { - if (selectedVariantId) - clearQuestionImages(question.id, selectedVariantId); + if (selectedVariantId) clearQuestionImages(question.id, selectedVariantId); }} cropAspectRatio={{ width: 452, height: 300 }} /> @@ -167,10 +141,13 @@ export default function OptionsPicture({ questionId={question.id} questionContentId={question.content.id} questionType={question.type} - questionHasParent={question.content.rule.parentId.length !== 0} + questionHasParent={question.content.rule.parentId?.length !== 0} setOpenBranchingPage={setOpenBranchingPage} /> - + ); } diff --git a/src/pages/Questions/OptionsPicture/settingOpytionsPict.tsx b/src/pages/Questions/OptionsPicture/settingOpytionsPict.tsx index ee1b061a..79477680 100644 --- a/src/pages/Questions/OptionsPicture/settingOpytionsPict.tsx +++ b/src/pages/Questions/OptionsPicture/settingOpytionsPict.tsx @@ -1,19 +1,15 @@ -import { - Box, - Button, - Typography, - useMediaQuery, - useTheme, -} from "@mui/material"; +import type { QuizQuestionImages, QuizQuestionVariant } from "@frontend/squzanswerer"; +import { Box, Button, Typography, useMediaQuery, useTheme } from "@mui/material"; import { updateQuestion } from "@root/questions/actions"; import CustomCheckbox from "@ui_kit/CustomCheckbox"; +import { memo } from "react"; +import FormatIcon1 from "../../../assets/icons/questionsPage/FormatIcon1"; +import FormatIcon2 from "../../../assets/icons/questionsPage/FormatIcon2"; import ProportionsIcon11 from "../../../assets/icons/questionsPage/ProportionsIcon11"; import ProportionsIcon12 from "../../../assets/icons/questionsPage/ProportionsIcon12"; import ProportionsIcon21 from "../../../assets/icons/questionsPage/ProportionsIcon21"; -import type { QuizQuestionImages } from "../../../model/questionTypes/images"; -import { memo } from "react"; -type Proportion = "1:1" | "2:1" | "1:2"; +type Proportion = "1:1" | "1:2" | "2:1"; type ProportionItem = { value: Proportion; @@ -22,18 +18,38 @@ type ProportionItem = { const PROPORTIONS: ProportionItem[] = [ { value: "1:1", icon: ProportionsIcon11 }, - { value: "2:1", icon: ProportionsIcon21 }, - { value: "1:2", icon: ProportionsIcon12 }, + { value: "1:2", icon: ProportionsIcon21 }, + { value: "2:1", icon: ProportionsIcon12 }, +]; + +type Format = "carousel" | "masonry"; + +type FormatItem = { + value: Format; + icon: (props: { color: string }) => JSX.Element; +}; + +const FORMATS: FormatItem[] = [ + { value: "carousel", icon: FormatIcon2 }, + { value: "masonry", icon: FormatIcon1 }, ]; type SettingOpytionsPictProps = { questionId: string; isRequired: boolean; + isMulti: boolean; + isOwn: boolean; + proportions: Proportion; + format: Format; }; const SettingOptionsPict = memo(function ({ questionId, isRequired, + isMulti, + isOwn, + proportions, + format, }) { const theme = useTheme(); const isTablet = useMediaQuery(theme.breakpoints.down(985)); @@ -47,38 +63,167 @@ const SettingOptionsPict = memo(function ({ justifyContent: "space-between", flexDirection: isTablet ? "column" : null, marginRight: isFigmaTablte ? (isMobile ? "0" : "0px") : "30px", + pb: "20px", + pl: "20px", + pt: isTablet ? "5px" : "0px", }} > - - + - Настройки вопросов - - - updateQuestion(questionId, (question) => { - if (question.type !== "images") return; + + Пропорции + + + {PROPORTIONS.map((proportionItem, index) => ( + { + updateQuestion(questionId, (question) => { + if (question.type !== "images") return; + question.content.xy = proportionItem.value; + }); + }} + /> + ))} + + + + + Настройки ответов + + { + updateQuestion(questionId, (question) => { + question.content.multi = target.checked; + }); + }} + /> + { + updateQuestion(questionId, (question) => { + question.content.own = target.checked; + }); + }} + /> + + */} + + {/* + + Формат + + + {FORMATS.map((formatItem, index) => ( + { + updateQuestion(questionId, (question) => { + if (question.type !== "images") return; + question.content.format = formatItem.value; + }); + }} + /> + ))} + + */} + + Настройки вопросов + + updateQuestion(questionId, (question) => { + if (question.type !== "images") return; - question.content.required = !target.checked; - }) - } - /> + question.content.required = !target.checked; + }) + } + /> + ); @@ -101,23 +246,13 @@ export function SelectIconButton({ Icon, isActive = false, onClick }: Props) { - - - setBackgroundTypeModal("linkVideo")} - sx={{ maxWidth: "170px", padding: "10px" }} - > - Ссылка на видео - - setBackgroundTypeModal("ownVideo")} - sx={{ maxWidth: "170px", padding: "10px" }} - > - Загрузить свое - - - {backgroundTypeModal === "linkVideo" ? ( - - - Ссылка на видео - - onUpload(target.value || " ")} - /> - - ) : ( - - - Загрузите видео - - - { - if (target.files?.length) { - onUpload(URL.createObjectURL(target.files[0] || " ")); - } - }} - hidden - accept="video/*" - multiple - type="file" - /> - ) => - event.preventDefault() - } - onDrop={handleDrop} - sx={{ - width: "580px", - padding: "33px 33px 33px 50px", - display: "flex", - alignItems: "center", - backgroundColor: theme.palette.background.default, - border: `1px solid ${theme.palette.grey2.main}`, - borderRadius: "8px", - gap: "50px", - }} - > - - - - Добавить видео - - - Принимает .mp4 и .mov формат — максимум 100мб - - - - - - )} + Готово + - + + setBackgroundTypeModal("linkVideo")} + sx={{ maxWidth: "170px", padding: "10px" }} + > + Ссылка на видео + + setBackgroundTypeModal("ownVideo")} + sx={{ maxWidth: "170px", padding: "10px" }} + > + Загрузить свое + + + {backgroundTypeModal === "linkVideo" ? ( + + Ссылка на видео + onUpload(target.value || " ")} + /> + + ) : ( + + Загрузите видео + + { + if (target.files?.length) { + onUpload(URL.createObjectURL(target.files[0] || " ")); + } + }} + hidden + accept="video/*" + multiple + type="file" + /> + ) => event.preventDefault()} + onDrop={handleDrop} + sx={{ + width: "580px", + padding: "33px 33px 33px 50px", + display: "flex", + alignItems: "center", + backgroundColor: theme.palette.background.default, + border: `1px solid ${theme.palette.grey2.main}`, + borderRadius: "8px", + gap: "50px", + }} + > + + + Добавить видео + Принимает .mp4 и .mov формат — максимум 100мб + + + + + )} + ); -}; +} diff --git a/src/pages/Questions/answerOptions/AnswerOptions.tsx b/src/pages/Questions/answerOptions/AnswerOptions.tsx index 87062da6..dc49b310 100755 --- a/src/pages/Questions/answerOptions/AnswerOptions.tsx +++ b/src/pages/Questions/answerOptions/AnswerOptions.tsx @@ -1,7 +1,7 @@ import { Box, Link, Typography, useMediaQuery, useTheme } from "@mui/material"; import { useEffect, useState } from "react"; import { EnterIcon } from "../../../assets/icons/questionsPage/enterIcon"; -import type { QuizQuestionVariant } from "../../../model/questionTypes/variant"; +import type { QuizQuestionVariant } from "@frontend/squzanswerer"; import { useAddAnswer } from "../../../utils/hooks/useAddAnswer"; import { AnswerDraggableList } from "../AnswerDraggableList"; import AnswerItem from "../AnswerDraggableList/AnswerItem"; @@ -14,11 +14,7 @@ interface Props { setOpenBranchingPage: (a: boolean) => void; } -export default function AnswerOptions({ - question, - openBranchingPage, - setOpenBranchingPage, -}: Props) { +export default function AnswerOptions({ question, openBranchingPage, setOpenBranchingPage }: Props) { const onClickAddAnAnswer = useAddAnswer(); const [switchState, setSwitchState] = useState("setting"); const theme = useTheme(); @@ -109,12 +105,15 @@ export default function AnswerOptions({ SSHC={setSwitchState} questionId={question.id} questionContentId={question.content.id} - questionHasParent={question.content.rule.parentId.length !== 0} + questionHasParent={question.content.rule.parentId?.length !== 0} questionType={question.type} openBranchingPage={openBranchingPage} setOpenBranchingPage={setOpenBranchingPage} /> - + ); } diff --git a/src/pages/Questions/answerOptions/responseSettings.tsx b/src/pages/Questions/answerOptions/responseSettings.tsx index a3663a8b..b7e73bd5 100644 --- a/src/pages/Questions/answerOptions/responseSettings.tsx +++ b/src/pages/Questions/answerOptions/responseSettings.tsx @@ -1,15 +1,18 @@ import { Box, Typography, useMediaQuery, useTheme } from "@mui/material"; import { updateQuestion } from "@root/questions/actions"; import CustomCheckbox from "@ui_kit/CustomCheckbox"; -import type { QuizQuestionVariant } from "../../../model/questionTypes/variant"; +import type { QuizQuestionVariant } from "@frontend/squzanswerer"; import { memo } from "react"; interface Props { questionId: string; isRequired: boolean; + isLargeCheck: boolean; + isMulti: boolean; + isOwn: boolean; } -const ResponseSettings = memo(function ({ questionId, isRequired }) { +const ResponseSettings = memo(function ({ questionId, isRequired, isLargeCheck, isMulti, isOwn }) { const theme = useTheme(); const isTablet = useMediaQuery(theme.breakpoints.down(900)); const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990)); @@ -20,16 +23,72 @@ const ResponseSettings = memo(function ({ questionId, isRequired }) { sx={{ display: "flex", justifyContent: "space-between", - flexDirection: isTablet ? "column" : "none", + flexDirection: isTablet ? "column" : "row", marginRight: isFigmaTablte ? (isMobile ? "0" : "0px") : "30px", + pb: "20px", + pl: "20px", + pt: isTablet ? "5px" : "0px", }} > + {/* + + Настройки ответов + + { + updateQuestion(questionId, (question) => { + question.content.largeCheck = target.checked; + }); + }} + /> + { + updateQuestion(questionId, (question) => { + question.content.multi = target.checked; + }); + }} + /> + { + updateQuestion(questionId, (question) => { + question.content.own = target.checked; + }); + }} + /> + */} ); case "help": diff --git a/src/pages/Questions/helpQuestions.tsx b/src/pages/Questions/helpQuestions.tsx index ede9b174..c811bccf 100644 --- a/src/pages/Questions/helpQuestions.tsx +++ b/src/pages/Questions/helpQuestions.tsx @@ -5,7 +5,7 @@ import SelectableButton from "@ui_kit/SelectableButton"; import UploadBox from "@ui_kit/UploadBox"; import { memo, useState } from "react"; import UploadIcon from "../../assets/icons/UploadIcon"; -import { UploadVideoModal } from "./UploadVideoModal"; +import UploadVideoModal from "./UploadVideoModal"; type BackgroundType = "text" | "video"; @@ -15,11 +15,7 @@ type HelpQuestionsProps = { hintText: string; }; -const HelpQuestions = memo(function ({ - questionId, - hintVideo, - hintText, -}) { +const HelpQuestions = memo(function ({ questionId, hintVideo, hintText }) { const [open, setOpen] = useState(false); const [backgroundType, setBackgroundType] = useState("text"); @@ -71,15 +67,17 @@ const HelpQuestions = memo(function ({ ) : ( - - Загрузите видео - + Загрузите видео setOpen(true)} sx={{ justifyContent: "flex-start" }} > {hintVideo ? ( -