Merge remote-tracking branch 'origin/staging'

This commit is contained in:
skeris 2024-07-15 11:21:17 +03:00
commit d10df12b7e
420 changed files with 24922 additions and 20761 deletions

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

@ -1,3 +1,4 @@
version: "3"
services:
squiz:
container_name: squiz

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

@ -1,12 +0,0 @@
{
"semi": true,
"trailingComma": "es5",
"singleQuote": false,
"printWidth": 120,
"tabWidth": 2,
"useTabs": false,
"endOfLine": "auto",
"bracketSpacing": true,
"arrowParens": "always",
"jsxSingleQuote": false
}

@ -1,12 +1,16 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<head>
<meta charset="utf-8" />
<title>Pena Quiz</title>
<meta name="description" content="Веб-сервис с инструментами для повышения эффективности маркетологов." />
<meta name="keywords" content=" Экосистема маркетинговых инструментов,
<title>Pena Quiz</title>
<meta
name="description"
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.svg" type="image/svg+xml" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/favicon.png" /><!-- 180×180 -->
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" sizes="any" />
<!-- 32×32 -->
<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="theme-color" content="#000000" />
<meta http-equiv="Pragma" content="no-cache" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<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" />
<!-- Yandex.Metrika counter -->
<script type="text/javascript">
(function (m, e, t, r, i, k, a) {
m[i] = m[i] || 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");
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta http-equiv="Pragma" content="no-cache" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<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"
/>
<!-- Yandex.Metrika counter -->
<script type="text/javascript">
(function (m, e, t, r, i, k, a) {
m[i] =
m[i] ||
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
if (domain === "quiz.pena.digital") {
ym(96979576, "init", {
clickmap: true,
trackLinks: true,
accurateTrackBounce: true,
webvisor: true
});
const domain = location.hostname;
if (domain === "quiz.pena.digital") {
ym(96979576, "init", {
clickmap: true,
trackLinks: true,
accurateTrackBounce: true,
webvisor: true,
});
// <!-- Top.Mail.Ru counter -->
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");
// <!-- /Top.Mail.Ru counter -->
};
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
});
};
</script>
<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>
// <!-- Top.Mail.Ru counter -->
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");
// <!-- /Top.Mail.Ru counter -->
}
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,
});
}
</script>
<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>

