Merge branch 'dev' into 'staging'
Dev See merge request frontend/squiz!326
@ -29,7 +29,6 @@
|
|||||||
"cytoscape": "^3.26.0",
|
"cytoscape": "^3.26.0",
|
||||||
"cytoscape-popper": "^2.0.0",
|
"cytoscape-popper": "^2.0.0",
|
||||||
"date-fns": "^3.0.6",
|
"date-fns": "^3.0.6",
|
||||||
"dayjs": "^1.11.10",
|
|
||||||
"emoji-mart": "^5.5.2",
|
"emoji-mart": "^5.5.2",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"formik": "^2.4.5",
|
"formik": "^2.4.5",
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
|
||||||
<head>
|
<title>Pena Quiz</title>
|
||||||
<meta charset="utf-8" />
|
<meta
|
||||||
|
name="description"
|
||||||
<title>Pena Quiz</title>
|
content="Веб-сервис с инструментами для повышения эффективности маркетологов."
|
||||||
<meta name="description" content="Веб-сервис с инструментами для повышения эффективности маркетологов." />
|
/>
|
||||||
<meta name="keywords" content=" Экосистема маркетинговых инструментов,
|
<meta
|
||||||
|
name="keywords"
|
||||||
|
content=" Экосистема маркетинговых инструментов,
|
||||||
Инструменты для социальных исследований,
|
Инструменты для социальных исследований,
|
||||||
Малый бизнес,
|
Малый бизнес,
|
||||||
Маркетинговые инструменты,
|
Маркетинговые инструменты,
|
||||||
@ -26,82 +30,140 @@
|
|||||||
Анализ данных,
|
Анализ данных,
|
||||||
Улучшение результатов,
|
Улучшение результатов,
|
||||||
Увеличение прибыли,
|
Увеличение прибыли,
|
||||||
Повышение конкурентоспособности " />
|
Повышение конкурентоспособности "
|
||||||
|
/>
|
||||||
|
|
||||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" sizes="any" /><!-- 32×32 -->
|
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" sizes="any" />
|
||||||
<link rel="icon" href="%PUBLIC_URL%/favicon.svg" type="image/svg+xml" />
|
<!-- 32×32 -->
|
||||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/favicon.png" /><!-- 180×180 -->
|
<link rel="icon" href="%PUBLIC_URL%/favicon.svg" type="image/svg+xml" />
|
||||||
|
<link rel="apple-touch-icon" href="%PUBLIC_URL%/favicon.png" />
|
||||||
|
<!-- 180×180 -->
|
||||||
|
|
||||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||||
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="theme-color" content="#000000" />
|
<meta name="theme-color" content="#000000" />
|
||||||
<meta http-equiv="Pragma" content="no-cache" />
|
<meta http-equiv="Pragma" content="no-cache" />
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Rubik:wght@400;500;600&display=swap" rel="stylesheet" />
|
<link
|
||||||
<!-- Yandex.Metrika counter -->
|
href="https://fonts.googleapis.com/css2?family=Rubik:wght@400;500;600&display=swap"
|
||||||
<script type="text/javascript">
|
rel="stylesheet"
|
||||||
(function (m, e, t, r, i, k, a) {
|
/>
|
||||||
m[i] = m[i] || function () { (m[i].a = m[i].a || []).push(arguments) };
|
<!-- Yandex.Metrika counter -->
|
||||||
m[i].l = 1 * new Date();
|
<script type="text/javascript">
|
||||||
for (var j = 0; j < document.scripts.length; j++) { if (document.scripts[j].src === r) { return; } }
|
(function (m, e, t, r, i, k, a) {
|
||||||
k = e.createElement(t), a = e.getElementsByTagName(t)[0], k.async = 1, k.src = r, a.parentNode.insertBefore(k, a)
|
m[i] =
|
||||||
})
|
m[i] ||
|
||||||
(window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym");
|
function () {
|
||||||
|
(m[i].a = m[i].a || []).push(arguments);
|
||||||
|
};
|
||||||
|
m[i].l = 1 * new Date();
|
||||||
|
for (var j = 0; j < document.scripts.length; j++) {
|
||||||
|
if (document.scripts[j].src === r) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(k = e.createElement(t)),
|
||||||
|
(a = e.getElementsByTagName(t)[0]),
|
||||||
|
(k.async = 1),
|
||||||
|
(k.src = r),
|
||||||
|
a.parentNode.insertBefore(k, a);
|
||||||
|
})(
|
||||||
|
window,
|
||||||
|
document,
|
||||||
|
"script",
|
||||||
|
"https://mc.yandex.ru/metrika/tag.js",
|
||||||
|
"ym",
|
||||||
|
);
|
||||||
|
|
||||||
const domain = location.hostname
|
const domain = location.hostname;
|
||||||
if (domain === "quiz.pena.digital") {
|
if (domain === "quiz.pena.digital") {
|
||||||
ym(96979576, "init", {
|
ym(96979576, "init", {
|
||||||
clickmap: true,
|
clickmap: true,
|
||||||
trackLinks: true,
|
trackLinks: true,
|
||||||
accurateTrackBounce: true,
|
accurateTrackBounce: true,
|
||||||
webvisor: true
|
webvisor: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// <!-- Top.Mail.Ru counter -->
|
// <!-- Top.Mail.Ru counter -->
|
||||||
var _tmr = window._tmr || (window._tmr = []);
|
var _tmr = window._tmr || (window._tmr = []);
|
||||||
_tmr.push({ id: "3513005", type: "pageView", start: (new Date()).getTime() });
|
_tmr.push({
|
||||||
(function (d, w, id) {
|
id: "3513005",
|
||||||
if (d.getElementById(id)) return;
|
type: "pageView",
|
||||||
var ts = d.createElement("script"); ts.type = "text/javascript"; ts.async = true; ts.id = id;
|
start: new Date().getTime(),
|
||||||
ts.src = "https://top-fwz1.mail.ru/js/code.js";
|
});
|
||||||
var f = function () { var s = d.getElementsByTagName("script")[0]; s.parentNode.insertBefore(ts, s); };
|
(function (d, w, id) {
|
||||||
if (w.opera == "[object Opera]") { d.addEventListener("DOMContentLoaded", f, false); } else { f(); }
|
if (d.getElementById(id)) return;
|
||||||
})(document, window, "tmr-code");
|
var ts = d.createElement("script");
|
||||||
// <!-- /Top.Mail.Ru counter -->
|
ts.type = "text/javascript";
|
||||||
|
ts.async = true;
|
||||||
};
|
ts.id = id;
|
||||||
if (domain === "squiz.pena.digital") {
|
ts.src = "https://top-fwz1.mail.ru/js/code.js";
|
||||||
ym(96979625, "init", {
|
var f = function () {
|
||||||
clickmap: true,
|
var s = d.getElementsByTagName("script")[0];
|
||||||
trackLinks: true,
|
s.parentNode.insertBefore(ts, s);
|
||||||
accurateTrackBounce: true,
|
};
|
||||||
webvisor: true
|
if (w.opera == "[object Opera]") {
|
||||||
});
|
d.addEventListener("DOMContentLoaded", f, false);
|
||||||
};
|
} else {
|
||||||
if (domain === "penaquiz.online" || domain === "penaquiz.ru") {
|
f();
|
||||||
ym(97241101, "init", {
|
}
|
||||||
clickmap: true,
|
})(document, window, "tmr-code");
|
||||||
trackLinks: true,
|
// <!-- /Top.Mail.Ru counter -->
|
||||||
accurateTrackBounce: true,
|
}
|
||||||
webvisor: true
|
if (domain === "squiz.pena.digital") {
|
||||||
});
|
ym(96979625, "init", {
|
||||||
};
|
clickmap: true,
|
||||||
|
trackLinks: true,
|
||||||
</script>
|
accurateTrackBounce: true,
|
||||||
<noscript>
|
webvisor: true,
|
||||||
<div><img src="https://mc.yandex.ru/watch/96979576" style="position:absolute; left:-9999px;" alt="" /></div>
|
});
|
||||||
<div><img src="https://mc.yandex.ru/watch/96979625" style="position:absolute; left:-9999px;" alt="" /></div>
|
}
|
||||||
<div><img src="https://mc.yandex.ru/watch/97241101" style="position:absolute; left:-9999px;" alt="" /></div>
|
if (domain === "penaquiz.online" || domain === "penaquiz.ru") {
|
||||||
<div><img src="https://top-fwz1.mail.ru/counter?id=3513005;js=na" style="position:absolute;left:-9999px;" alt="Top.Mail.Ru" /></div>
|
ym(97241101, "init", {
|
||||||
</noscript>
|
clickmap: true,
|
||||||
<!-- /Yandex.Metrika counter -->
|
trackLinks: true,
|
||||||
</head>
|
accurateTrackBounce: true,
|
||||||
|
webvisor: true,
|
||||||
<body>
|
});
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
}
|
||||||
<div id="root"></div>
|
</script>
|
||||||
</body>
|
<noscript>
|
||||||
|
<div>
|
||||||
|
<img
|
||||||
|
src="https://mc.yandex.ru/watch/96979576"
|
||||||
|
style="position: absolute; left: -9999px"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<img
|
||||||
|
src="https://mc.yandex.ru/watch/96979625"
|
||||||
|
style="position: absolute; left: -9999px"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<img
|
||||||
|
src="https://mc.yandex.ru/watch/97241101"
|
||||||
|
style="position: absolute; left: -9999px"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<img
|
||||||
|
src="https://top-fwz1.mail.ru/counter?id=3513005;js=na"
|
||||||
|
style="position: absolute; left: -9999px"
|
||||||
|
alt="Top.Mail.Ru"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</noscript>
|
||||||
|
<!-- /Yandex.Metrika counter -->
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
73
src/App.tsx
@ -1,9 +1,7 @@
|
|||||||
import type { SuspenseProps } from "react";
|
import type { SuspenseProps } from "react";
|
||||||
import { lazy, Suspense, useEffect, useLayoutEffect, useRef } from "react";
|
import { lazy, Suspense } from "react";
|
||||||
import { lazily } from "react-lazily";
|
import { lazily } from "react-lazily";
|
||||||
import ContactFormModal from "@ui_kit/ContactForm";
|
import ContactFormModal from "@ui_kit/ContactForm";
|
||||||
import dayjs from "dayjs";
|
|
||||||
import "dayjs/locale/ru";
|
|
||||||
import SigninDialog from "./pages/auth/Signin";
|
import SigninDialog from "./pages/auth/Signin";
|
||||||
import SignupDialog from "./pages/auth/Signup";
|
import SignupDialog from "./pages/auth/Signup";
|
||||||
import {
|
import {
|
||||||
@ -18,13 +16,10 @@ import Landing from "./pages/Landing/Landing";
|
|||||||
import Main from "./pages/main";
|
import Main from "./pages/main";
|
||||||
import {
|
import {
|
||||||
clearAuthToken,
|
clearAuthToken,
|
||||||
createUserAccount,
|
|
||||||
devlog,
|
|
||||||
getMessageFromFetchError,
|
getMessageFromFetchError,
|
||||||
UserAccount,
|
UserAccount,
|
||||||
useUserFetcher,
|
useUserFetcher,
|
||||||
} from "@frontend/kitui";
|
} from "@frontend/kitui";
|
||||||
import makeRequest from "@api/makeRequest";
|
|
||||||
import type { OriginalUserAccount } from "@root/user";
|
import type { OriginalUserAccount } from "@root/user";
|
||||||
import {
|
import {
|
||||||
clearUserData,
|
clearUserData,
|
||||||
@ -44,9 +39,11 @@ import RecoverPassword from "./pages/auth/RecoverPassword";
|
|||||||
import { InfoPrivilege } from "./pages/InfoPrivilege";
|
import { InfoPrivilege } from "./pages/InfoPrivilege";
|
||||||
import OutdatedLink from "./pages/auth/OutdatedLink";
|
import OutdatedLink from "./pages/auth/OutdatedLink";
|
||||||
import { useAfterpay } from "@utils/hooks/useAfterpay";
|
import { useAfterpay } from "@utils/hooks/useAfterpay";
|
||||||
|
import { useUserAccountFetcher } from "@utils/hooks/useUserAccountFetcher";
|
||||||
|
|
||||||
const MyQuizzesFull = lazy(() => import("./pages/createQuize/MyQuizzesFull"));
|
const MyQuizzesFull = lazy(() => import("./pages/createQuize/MyQuizzesFull"));
|
||||||
|
|
||||||
|
const QuizGallery = lazy(() => import("./pages/createQuize/QuizGallery"));
|
||||||
const ViewPage = lazy(() => import("./pages/ViewPublicationPage"));
|
const ViewPage = lazy(() => import("./pages/ViewPublicationPage"));
|
||||||
const Analytics = lazy(() => import("./pages/Analytics/Analytics"));
|
const Analytics = lazy(() => import("./pages/Analytics/Analytics"));
|
||||||
const EditPage = lazy(() => import("./pages/startPage/EditPage"));
|
const EditPage = lazy(() => import("./pages/startPage/EditPage"));
|
||||||
@ -62,8 +59,6 @@ const ChatImageNewWindow = lazy(
|
|||||||
() => import("@ui_kit/FloatingSupportChat/ChatImageNewWindow"),
|
() => import("@ui_kit/FloatingSupportChat/ChatImageNewWindow"),
|
||||||
);
|
);
|
||||||
|
|
||||||
dayjs.locale("ru");
|
|
||||||
|
|
||||||
const routeslink = [
|
const routeslink = [
|
||||||
{
|
{
|
||||||
path: "/edit",
|
path: "/edit",
|
||||||
@ -92,65 +87,13 @@ const LazyLoading = ({ children, fallback }: SuspenseProps) => (
|
|||||||
<Suspense fallback={fallback ?? <></>}>{children}</Suspense>
|
<Suspense fallback={fallback ?? <></>}>{children}</Suspense>
|
||||||
);
|
);
|
||||||
|
|
||||||
export function useUserAccountFetcher<T = UserAccount>({
|
|
||||||
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<never, T>({
|
|
||||||
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() {
|
export default function App() {
|
||||||
const userId = useUserStore((state) => state.userId);
|
const userId = useUserStore((state) => state.userId);
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
useUserFetcher({
|
useUserFetcher({
|
||||||
url: process.env.REACT_APP_DOMAIN + `/user/${userId}`,
|
url: `${process.env.REACT_APP_DOMAIN}/user/${userId}`,
|
||||||
userId,
|
userId,
|
||||||
onNewUser: setUser,
|
onNewUser: setUser,
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
@ -164,7 +107,7 @@ export default function App() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
useUserAccountFetcher<UserAccount>({
|
useUserAccountFetcher<UserAccount>({
|
||||||
url: process.env.REACT_APP_DOMAIN + "/customer/account",
|
url: `${process.env.REACT_APP_DOMAIN}/customer/account`,
|
||||||
userId,
|
userId,
|
||||||
onNewUserAccount: setCustomerAccount,
|
onNewUserAccount: setCustomerAccount,
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
@ -179,7 +122,7 @@ export default function App() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
useUserAccountFetcher<OriginalUserAccount>({
|
useUserAccountFetcher<OriginalUserAccount>({
|
||||||
url: process.env.REACT_APP_DOMAIN + "/squiz/account/get",
|
url: `${process.env.REACT_APP_DOMAIN}/squiz/account/get`,
|
||||||
userId,
|
userId,
|
||||||
onNewUserAccount: setUserAccount,
|
onNewUserAccount: setUserAccount,
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
@ -259,6 +202,10 @@ export default function App() {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="/gallery"
|
||||||
|
element={<LazyLoading children={<QuizGallery />} />}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/list"
|
path="/list"
|
||||||
element={<LazyLoading children={<MyQuizzesFull />} />}
|
element={<LazyLoading children={<MyQuizzesFull />} />}
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import makeRequest from "@api/makeRequest";
|
import { makeRequest } from "@api/makeRequest";
|
||||||
|
|
||||||
|
import { parseAxiosError } from "@utils/parse-error";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
LoginRequest,
|
LoginRequest,
|
||||||
@ -6,21 +8,24 @@ import type {
|
|||||||
RegisterRequest,
|
RegisterRequest,
|
||||||
RegisterResponse,
|
RegisterResponse,
|
||||||
} from "@frontend/kitui";
|
} from "@frontend/kitui";
|
||||||
import { parseAxiosError } from "../utils/parse-error";
|
|
||||||
|
|
||||||
const apiUrl = process.env.REACT_APP_DOMAIN + "/auth";
|
type RecoverResponse = {
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
|
||||||
export async function register(
|
const API_URL = `${process.env.REACT_APP_DOMAIN}/auth`;
|
||||||
|
|
||||||
|
export const register = async (
|
||||||
login: string,
|
login: string,
|
||||||
password: string,
|
password: string,
|
||||||
phoneNumber: string,
|
phoneNumber: string,
|
||||||
): Promise<[RegisterResponse | null, string?]> {
|
): Promise<[RegisterResponse | null, string?]> => {
|
||||||
try {
|
try {
|
||||||
const registerResponse = await makeRequest<
|
const registerResponse = await makeRequest<
|
||||||
RegisterRequest,
|
RegisterRequest,
|
||||||
RegisterResponse
|
RegisterResponse
|
||||||
>({
|
>({
|
||||||
url: apiUrl + "/register",
|
url: `${API_URL}/register`,
|
||||||
body: { login, password, phoneNumber },
|
body: { login, password, phoneNumber },
|
||||||
useToken: false,
|
useToken: false,
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
@ -32,15 +37,15 @@ export async function register(
|
|||||||
|
|
||||||
return [null, `Не удалось зарегестрировать аккаунт. ${error}`];
|
return [null, `Не удалось зарегестрировать аккаунт. ${error}`];
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export async function login(
|
export const login = async (
|
||||||
login: string,
|
login: string,
|
||||||
password: string,
|
password: string,
|
||||||
): Promise<[LoginResponse | null, string?]> {
|
): Promise<[LoginResponse | null, string?]> => {
|
||||||
try {
|
try {
|
||||||
const loginResponse = await makeRequest<LoginRequest, LoginResponse>({
|
const loginResponse = await makeRequest<LoginRequest, LoginResponse>({
|
||||||
url: apiUrl + "/login",
|
url: `${API_URL}/login`,
|
||||||
body: { login, password },
|
body: { login, password },
|
||||||
useToken: false,
|
useToken: false,
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
@ -52,13 +57,13 @@ export async function login(
|
|||||||
|
|
||||||
return [null, `Не удалось войти. ${error}`];
|
return [null, `Не удалось войти. ${error}`];
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export async function logout(): Promise<[unknown, string?]> {
|
export const logout = async (): Promise<[void | null, string?]> => {
|
||||||
try {
|
try {
|
||||||
const logoutResponse = await makeRequest<never, void>({
|
const logoutResponse = await makeRequest<never, void>({
|
||||||
url: apiUrl + "/logout",
|
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
url: `${API_URL}/logout`,
|
||||||
useToken: true,
|
useToken: true,
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
});
|
});
|
||||||
@ -67,30 +72,32 @@ export async function logout(): Promise<[unknown, string?]> {
|
|||||||
} catch (nativeError) {
|
} catch (nativeError) {
|
||||||
const [error] = parseAxiosError(nativeError);
|
const [error] = parseAxiosError(nativeError);
|
||||||
|
|
||||||
return [null];
|
return [null, `Не удалось выйти. ${error}`];
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export async function recover(
|
export const recover = async (
|
||||||
email: string,
|
email: string,
|
||||||
): Promise<[unknown | null, string?]> {
|
): Promise<[RecoverResponse | null, string?]> => {
|
||||||
try {
|
try {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("email", email);
|
formData.append("email", email);
|
||||||
formData.append(
|
formData.append(
|
||||||
"RedirectionURL",
|
"RedirectionURL",
|
||||||
process.env.REACT_APP_DOMAIN + "/changepwd",
|
`${process.env.REACT_APP_DOMAIN}/changepwd`,
|
||||||
);
|
);
|
||||||
const recoverResponse = await makeRequest<unknown, unknown>({
|
|
||||||
url: process.env.REACT_APP_DOMAIN + "/codeword/recover",
|
const recoverResponse = await makeRequest<FormData, RecoverResponse>({
|
||||||
|
url: `${process.env.REACT_APP_DOMAIN}/codeword/recover`,
|
||||||
body: formData,
|
body: formData,
|
||||||
useToken: false,
|
useToken: false,
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
return [recoverResponse];
|
return [recoverResponse];
|
||||||
} catch (nativeError) {
|
} catch (nativeError) {
|
||||||
const [error] = parseAxiosError(nativeError);
|
const [error] = parseAxiosError(nativeError);
|
||||||
|
|
||||||
return [null, `Не удалось восстановить пароль. ${error}`];
|
return [null, `Не удалось восстановить пароль. ${error}`];
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
import { UserAccount } from "@frontend/kitui";
|
import { makeRequest } from "@api/makeRequest";
|
||||||
import makeRequest from "@api/makeRequest";
|
|
||||||
|
|
||||||
import { parseAxiosError } from "@utils/parse-error";
|
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/cart`;
|
||||||
|
|
||||||
|
const payCart = async (): Promise<[UserAccount | null, string?]> => {
|
||||||
try {
|
try {
|
||||||
const payCartResponse = await makeRequest<never, UserAccount>({
|
const payCartResponse = await makeRequest<never, UserAccount>({
|
||||||
url: apiUrl + "/cart/pay",
|
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
url: `${API_URL}/pay`,
|
||||||
useToken: true,
|
useToken: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -19,4 +20,44 @@ export async function payCart(): Promise<[UserAccount | null, string?]> {
|
|||||||
|
|
||||||
return [null, `Не удалось оплатить товар из корзины. ${error}`];
|
return [null, `Не удалось оплатить товар из корзины. ${error}`];
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
const addCartItem = async (
|
||||||
|
id: string,
|
||||||
|
): Promise<[UserAccount | null, string?]> => {
|
||||||
|
try {
|
||||||
|
const addedItem = await makeRequest<never, UserAccount>({
|
||||||
|
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<never, UserAccount>({
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
@ -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;
|
contact: string;
|
||||||
whoami: string;
|
whoami: string;
|
||||||
}) {
|
};
|
||||||
return axios(`${domen}/feedback/callme`, {
|
|
||||||
method: "POST",
|
export const sendContactFormRequest = async (
|
||||||
headers: {
|
body: SendContactFormBody,
|
||||||
"Content-Type": "application/json",
|
): Promise<[unknown | null, string?, number?]> => {
|
||||||
},
|
try {
|
||||||
data: body,
|
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];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
@ -1,17 +1,19 @@
|
|||||||
import { makeRequest } from "@frontend/kitui";
|
import { makeRequest } from "@frontend/kitui";
|
||||||
|
|
||||||
import { parseAxiosError } from "@utils/parse-error";
|
import { parseAxiosError } from "@utils/parse-error";
|
||||||
|
|
||||||
import type { Discount } from "@model/discounts";
|
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,
|
userId: string,
|
||||||
): Promise<[Discount[] | null, string?]> {
|
): Promise<[Discount[] | null, string?]> => {
|
||||||
try {
|
try {
|
||||||
const { Discounts } = await makeRequest<unknown, { Discounts: Discount[] }>(
|
const { Discounts } = await makeRequest<never, { Discounts: Discount[] }>({
|
||||||
{ method: "GET", url: `${API_URL}/user/${userId}` },
|
method: "GET",
|
||||||
);
|
url: `${API_URL}/user/${userId}`,
|
||||||
|
});
|
||||||
|
|
||||||
return [Discounts];
|
return [Discounts];
|
||||||
} catch (nativeError) {
|
} catch (nativeError) {
|
||||||
@ -19,4 +21,4 @@ export async function getDiscounts(
|
|||||||
|
|
||||||
return [null, `Не удалось получить скидки. ${error}`];
|
return [null, `Не удалось получить скидки. ${error}`];
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
332
src/api/integration.ts
Normal file
@ -0,0 +1,332 @@
|
|||||||
|
import { makeRequest } from "@api/makeRequest";
|
||||||
|
import { parseAxiosError } from "@utils/parse-error";
|
||||||
|
|
||||||
|
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;
|
||||||
|
Email: string;
|
||||||
|
Role: string;
|
||||||
|
Group: number;
|
||||||
|
Deleted: boolean;
|
||||||
|
CreatedAt: number;
|
||||||
|
Subdomain: string;
|
||||||
|
Amoiserid: number;
|
||||||
|
Country: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAccount = async (): Promise<
|
||||||
|
[AccountResponse | null, string?]
|
||||||
|
> => {
|
||||||
|
try {
|
||||||
|
const response = await makeRequest<void, AccountResponse>({
|
||||||
|
method: "GET",
|
||||||
|
url: `${API_URL}/account`,
|
||||||
|
useToken: true,
|
||||||
|
});
|
||||||
|
return [response];
|
||||||
|
} catch (nativeError) {
|
||||||
|
const [error] = parseAxiosError(nativeError);
|
||||||
|
return [null, `Не удалось получить информацию об аккаунте. ${error}`];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// подключить Amo
|
||||||
|
|
||||||
|
export const connectAmo = async (): Promise<[string | null, string?]> => {
|
||||||
|
try {
|
||||||
|
const response = await makeRequest<void, { link: string }>({
|
||||||
|
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<void, TokenPair>({
|
||||||
|
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<PaginationRequest, TagsResponse>({
|
||||||
|
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;
|
||||||
|
AccountID: string;
|
||||||
|
AmoID: number;
|
||||||
|
Name: string;
|
||||||
|
Email: string;
|
||||||
|
Role: string;
|
||||||
|
Group: number;
|
||||||
|
Deleted: boolean;
|
||||||
|
CreatedAt: number;
|
||||||
|
Subdomain: string;
|
||||||
|
Amoiserid: number;
|
||||||
|
Country: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UsersResponse = {
|
||||||
|
count: number;
|
||||||
|
items: User[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getUsers = async ({
|
||||||
|
page,
|
||||||
|
size,
|
||||||
|
}: PaginationRequest): Promise<[UsersResponse | null, string?]> => {
|
||||||
|
try {
|
||||||
|
const usersResponse = await makeRequest<PaginationRequest, UsersResponse>({
|
||||||
|
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<
|
||||||
|
PaginationRequest & { pipelineId: number },
|
||||||
|
StepsResponse
|
||||||
|
>({
|
||||||
|
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<
|
||||||
|
PaginationRequest,
|
||||||
|
PipelinesResponse
|
||||||
|
>({
|
||||||
|
method: "GET",
|
||||||
|
url: `${API_URL}/pipelines?page=${page}&size=${size}`,
|
||||||
|
});
|
||||||
|
return [pipelinesResponse];
|
||||||
|
} catch (nativeError) {
|
||||||
|
const [error] = parseAxiosError(nativeError);
|
||||||
|
return [null, `Не удалось получить список воронок. ${error}`];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
//получение настроек интеграции
|
||||||
|
|
||||||
|
export type IntegrationRules = {
|
||||||
|
ID: number;
|
||||||
|
AccountID: number;
|
||||||
|
QuizID: number;
|
||||||
|
PerformerID: number;
|
||||||
|
PipelineID: number;
|
||||||
|
StepID: number;
|
||||||
|
UTMs: number[];
|
||||||
|
FieldsRule: {
|
||||||
|
lead: { QuestionID: number }[];
|
||||||
|
contact: { ContactRuleMap: string }[];
|
||||||
|
company: { QuestionID: number }[];
|
||||||
|
customer: { QuestionID: number }[];
|
||||||
|
};
|
||||||
|
Deleted: boolean;
|
||||||
|
CreatedAt: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getIntegrationRules = async (
|
||||||
|
quizID: string,
|
||||||
|
): Promise<[IntegrationRules | null, string?]> => {
|
||||||
|
try {
|
||||||
|
const settingsResponse = await makeRequest<void, IntegrationRules>({
|
||||||
|
method: "GET",
|
||||||
|
url: `${API_URL}/rules/${quizID}`,
|
||||||
|
});
|
||||||
|
return [settingsResponse];
|
||||||
|
} catch (nativeError) {
|
||||||
|
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 updateIntegrationRules = async (
|
||||||
|
quizID: string,
|
||||||
|
settings: IntegrationRulesUpdate,
|
||||||
|
): Promise<[string | null, string?]> => {
|
||||||
|
try {
|
||||||
|
const updateResponse = await makeRequest<IntegrationRulesUpdate, string>({
|
||||||
|
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 CustomFieldsResponse = {
|
||||||
|
count: number;
|
||||||
|
items: CustomField[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCustomFields = async (
|
||||||
|
pagination: PaginationRequest,
|
||||||
|
): Promise<[CustomFieldsResponse | null, string?]> => {
|
||||||
|
try {
|
||||||
|
const fieldsResponse = await makeRequest<
|
||||||
|
PaginationRequest,
|
||||||
|
CustomFieldsResponse
|
||||||
|
>({
|
||||||
|
method: "GET",
|
||||||
|
url: `${API_URL}/fields?page=${pagination.page}&size=${pagination.size}`,
|
||||||
|
});
|
||||||
|
return [fieldsResponse];
|
||||||
|
} catch (nativeError) {
|
||||||
|
const [error] = parseAxiosError(nativeError);
|
||||||
|
return [null, `Не удалось получить список кастомных полей. ${error}`];
|
||||||
|
}
|
||||||
|
};
|
@ -1,10 +1,13 @@
|
|||||||
import * as KIT from "@frontend/kitui";
|
import * as KIT from "@frontend/kitui";
|
||||||
import { Method, ResponseType, AxiosError } from "axios";
|
import { Method, ResponseType, AxiosError } from "axios";
|
||||||
|
import { redirect } from "react-router-dom";
|
||||||
import { clearAuthToken } from "@frontend/kitui";
|
import { clearAuthToken } from "@frontend/kitui";
|
||||||
|
|
||||||
import { cleanAuthTicketData } from "@root/ticket";
|
import { cleanAuthTicketData } from "@root/ticket";
|
||||||
import { clearUserData } from "@root/user";
|
import { clearUserData } from "@root/user";
|
||||||
import { clearQuizData } from "@root/quizes/store";
|
import { clearQuizData } from "@root/quizes/store";
|
||||||
import { redirect } from "react-router-dom";
|
|
||||||
|
import type { AxiosResponse } from "axios";
|
||||||
|
|
||||||
interface MakeRequest {
|
interface MakeRequest {
|
||||||
method?: Method | undefined;
|
method?: Method | undefined;
|
||||||
@ -17,18 +20,22 @@ interface MakeRequest {
|
|||||||
withCredentials?: boolean | undefined;
|
withCredentials?: boolean | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function makeRequest<TRequest = unknown, TResponse = unknown>(
|
type ExtendedAxiosResponse = AxiosResponse & { message: string };
|
||||||
data: MakeRequest,
|
|
||||||
): Promise<TResponse> {
|
export const makeRequest = async <TRequest = unknown, TResponse = unknown>(
|
||||||
try {
|
data: MakeRequest,
|
||||||
const response = await KIT.makeRequest<unknown>(data);
|
): Promise<TResponse> => {
|
||||||
|
try {
|
||||||
|
const response = await KIT.makeRequest<unknown, TResponse>(data);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (nativeError) {
|
||||||
|
const error = nativeError as AxiosError;
|
||||||
|
|
||||||
return response as TResponse;
|
|
||||||
} catch (e) {
|
|
||||||
const error = e as AxiosError;
|
|
||||||
if (
|
if (
|
||||||
error.response?.status === 400 &&
|
error.response?.status === 400 &&
|
||||||
error.response?.data?.message === "refreshToken is empty"
|
(error.response?.data as ExtendedAxiosResponse)?.message ===
|
||||||
|
"refreshToken is empty"
|
||||||
) {
|
) {
|
||||||
cleanAuthTicketData();
|
cleanAuthTicketData();
|
||||||
clearAuthToken();
|
clearAuthToken();
|
||||||
@ -36,7 +43,7 @@ async function makeRequest<TRequest = unknown, TResponse = unknown>(
|
|||||||
clearQuizData();
|
clearQuizData();
|
||||||
redirect("/");
|
redirect("/");
|
||||||
}
|
}
|
||||||
throw e;
|
|
||||||
|
throw nativeError;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
export default makeRequest;
|
|
||||||
|
@ -1,25 +1,30 @@
|
|||||||
import makeRequest from "@api/makeRequest";
|
import { makeRequest } from "@api/makeRequest";
|
||||||
|
|
||||||
import { parseAxiosError } from "@utils/parse-error";
|
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 {
|
try {
|
||||||
const response = await makeRequest<
|
const response = await makeRequest<
|
||||||
{ codeword: string } | { fastLink: string },
|
ActivatePromocodeRequest,
|
||||||
{ greetings: string }
|
ActivatePromocodeResponse
|
||||||
>({
|
>({
|
||||||
url: apiUrl + "/activate",
|
|
||||||
method: "POST",
|
method: "POST",
|
||||||
contentType: true,
|
url: `${API_URL}/activate`,
|
||||||
body: { codeword: promocode },
|
body: { codeword: promocode },
|
||||||
|
contentType: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
return response.greetings;
|
return [response.greetings];
|
||||||
} catch (nativeError) {
|
} catch (nativeError) {
|
||||||
const [error] = parseAxiosError(nativeError);
|
const [error] = parseAxiosError(nativeError);
|
||||||
throw new Error(error);
|
|
||||||
|
|
||||||
|
return [null, `Ошибка при активации промокода. ${error}`];
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
@ -1,84 +1,155 @@
|
|||||||
import makeRequest from "@api/makeRequest";
|
import { makeRequest } from "@api/makeRequest";
|
||||||
import { CreateQuestionRequest } from "model/question/create";
|
|
||||||
import { RawQuestion } from "model/question/question";
|
import { replaceSpacesToEmptyLines } from "@utils/replaceSpacesToEmptyLines";
|
||||||
import {
|
import { parseAxiosError } from "@utils/parse-error";
|
||||||
|
|
||||||
|
import type { CreateQuestionRequest } from "model/question/create";
|
||||||
|
import type { RawQuestion } from "model/question/question";
|
||||||
|
import type {
|
||||||
GetQuestionListRequest,
|
GetQuestionListRequest,
|
||||||
GetQuestionListResponse,
|
GetQuestionListResponse,
|
||||||
} from "@model/question/getList";
|
} from "@model/question/getList";
|
||||||
import {
|
import type {
|
||||||
EditQuestionRequest,
|
EditQuestionRequest,
|
||||||
EditQuestionResponse,
|
EditQuestionResponse,
|
||||||
} from "@model/question/edit";
|
} from "@model/question/edit";
|
||||||
import {
|
import type {
|
||||||
DeleteQuestionRequest,
|
DeleteQuestionRequest,
|
||||||
DeleteQuestionResponse,
|
DeleteQuestionResponse,
|
||||||
} from "@model/question/delete";
|
} from "@model/question/delete";
|
||||||
import {
|
import type {
|
||||||
CopyQuestionRequest,
|
CopyQuestionRequest,
|
||||||
CopyQuestionResponse,
|
CopyQuestionResponse,
|
||||||
} from "@model/question/copy";
|
} 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) {
|
export const createQuestion = async (
|
||||||
return makeRequest<CreateQuestionRequest, RawQuestion>({
|
body: CreateQuestionRequest,
|
||||||
url: `${baseUrl}/question/create`,
|
): Promise<[RawQuestion | null, string?]> => {
|
||||||
body,
|
try {
|
||||||
method: "POST",
|
const createdQuestion = await makeRequest<
|
||||||
});
|
CreateQuestionRequest,
|
||||||
}
|
RawQuestion
|
||||||
|
>({
|
||||||
|
method: "POST",
|
||||||
|
url: `${API_URL}/question/create`,
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
|
||||||
async function getQuestionList(body?: Partial<GetQuestionListRequest>) {
|
return [createdQuestion];
|
||||||
if (!body?.quiz_id) return null;
|
} catch (nativeError) {
|
||||||
|
const [error] = parseAxiosError(nativeError);
|
||||||
|
|
||||||
const response = await makeRequest<
|
return [null, `Не удалось создать вопрос. ${error}`];
|
||||||
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 replaceSpacesToEmptyLines(clearArrayFromEmptySpaceBlaBlaValue);
|
const getQuestionList = async (
|
||||||
}
|
body?: Partial<GetQuestionListRequest>,
|
||||||
|
): Promise<[RawQuestion[] | null, string?]> => {
|
||||||
|
try {
|
||||||
|
if (!body?.quiz_id) return [null, "Квиз не найден"];
|
||||||
|
|
||||||
function editQuestion(body: EditQuestionRequest, signal?: AbortSignal) {
|
const response = await makeRequest<
|
||||||
return makeRequest<EditQuestionRequest, EditQuestionResponse>({
|
GetQuestionListRequest,
|
||||||
url: `${baseUrl}/question/edit`,
|
GetQuestionListResponse
|
||||||
body,
|
>({
|
||||||
method: "PATCH",
|
method: "POST",
|
||||||
signal,
|
url: `${API_URL}/question/getList`,
|
||||||
});
|
body: { ...defaultGetQuestionListBody, ...body },
|
||||||
}
|
});
|
||||||
|
|
||||||
function copyQuestion(questionId: number, quizId: number) {
|
const clearArrayFromEmptySpaceBlaBlaValue = response.items?.map(
|
||||||
return makeRequest<CopyQuestionRequest, CopyQuestionResponse>({
|
(question) => {
|
||||||
url: `${baseUrl}/question/copy`,
|
let data = question;
|
||||||
body: { id: questionId, quiz_id: quizId },
|
|
||||||
method: "POST",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteQuestion(id: number) {
|
for (let key in question) {
|
||||||
return makeRequest<DeleteQuestionRequest, DeleteQuestionResponse>({
|
if (question[key as keyof RawQuestion] === " ") {
|
||||||
url: `${baseUrl}/question/delete`,
|
//@ts-ignore
|
||||||
body: { id },
|
data[key] = "";
|
||||||
method: "DELETE",
|
}
|
||||||
});
|
}
|
||||||
}
|
|
||||||
|
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 = {
|
export const questionApi = {
|
||||||
create: createQuestion,
|
create: createQuestion,
|
||||||
|
240
src/api/quiz.ts
@ -1,79 +1,190 @@
|
|||||||
import makeRequest from "@api/makeRequest";
|
import { makeRequest } from "@api/makeRequest";
|
||||||
import { defaultQuizConfig } from "@model/quizSettings";
|
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";
|
import { parseAxiosError } from "@utils/parse-error";
|
||||||
const imagesUrl = process.env.REACT_APP_DOMAIN + "/squizstorer";
|
|
||||||
|
|
||||||
function createQuiz(body?: Partial<CreateQuizRequest>) {
|
import type { RawQuiz } from "model/quiz/quiz";
|
||||||
return makeRequest<CreateQuizRequest, RawQuiz>({
|
import type { CopyQuizRequest, CopyQuizResponse } from "model/quiz/copy";
|
||||||
url: `${baseUrl}/quiz/create`,
|
import type { CreateQuizRequest } from "model/quiz/create";
|
||||||
body: { ...defaultCreateQuizBody, ...body },
|
import type { DeleteQuizRequest, DeleteQuizResponse } from "model/quiz/delete";
|
||||||
method: "POST",
|
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<GetQuizListRequest>) {
|
type AddedQuizImagesResponse = {
|
||||||
const response = await makeRequest<GetQuizListRequest, GetQuizListResponse>({
|
[key: string]: string;
|
||||||
url: `${baseUrl}/quiz/getList`,
|
};
|
||||||
body: { ...defaultGetQuizListBody, ...body },
|
|
||||||
method: "POST",
|
|
||||||
});
|
|
||||||
|
|
||||||
return response.items;
|
const API_URL = `${process.env.REACT_APP_DOMAIN}/squiz`;
|
||||||
}
|
const IMAGES_URL = `${process.env.REACT_APP_DOMAIN}/squizstorer`;
|
||||||
|
|
||||||
function getQuiz(body?: Partial<GetQuizRequest>) {
|
export const createQuiz = async (
|
||||||
return makeRequest<GetQuizRequest, GetQuizResponse>({
|
body?: Partial<CreateQuizRequest>,
|
||||||
url: `${baseUrl}/quiz/get`,
|
): Promise<[RawQuiz | null, string?]> => {
|
||||||
body: { ...defaultGetQuizBody, ...body },
|
try {
|
||||||
method: "GET",
|
const createdQuiz = await makeRequest<CreateQuizRequest, RawQuiz>({
|
||||||
});
|
method: "POST",
|
||||||
}
|
url: `${API_URL}/quiz/create`,
|
||||||
|
body: { ...defaultCreateQuizBody, ...body },
|
||||||
|
});
|
||||||
|
|
||||||
async function editQuiz(body: EditQuizRequest, signal?: AbortSignal) {
|
return [createdQuiz];
|
||||||
return makeRequest<EditQuizRequest, EditQuizResponse>({
|
} catch (nativeError) {
|
||||||
url: `${baseUrl}/quiz/edit`,
|
const [error] = parseAxiosError(nativeError);
|
||||||
body,
|
|
||||||
method: "PATCH",
|
|
||||||
signal,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function copyQuiz(id: number) {
|
return [null, `Не удалось создать квиз. ${error}`];
|
||||||
return makeRequest<CopyQuizRequest, CopyQuizResponse>({
|
}
|
||||||
url: `${baseUrl}/quiz/copy`,
|
};
|
||||||
body: { id },
|
|
||||||
method: "POST",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteQuiz(id: number) {
|
export const getQuizList = async (
|
||||||
return makeRequest<DeleteQuizRequest, DeleteQuizResponse>({
|
body?: Partial<CreateQuizRequest>,
|
||||||
url: `${baseUrl}/quiz/delete`,
|
): Promise<[RawQuiz[] | null, string?]> => {
|
||||||
body: { id },
|
try {
|
||||||
method: "DELETE",
|
const { items } = await makeRequest<
|
||||||
});
|
GetQuizListRequest,
|
||||||
}
|
GetQuizListResponse
|
||||||
|
>({
|
||||||
|
method: "POST",
|
||||||
|
url: `${API_URL}/quiz/getList`,
|
||||||
|
body: { ...defaultGetQuizListBody, ...body },
|
||||||
|
});
|
||||||
|
|
||||||
function addQuizImages(quizId: number, image: Blob) {
|
return [items];
|
||||||
const formData = new FormData();
|
} catch (nativeError) {
|
||||||
|
const [error] = parseAxiosError(nativeError);
|
||||||
|
|
||||||
formData.append("quiz", quizId.toString());
|
return [null, `Не удалось получить список квизов. ${error}`];
|
||||||
formData.append("image", image);
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return makeRequest<FormData, { [key: string]: string }>({
|
export const getQuiz = async (
|
||||||
url: `${imagesUrl}/quiz/putImages`,
|
body?: Partial<GetQuizRequest>,
|
||||||
body: formData,
|
): Promise<[GetQuizResponse | null, string?]> => {
|
||||||
method: "PUT",
|
try {
|
||||||
});
|
const quiz = await makeRequest<GetQuizRequest, GetQuizResponse>({
|
||||||
}
|
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<EditQuizRequest, EditQuizResponse>({
|
||||||
|
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<CopyQuizRequest, CopyQuizResponse>({
|
||||||
|
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 = {
|
export const quizApi = {
|
||||||
create: createQuiz,
|
create: createQuiz,
|
||||||
@ -83,6 +194,7 @@ export const quizApi = {
|
|||||||
copy: copyQuiz,
|
copy: copyQuiz,
|
||||||
delete: deleteQuiz,
|
delete: deleteQuiz,
|
||||||
addImages: addQuizImages,
|
addImages: addQuizImages,
|
||||||
|
copyTemplate: copyQuizTemplate,
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultCreateQuizBody: CreateQuizRequest = {
|
const defaultCreateQuizBody: CreateQuizRequest = {
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
import makeRequest from "@api/makeRequest";
|
import { makeRequest } from "@api/makeRequest";
|
||||||
import { RawResult } from "@model/result/result";
|
|
||||||
|
import { parseAxiosError } from "@utils/parse-error";
|
||||||
|
|
||||||
|
import type { RawResult } from "@model/result/result";
|
||||||
|
|
||||||
interface IResultListBody {
|
interface IResultListBody {
|
||||||
to: number;
|
to: number;
|
||||||
@ -29,47 +32,113 @@ export interface IAnswerResult {
|
|||||||
question_id: number;
|
question_id: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getResultList(quizId: number, page: number, body: any) {
|
type ResultFilter = {
|
||||||
return makeRequest<IResultListBody, RawResult>({
|
from?: string;
|
||||||
url: process.env.REACT_APP_DOMAIN + `/squiz/results/getResults/${quizId}`,
|
new?: boolean;
|
||||||
method: "POST",
|
to?: string;
|
||||||
body: { page: page, limit: 10, ...body },
|
};
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteResult(resultId: number) {
|
type ObsolescenceRequest = {
|
||||||
return makeRequest<unknown, unknown>({
|
answers: number[];
|
||||||
url: process.env.REACT_APP_DOMAIN + `/squiz/results/delete/${resultId}`,
|
};
|
||||||
body: {},
|
|
||||||
method: "DELETE",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function obsolescenceResult(idResultArray: number[]) {
|
const API_URL = `${process.env.REACT_APP_DOMAIN}/squiz`;
|
||||||
return makeRequest<unknown, unknown>({
|
|
||||||
url: process.env.REACT_APP_DOMAIN + `/squiz/result/seen`,
|
|
||||||
body: {
|
|
||||||
answers: idResultArray,
|
|
||||||
},
|
|
||||||
method: "PATCH",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAnswerResultList(resultId: number) {
|
const getResultList = async (
|
||||||
return makeRequest<unknown, IAnswerResult[]>({
|
quizId: number,
|
||||||
url: process.env.REACT_APP_DOMAIN + `/squiz/result/${resultId}`,
|
page: number,
|
||||||
method: "GET",
|
body: ResultFilter,
|
||||||
});
|
): Promise<[RawResult | null, string?]> => {
|
||||||
}
|
try {
|
||||||
|
const resultList = await makeRequest<IResultListBody, RawResult>({
|
||||||
|
method: "POST",
|
||||||
|
url: `${API_URL}/results/getResults/${quizId}`,
|
||||||
|
body: { page: page, limit: 10, ...body },
|
||||||
|
});
|
||||||
|
|
||||||
function AnswerResultListEx(quizId: number, body: any) {
|
return [resultList];
|
||||||
return makeRequest<unknown, unknown>({
|
} catch (nativeError) {
|
||||||
responseType: "blob",
|
const [error] = parseAxiosError(nativeError);
|
||||||
url: process.env.REACT_APP_DOMAIN + `/squiz/results/${quizId}/export`,
|
|
||||||
method: "POST",
|
return [null, `Не удалось получить результат. ${error}`];
|
||||||
body: body,
|
}
|
||||||
});
|
};
|
||||||
}
|
|
||||||
|
const deleteResult = async (
|
||||||
|
resultId: number,
|
||||||
|
): Promise<[string | null, string?]> => {
|
||||||
|
try {
|
||||||
|
const deletedResult = await makeRequest<void, string>({
|
||||||
|
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<ObsolescenceRequest, null>({
|
||||||
|
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<never, IAnswerResult[]>({
|
||||||
|
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<ResultFilter, Blob>({
|
||||||
|
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 = {
|
export const resultApi = {
|
||||||
getList: getResultList,
|
getList: getResultList,
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
import makeRequest from "@api/makeRequest";
|
import { makeRequest } from "@api/makeRequest";
|
||||||
|
|
||||||
import { parseAxiosError } from "@utils/parse-error";
|
import { parseAxiosError } from "@utils/parse-error";
|
||||||
|
|
||||||
const apiUrl = process.env.REACT_APP_DOMAIN + "/squiz/statistic";
|
|
||||||
|
|
||||||
export type DevicesResponse = {
|
export type DevicesResponse = {
|
||||||
Device: Record<string, number>;
|
Device: Record<string, number>;
|
||||||
OS: Record<string, number>;
|
OS: Record<string, number>;
|
||||||
@ -29,6 +27,8 @@ type TRequest = {
|
|||||||
from: number;
|
from: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const API_URL = `${process.env.REACT_APP_DOMAIN}/squiz/statistic`;
|
||||||
|
|
||||||
export const getDevices = async (
|
export const getDevices = async (
|
||||||
quizId: string,
|
quizId: string,
|
||||||
to: number,
|
to: number,
|
||||||
@ -37,7 +37,7 @@ export const getDevices = async (
|
|||||||
try {
|
try {
|
||||||
const devicesResponse = await makeRequest<TRequest, DevicesResponse>({
|
const devicesResponse = await makeRequest<TRequest, DevicesResponse>({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: `${apiUrl}/${quizId}/devices`,
|
url: `${API_URL}/${quizId}/devices`,
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
body: { to, from },
|
body: { to, from },
|
||||||
});
|
});
|
||||||
@ -58,7 +58,7 @@ export const getGeneral = async (
|
|||||||
try {
|
try {
|
||||||
const generalResponse = await makeRequest<TRequest, GeneralResponse>({
|
const generalResponse = await makeRequest<TRequest, GeneralResponse>({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: `${apiUrl}/${quizId}/general`,
|
url: `${API_URL}/${quizId}/general`,
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
body: { to, from },
|
body: { to, from },
|
||||||
});
|
});
|
||||||
@ -79,7 +79,7 @@ export const getQuestions = async (
|
|||||||
try {
|
try {
|
||||||
const questionsResponse = await makeRequest<TRequest, QuestionsResponse>({
|
const questionsResponse = await makeRequest<TRequest, QuestionsResponse>({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: `${apiUrl}/${quizId}/questions`,
|
url: `${API_URL}/${quizId}/questions`,
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
body: { to, from },
|
body: { to, from },
|
||||||
});
|
});
|
||||||
|
23
src/api/tariff.ts
Normal file
@ -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<never, GetTariffsResponse>({
|
||||||
|
method: "GET",
|
||||||
|
url: `${API_URL}?page=${page}&limit=100`,
|
||||||
|
});
|
||||||
|
return [tariffs];
|
||||||
|
} catch (nativeError) {
|
||||||
|
const [error] = parseAxiosError(nativeError);
|
||||||
|
|
||||||
|
return [null, `Ошибка при получении списка тарифов. ${error}`];
|
||||||
|
}
|
||||||
|
};
|
@ -1,20 +1,30 @@
|
|||||||
import makeRequest from "@api/makeRequest";
|
import { createTicket as createTicketRequest } from "@frontend/kitui";
|
||||||
import { parseAxiosError } from "../utils/parse-error";
|
|
||||||
|
|
||||||
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`;
|
||||||
|
|
||||||
|
export const sendTicketMessage = async (
|
||||||
ticketId: string,
|
ticketId: string,
|
||||||
message: string,
|
message: string,
|
||||||
): Promise<[null, string?]> {
|
): Promise<[null, string?]> => {
|
||||||
try {
|
try {
|
||||||
const sendTicketMessageResponse = await makeRequest<
|
const sendTicketMessageResponse = await makeRequest<
|
||||||
SendTicketMessageRequest,
|
SendTicketMessageRequest,
|
||||||
null
|
null
|
||||||
>({
|
>({
|
||||||
url: `${apiUrl}/send`,
|
url: `${API_URL}/send`,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
useToken: true,
|
useToken: true,
|
||||||
body: { ticket: ticketId, message: message, lang: "ru", files: [] },
|
body: { ticket: ticketId, message: message, lang: "ru", files: [] },
|
||||||
@ -26,12 +36,12 @@ export async function sendTicketMessage(
|
|||||||
|
|
||||||
return [null, `Не удалось отправить сообщение. ${error}`];
|
return [null, `Не удалось отправить сообщение. ${error}`];
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export async function shownMessage(id: string): Promise<[null, string?]> {
|
export const shownMessage = async (id: string): Promise<[null, string?]> => {
|
||||||
try {
|
try {
|
||||||
const shownMessageResponse = await makeRequest<{ id: string }, null>({
|
const shownMessageResponse = await makeRequest<{ id: string }, null>({
|
||||||
url: apiUrl + "/shown",
|
url: `${API_URL}/shown`,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
useToken: true,
|
useToken: true,
|
||||||
body: { id },
|
body: { id },
|
||||||
@ -43,4 +53,47 @@ export async function shownMessage(id: string): Promise<[null, string?]> {
|
|||||||
|
|
||||||
return [null, `Не удалось прочесть сообщение. ${error}`];
|
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<FormData, SendFileResponse>({
|
||||||
|
method: "POST",
|
||||||
|
url: `${process.env.REACT_APP_DOMAIN}/heruvym/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/create`,
|
||||||
|
body: { Title: "Unauth title", Message: message },
|
||||||
|
useToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
return [createdTicket];
|
||||||
|
} catch (nativeError) {
|
||||||
|
const [error] = parseAxiosError(nativeError);
|
||||||
|
|
||||||
|
return [null, `Не удалось создать тикет. ${error}`];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
66
src/api/user.ts
Normal file
@ -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<never, UserAccount>({
|
||||||
|
method: "GET",
|
||||||
|
url: `${process.env.REACT_APP_DOMAIN}/customer/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<never, OriginalUserAccount>({
|
||||||
|
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<RecoverUserRequest, unknown>({
|
||||||
|
url: `${process.env.REACT_APP_DOMAIN}/user`,
|
||||||
|
method: "PATCH",
|
||||||
|
body: { password },
|
||||||
|
});
|
||||||
|
|
||||||
|
return [recoverResponse];
|
||||||
|
} catch (nativeError) {
|
||||||
|
const [error] = parseAxiosError(nativeError);
|
||||||
|
|
||||||
|
return [null, `Не удалось восстановить пароль. ${error}`];
|
||||||
|
}
|
||||||
|
};
|
BIN
src/assets/icons/Amologo.png
Normal file
After Width: | Height: | Size: 5.1 KiB |
@ -4,10 +4,15 @@ interface Props {
|
|||||||
color?: string;
|
color?: string;
|
||||||
bgcolor?: string;
|
bgcolor?: string;
|
||||||
marL?: 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();
|
const theme = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
Before Width: | Height: | Size: 124 KiB |
Before Width: | Height: | Size: 60 KiB |
Before Width: | Height: | Size: 170 KiB |
Before Width: | Height: | Size: 83 KiB |
Before Width: | Height: | Size: 124 KiB |
Before Width: | Height: | Size: 86 KiB |
BIN
src/assets/quiz-templates/auto/auto-1.jpg
Normal file
After Width: | Height: | Size: 123 KiB |
BIN
src/assets/quiz-templates/auto/auto-10.jpg
Normal file
After Width: | Height: | Size: 1.3 MiB |
BIN
src/assets/quiz-templates/auto/auto-2.jpg
Normal file
After Width: | Height: | Size: 164 KiB |
BIN
src/assets/quiz-templates/auto/auto-3.jpg
Normal file
After Width: | Height: | Size: 861 KiB |
BIN
src/assets/quiz-templates/auto/auto-4.jpg
Normal file
After Width: | Height: | Size: 514 KiB |
BIN
src/assets/quiz-templates/auto/auto-5.jpg
Normal file
After Width: | Height: | Size: 642 KiB |
BIN
src/assets/quiz-templates/auto/auto-6.jpg
Normal file
After Width: | Height: | Size: 667 KiB |
BIN
src/assets/quiz-templates/auto/auto-7.jpg
Normal file
After Width: | Height: | Size: 107 KiB |
BIN
src/assets/quiz-templates/auto/auto-8.jpg
Normal file
After Width: | Height: | Size: 93 KiB |
BIN
src/assets/quiz-templates/auto/auto-9.jpg
Normal file
After Width: | Height: | Size: 600 KiB |
BIN
src/assets/quiz-templates/education/education-1.jpg
Normal file
After Width: | Height: | Size: 618 KiB |
BIN
src/assets/quiz-templates/education/education-10.jpg
Normal file
After Width: | Height: | Size: 442 KiB |
BIN
src/assets/quiz-templates/education/education-2.jpg
Normal file
After Width: | Height: | Size: 1.1 MiB |
BIN
src/assets/quiz-templates/education/education-3.jpg
Normal file
After Width: | Height: | Size: 82 KiB |
BIN
src/assets/quiz-templates/education/education-4.jpg
Normal file
After Width: | Height: | Size: 264 KiB |
BIN
src/assets/quiz-templates/education/education-5.jpg
Normal file
After Width: | Height: | Size: 600 KiB |
BIN
src/assets/quiz-templates/education/education-6.jpg
Normal file
After Width: | Height: | Size: 295 KiB |
BIN
src/assets/quiz-templates/education/education-7.jpg
Normal file
After Width: | Height: | Size: 645 KiB |
BIN
src/assets/quiz-templates/education/education-8.jpg
Normal file
After Width: | Height: | Size: 847 KiB |
BIN
src/assets/quiz-templates/education/education-9.jpg
Normal file
After Width: | Height: | Size: 746 KiB |
BIN
src/assets/quiz-templates/health/health-1.jpg
Normal file
After Width: | Height: | Size: 431 KiB |
BIN
src/assets/quiz-templates/health/health-10.jpg
Normal file
After Width: | Height: | Size: 752 KiB |
BIN
src/assets/quiz-templates/health/health-11.jpg
Normal file
After Width: | Height: | Size: 424 KiB |
BIN
src/assets/quiz-templates/health/health-12.jpg
Normal file
After Width: | Height: | Size: 584 KiB |
BIN
src/assets/quiz-templates/health/health-13.jpg
Normal file
After Width: | Height: | Size: 205 KiB |
BIN
src/assets/quiz-templates/health/health-14.jpg
Normal file
After Width: | Height: | Size: 517 KiB |
BIN
src/assets/quiz-templates/health/health-15.jpg
Normal file
After Width: | Height: | Size: 442 KiB |
BIN
src/assets/quiz-templates/health/health-16.jpg
Normal file
After Width: | Height: | Size: 209 KiB |
BIN
src/assets/quiz-templates/health/health-17.jpg
Normal file
After Width: | Height: | Size: 719 KiB |
BIN
src/assets/quiz-templates/health/health-18.jpg
Normal file
After Width: | Height: | Size: 132 KiB |
BIN
src/assets/quiz-templates/health/health-19.jpg
Normal file
After Width: | Height: | Size: 870 KiB |
BIN
src/assets/quiz-templates/health/health-2.jpg
Normal file
After Width: | Height: | Size: 516 KiB |
BIN
src/assets/quiz-templates/health/health-20.jpg
Normal file
After Width: | Height: | Size: 522 KiB |
BIN
src/assets/quiz-templates/health/health-3.jpg
Normal file
After Width: | Height: | Size: 310 KiB |
BIN
src/assets/quiz-templates/health/health-4.jpg
Normal file
After Width: | Height: | Size: 572 KiB |
BIN
src/assets/quiz-templates/health/health-5.jpg
Normal file
After Width: | Height: | Size: 758 KiB |
BIN
src/assets/quiz-templates/health/health-6.jpg
Normal file
After Width: | Height: | Size: 1013 KiB |
BIN
src/assets/quiz-templates/health/health-7.jpg
Normal file
After Width: | Height: | Size: 436 KiB |
BIN
src/assets/quiz-templates/health/health-8.jpg
Normal file
After Width: | Height: | Size: 571 KiB |
BIN
src/assets/quiz-templates/health/health-9.jpg
Normal file
After Width: | Height: | Size: 669 KiB |
BIN
src/assets/quiz-templates/production/production-1.jpg
Normal file
After Width: | Height: | Size: 425 KiB |
BIN
src/assets/quiz-templates/production/production-10.jpg
Normal file
After Width: | Height: | Size: 865 KiB |
BIN
src/assets/quiz-templates/production/production-2.jpg
Normal file
After Width: | Height: | Size: 378 KiB |
BIN
src/assets/quiz-templates/production/production-3.jpg
Normal file
After Width: | Height: | Size: 568 KiB |
BIN
src/assets/quiz-templates/production/production-4.jpg
Normal file
After Width: | Height: | Size: 550 KiB |
BIN
src/assets/quiz-templates/production/production-5.jpg
Normal file
After Width: | Height: | Size: 512 KiB |
BIN
src/assets/quiz-templates/production/production-6.jpg
Normal file
After Width: | Height: | Size: 410 KiB |
BIN
src/assets/quiz-templates/production/production-7.jpg
Normal file
After Width: | Height: | Size: 1.2 MiB |
BIN
src/assets/quiz-templates/production/production-8.jpg
Normal file
After Width: | Height: | Size: 351 KiB |
BIN
src/assets/quiz-templates/production/production-9.jpg
Normal file
After Width: | Height: | Size: 658 KiB |
BIN
src/assets/quiz-templates/real-estate/real-estate-1.jpg
Normal file
After Width: | Height: | Size: 242 KiB |
BIN
src/assets/quiz-templates/real-estate/real-estate-10.jpg
Normal file
After Width: | Height: | Size: 297 KiB |
BIN
src/assets/quiz-templates/real-estate/real-estate-2.jpg
Normal file
After Width: | Height: | Size: 698 KiB |
BIN
src/assets/quiz-templates/real-estate/real-estate-3.jpg
Normal file
After Width: | Height: | Size: 356 KiB |
BIN
src/assets/quiz-templates/real-estate/real-estate-4.jpg
Normal file
After Width: | Height: | Size: 1.0 MiB |
BIN
src/assets/quiz-templates/real-estate/real-estate-5.jpg
Normal file
After Width: | Height: | Size: 262 KiB |
BIN
src/assets/quiz-templates/real-estate/real-estate-6.jpg
Normal file
After Width: | Height: | Size: 788 KiB |
BIN
src/assets/quiz-templates/real-estate/real-estate-7.jpg
Normal file
After Width: | Height: | Size: 533 KiB |
BIN
src/assets/quiz-templates/real-estate/real-estate-8.jpg
Normal file
After Width: | Height: | Size: 656 KiB |
BIN
src/assets/quiz-templates/real-estate/real-estate-9.jpg
Normal file
After Width: | Height: | Size: 352 KiB |
BIN
src/assets/quiz-templates/repair/repair-1.jpg
Normal file
After Width: | Height: | Size: 972 KiB |
BIN
src/assets/quiz-templates/repair/repair-10.jpg
Normal file
After Width: | Height: | Size: 488 KiB |
BIN
src/assets/quiz-templates/repair/repair-2.jpg
Normal file
After Width: | Height: | Size: 592 KiB |
BIN
src/assets/quiz-templates/repair/repair-3.jpg
Normal file
After Width: | Height: | Size: 637 KiB |
BIN
src/assets/quiz-templates/repair/repair-4.jpg
Normal file
After Width: | Height: | Size: 771 KiB |
BIN
src/assets/quiz-templates/repair/repair-5.jpg
Normal file
After Width: | Height: | Size: 447 KiB |
BIN
src/assets/quiz-templates/repair/repair-6.jpg
Normal file
After Width: | Height: | Size: 1.3 MiB |
BIN
src/assets/quiz-templates/repair/repair-7.jpg
Normal file
After Width: | Height: | Size: 518 KiB |
BIN
src/assets/quiz-templates/repair/repair-8.jpg
Normal file
After Width: | Height: | Size: 580 KiB |
BIN
src/assets/quiz-templates/repair/repair-9.jpg
Normal file
After Width: | Height: | Size: 826 KiB |
BIN
src/assets/quiz-templates/research/research-1.jpg
Normal file
After Width: | Height: | Size: 108 KiB |
BIN
src/assets/quiz-templates/research/research-10.jpg
Normal file
After Width: | Height: | Size: 2.0 MiB |
BIN
src/assets/quiz-templates/research/research-2.jpg
Normal file
After Width: | Height: | Size: 369 KiB |
BIN
src/assets/quiz-templates/research/research-3.jpg
Normal file
After Width: | Height: | Size: 834 KiB |
BIN
src/assets/quiz-templates/research/research-4.jpg
Normal file
After Width: | Height: | Size: 112 KiB |