@ -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) => (
<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() {
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<UserAccount>({
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<OriginalUserAccount>({
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 (
<Navigate
@ -202,39 +124,68 @@ export default function App() {
/>
);
useAfterpay();
return (
<>
{amoAccount && <AmoTokenExpiredDialog isAmoTokenExpired={amoAccount.stale} />}
<ContactFormModal />
<FloatingSupportChat />
{location.state?.backgroundLocation && (
<Routes>
<Route path="/signin" element={<SigninDialog />} />
<Route path="/signup" element={<SignupDialog />} />
<Route path="/recover" element={<Restore />} />
<Route path="/changepwd" element={<RecoverPassword />} />
<Route path="/changepwd/expired" element={<OutdatedLink />} />
<Route
path="/signin"
element={<SigninDialog />}
/>
<Route
path="/signup"
element={<SignupDialog />}
/>
<Route
path="/recover"
element={<Restore />}
/>
<Route
path="/changepwd"
element={<RecoverPassword />}
/>
<Route
path="/changepwd/expired"
element={<OutdatedLink />}
/>
</Routes>
)}
<Routes location={location.state?.backgroundLocation || location}>
<Route path="/" element={<Landing />} />
<Route
path="/"
element={<Landing />}
/>
<Route
path="/signin"
element={
<Navigate to="/" replace state={{ redirectTo: "/signin" }} />
<Navigate
to="/"
replace
state={{ redirectTo: "/signin" }}
/>
}
/>
<Route
path="/signup"
element={
<Navigate to="/" replace state={{ redirectTo: "/signup" }} />
<Navigate
to="/"
replace
state={{ redirectTo: "/signup" }}
/>
}
/>
<Route
path="/recover"
element={
<Navigate to="/" replace state={{ redirectTo: "/recover" }} />
<Navigate
to="/"
replace
state={{ redirectTo: "/recover" }}
/>
}
/>
<Route
@ -259,9 +210,18 @@ export default function App() {
/>
}
/>
<Route
path="/gallery"
element={<LazyLoading children={<QuizGallery />} />}
/>
<Route
path="/list"
element={<LazyLoading children={<MyQuizzesFull />} />}
element={
<LazyLoading
children={<MyQuizzesFull />}
fallback={<ListPageDummy />}
/>
}
/>
<Route
path={"/view/:quizId"}
@ -283,7 +243,10 @@ export default function App() {
path={"/qaz"}
element={<LazyLoading children={<InfoPrivilege />} />}
/>
<Route path={"/image/:srcImage"} element={<ChatImageNewWindow />} />
<Route
path={"/image/:srcImage"}
element={<ChatImageNewWindow />}
/>
<Route element={<PrivateRoute />}>
{routeslink.map((e, i) => (
<Route

@ -1,4 +1,6 @@
import makeRequest from "@api/makeRequest";
import { makeRequest } from "@api/makeRequest";
import { parseAxiosError } from "@utils/parse-error";
import type {
LoginRequest,
@ -6,21 +8,24 @@ import type {
RegisterRequest,
RegisterResponse,
} 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,
password: string,
phoneNumber: string,
): Promise<[RegisterResponse | null, string?]> {
): 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<LoginRequest, LoginResponse>({
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<never, void>({
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<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,
useToken: false,
withCredentials: true,
});
return [recoverResponse];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось восстановить пароль. ${error}`];
}
}
};

@ -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<never, UserAccount>({
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<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;
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];
}
};

@ -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<unknown, { Discounts: Discount[] }>(
{ method: "GET", url: `${API_URL}/user/${userId}` },
);
const { Discounts } = await makeRequest<never, { Discounts: Discount[] }>({
method: "GET",
url: `${API_URL}/user/${userId}`,
});
return [Discounts];
} catch (nativeError) {
@ -19,4 +21,4 @@ export async function getDiscounts(
return [null, `Не удалось получить скидки. ${error}`];
}
}
};

377
src/api/integration.ts Normal file

@ -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<void, AccountResponse>({
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<void, AccountResponse>({
method: "GET",
url: `${API_URL}/account`,
useToken: true,
})
);
}
// подключить 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;
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<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 QuestionID = Record<string, number>;
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<Partial<QuestionKeys>, null | [{ QuestionID: QuestionID }]>;
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 || 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<IntegrationRulesUpdate, string>({
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<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 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<PaginationRequest, CustomFieldsResponse>({
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<void>({
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<PaginationRequest, FieldsResponse>({
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 { 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<TRequest = unknown, TResponse = unknown>(
data: MakeRequest,
): Promise<TResponse> {
try {
const response = await KIT.makeRequest<unknown>(data);
type ExtendedAxiosResponse = AxiosResponse & { message: string };
export const makeRequest = async <TRequest = unknown, TResponse = unknown>(
data: MakeRequest,
): 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 (
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<TRequest = unknown, TResponse = unknown>(
clearQuizData();
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";
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}`];
}
}
};

@ -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<CreateQuestionRequest, RawQuestion>({
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<GetQuestionListRequest>) {
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<GetQuestionListRequest>,
): Promise<[RawQuestion[] | null, string?]> => {
try {
if (!body?.quiz_id) return [null, "Квиз не найден"];
function editQuestion(body: EditQuestionRequest, signal?: AbortSignal) {
return makeRequest<EditQuestionRequest, EditQuestionResponse>({
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<CopyQuestionRequest, CopyQuestionResponse>({
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<DeleteQuestionRequest, DeleteQuestionResponse>({
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,

@ -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<CreateQuizRequest>) {
return makeRequest<CreateQuizRequest, RawQuiz>({
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<GetQuizListRequest>) {
const response = await makeRequest<GetQuizListRequest, GetQuizListResponse>({
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<GetQuizRequest>) {
return makeRequest<GetQuizRequest, GetQuizResponse>({
url: `${baseUrl}/quiz/get`,
body: { ...defaultGetQuizBody, ...body },
method: "GET",
});
}
export const createQuiz = async (
body?: Partial<CreateQuizRequest>,
): Promise<[RawQuiz | null, string?]> => {
try {
const createdQuiz = await makeRequest<CreateQuizRequest, RawQuiz>({
method: "POST",
url: `${API_URL}/quiz/create`,
body: { ...defaultCreateQuizBody, ...body },
});
async function editQuiz(body: EditQuizRequest, signal?: AbortSignal) {
return makeRequest<EditQuizRequest, EditQuizResponse>({
url: `${baseUrl}/quiz/edit`,
body,
method: "PATCH",
signal,
});
}
return [createdQuiz];
} catch (nativeError) {
const [error] = parseAxiosError(nativeError);
function copyQuiz(id: number) {
return makeRequest<CopyQuizRequest, CopyQuizResponse>({
url: `${baseUrl}/quiz/copy`,
body: { id },
method: "POST",
});
}
return [null, `Не удалось создать квиз. ${error}`];
}
};
function deleteQuiz(id: number) {
return makeRequest<DeleteQuizRequest, DeleteQuizResponse>({
url: `${baseUrl}/quiz/delete`,
body: { id },
method: "DELETE",
});
}
export const getQuizList = async (
body?: Partial<CreateQuizRequest>,
): 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<FormData, { [key: string]: string }>({
url: `${imagesUrl}/quiz/putImages`,
body: formData,
method: "PUT",
});
}
export const getQuiz = async (
body?: Partial<GetQuizRequest>,
): Promise<[GetQuizResponse | null, string?]> => {
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 = {
create: createQuiz,
@ -83,6 +194,7 @@ export const quizApi = {
copy: copyQuiz,
delete: deleteQuiz,
addImages: addQuizImages,
copyTemplate: copyQuizTemplate,
};
const defaultCreateQuizBody: CreateQuizRequest = {

@ -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<IResultListBody, RawResult>({
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<unknown, unknown>({
url: process.env.REACT_APP_DOMAIN + `/squiz/results/delete/${resultId}`,
body: {},
method: "DELETE",
});
}
type ObsolescenceRequest = {
answers: number[];
};
function obsolescenceResult(idResultArray: number[]) {
return makeRequest<unknown, unknown>({
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<unknown, IAnswerResult[]>({
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<IResultListBody, RawResult>({
method: "POST",
url: `${API_URL}/results/getResults/${quizId}`,
body: { page: page, limit: 10, ...body },
});
function AnswerResultListEx(quizId: number, body: any) {
return makeRequest<unknown, unknown>({
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<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 = {
getList: getResultList,

@ -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<string, number>;
OS: Record<string, number>;
@ -23,12 +21,15 @@ export type QuestionsResponse = {
Results: Record<string, number>;
Questions: Record<string, Record<string, number>>;
};
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<TRequest, DevicesResponse>({
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<TRequest, GeneralResponse>({
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<TRequest, QuestionsResponse>({
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<TRequest, QuestionsResponse>({
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}`];
}
};

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 { 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<FormData, SendFileResponse>({
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}`];
}
};

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/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<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}`];
}
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

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

@ -1,41 +0,0 @@
import { Box } from "@mui/material";
interface Props {
color: string;
}
export default function NumberThree({ color }: Props) {
return (
<Box
sx={{
height: "30px",
width: "30px",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<svg
width="24"
height="25"
viewBox="0 0 24 25"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 21.1875C16.9706 21.1875 21 17.1581 21 12.1875C21 7.21694 16.9706 3.1875 12 3.1875C7.02944 3.1875 3 7.21694 3 12.1875C3 17.1581 7.02944 21.1875 12 21.1875Z"
stroke={color}
strokeWidth="1.5"
strokeMiterlimit="10"
/>
<path
d="M9.75 8.0625H14.25L11.625 11.8125C12.0567 11.8125 12.4817 11.919 12.8624 12.1225C13.243 12.326 13.5677 12.6203 13.8075 12.9792C14.0473 13.3381 14.1949 13.7507 14.2372 14.1803C14.2795 14.6099 14.2152 15.0433 14.05 15.4421C13.8848 15.8409 13.6238 16.1928 13.2901 16.4666C12.9564 16.7405 12.5603 16.9278 12.137 17.0121C11.7136 17.0963 11.276 17.0748 10.8629 16.9495C10.4498 16.8242 10.074 16.599 9.76875 16.2937"
stroke={color}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</Box>
);
}

@ -1,41 +0,0 @@
import { Box } from "@mui/material";
interface Props {
color: string;
}
export default function NumberTwo({ color }: Props) {
return (
<Box
sx={{
height: "30px",
width: "30px",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<svg
width="24"
height="25"
viewBox="0 0 24 25"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 21.1875C16.9706 21.1875 21 17.1581 21 12.1875C21 7.21694 16.9706 3.1875 12 3.1875C7.02944 3.1875 3 7.21694 3 12.1875C3 17.1581 7.02944 21.1875 12 21.1875Z"
stroke={color}
strokeWidth="1.5"
strokeMiterlimit="10"
/>
<path
d="M9.92813 9.06402C10.1303 8.58652 10.4913 8.19352 10.9499 7.9515C11.4085 7.70948 11.9366 7.6333 12.4449 7.73584C12.9533 7.83839 13.4106 8.11336 13.7395 8.51426C14.0684 8.91515 14.2487 9.41735 14.25 9.9359C14.252 10.3839 14.118 10.822 13.8656 11.1921V11.1921L9.75 16.6859H14.25"
stroke={color}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</Box>
);
}

@ -1,41 +0,0 @@
import { Box } from "@mui/material";
interface Props {
color: string;
}
export default function OneIconBorder({ color }: Props) {
return (
<Box
sx={{
height: "30px",
width: "30px",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 21C16.9706 21 21 16.9706 21 12C21 7.02944 16.9706 3 12 3C7.02944 3 3 7.02944 3 12C3 16.9706 7.02944 21 12 21Z"
stroke={color}
strokeWidth="1.5"
strokeMiterlimit="10"
/>
<path
d="M10.125 9.375L12.375 7.875V16.5"
stroke={color}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</Box>
);
}

@ -0,0 +1,22 @@
import { FC } from "react";
import { Box } from "@mui/material";
export const AlignIcon: FC = () => (
<Box sx={{ width: `20px`, height: "20px" }}>
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19 8.18348L11.8 8.22299V1M8.2 19V11.8165L1 11.7752"
stroke="white"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</Box>
);

File diff suppressed because one or more lines are too long

@ -0,0 +1,29 @@
import { FC } from "react";
import { Box } from "@mui/material";
export const ExpandIcon: FC = () => (
<Box sx={{ width: `24px`, height: "24px" }}>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M18 6.58545L12.4984 2.17075C12.434 2.11677 12.3566 2.07382 12.271 2.04447C12.1853 2.01513 12.0931 2 12 2C11.9069 2 11.8147 2.01513 11.729 2.04447C11.6434 2.07382 11.566 2.11677 11.5016 2.17075L6 6.58545M18 17.4146L12.4984 21.8293C12.434 21.8832 12.3566 21.9262 12.271 21.9555C12.1853 21.9849 12.0931 22 12 22C11.9069 22 11.8147 21.9849 11.729 21.9555C11.6434 21.9262 11.566 21.8832 11.5016 21.8293L6 17.4146"
stroke="white"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M11.9988 14.0054C13.0682 14.0054 13.9351 13.1385 13.9351 12.0691C13.9351 10.9997 13.0682 10.1328 11.9988 10.1328C10.9294 10.1328 10.0625 10.9997 10.0625 12.0691C10.0625 13.1385 10.9294 14.0054 11.9988 14.0054Z"
stroke="white"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</Box>
);

@ -0,0 +1,29 @@
import { FC } from "react";
import { Box } from "@mui/material";
export const GrayPlus: FC = () => (
<Box sx={{ width: `32px` }}>
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M16.0029 1V31"
stroke="#9A9AAF"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M31 15.9941L1 15.9941"
stroke="#9A9AAF"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</Box>
);

@ -0,0 +1,38 @@
import { FC } from "react";
import { Box } from "@mui/material";
export const RoundedCheckedIcon: FC = () => (
<Box
sx={{
width: `26px`,
height: "26px",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<svg
width="26"
height="27"
viewBox="0 0 26 27"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect
x="0.5"
y="1"
width="25"
height="25"
rx="12.5"
fill="#F2F3F7"
stroke="#F2F3F7"
/>
<path
d="M8 13.5L12.2857 17.5L18 10"
stroke="#9A9AAF"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</Box>
);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 861 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 514 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 642 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 667 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 600 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 618 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 600 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 645 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 847 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 746 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 431 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 752 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 424 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 584 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 517 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 719 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 870 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 516 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 522 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 572 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 758 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1013 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 436 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 571 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 669 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 425 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 865 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 378 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 568 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 550 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 512 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 410 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 351 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 658 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 297 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 698 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 356 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 788 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 533 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 656 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 352 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 972 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 488 KiB

Some files were not shown because too many files have changed in this diff Show More