Compare commits

..

No commits in common. "main" and "statistics_bugs" have entirely different histories.

20 changed files with 4576 additions and 3395 deletions

@ -6,32 +6,20 @@ on:
types: [published]
jobs:
# CreateImage:
# runs-on: [skeris]
# uses: https://gitea.pena/PenaDevops/actions.git/.gitea/workflows/build-image.yml@v1.1.6-p
# with:
# runner: skeris
# secrets:
# REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
# REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
# DeployService:
# runs-on: [frontprod]
# needs: CreateImage
# uses: https://gitea.pena/PenaDevops/actions.git/.gitea/workflows/deploy.yml@v1.1.4-p7
# with:
# runner: hubprod
# actionid: ${{ gitea.run_id }}
#
CreateImage:
runs-on: [skeris]
uses: https://gitea.pena/PenaDevops/actions.git/.gitea/workflows/build-image.yml@v1.1.6-p
with:
runner: skeris
secrets:
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
DeployService:
if: contains(github.event.package.name, 'main')
runs-on: [frontprod]
container:
image: gitea.pena/penadevops/container-images/node-compose:main
env:
GITHUB_RUN_NUMBER: "${{ inputs.actionid }}"
volumes:
- /run/user/1000/podman/podman.sock:/run/user/1000/podman/podman.sock
steps:
- name: Check out repository code
uses: http://gitea.pena/PenaDevops/actions.git/checkout@v1
- run: compose -f deployments/main/docker-compose.yaml up -d
needs: CreateImage
uses: https://gitea.pena/PenaDevops/actions.git/.gitea/workflows/deploy.yml@v1.1.4-p7
with:
runner: hubprod
actionid: ${{ gitea.run_id }}

@ -2,7 +2,6 @@ services:
squiz:
container_name: squiz
restart: unless-stopped
image: gitea.pena/squiz/frontpanel/main:latest
image: gitea.pena/squiz/frontpanel/main:$GITHUB_RUN_NUMBER
hostname: squiz
tty: true
pull_policy: always

2669
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -89,10 +89,12 @@
"devDependencies": {
"@emoji-mart/data": "^1.2.1",
"@emoji-mart/react": "^1.1.1",
"@types/cypress": "^1.1.6",
"@types/cytoscape-popper": "^2.0.4",
"@types/react-beautiful-dnd": "^13.1.4",
"@types/react-cytoscapejs": "^1.2.4",
"craco-alias": "^3.0.1",
"cypress": "^14.4.1",
"husky": "^8.0.3",
"lint-staged": "^15.2.0",
"prettier": "^3.1.1"

@ -277,10 +277,31 @@ export default function App() {
path="/gallery"
element={<LazyLoading children={<QuizGallery />} />}
/>
<Route
path="/list"
element={
<LazyLoading
children={<MyQuizzesFull />}
fallback={<ListPageDummy />}
/>
}
/>
<Route
path={"/view/:quizId"}
element={<LazyLoading children={<ViewPage />} />}
/>
<Route
path={"/tariffs"}
element={<LazyLoading children={<Tariffs />} />}
/>
<Route
path={"/analytics"}
element={<LazyLoading children={<Analytics />} />}
/>
<Route
path={"/results/:quizId"}
element={<LazyLoading children={<QuizAnswersPage />} />}
/>
<Route
path={"/qaz"}
element={<LazyLoading children={<InfoPrivilege />} />}
@ -312,27 +333,6 @@ export default function App() {
}
/>
))}
<Route
path="/list"
element={
<LazyLoading
children={<MyQuizzesFull />}
fallback={<ListPageDummy />}
/>
}
/>
<Route
path={"/view/:quizId"}
element={<LazyLoading children={<ViewPage />} />}
/>
<Route
path={"/analytics"}
element={<LazyLoading children={<Analytics />} />}
/>
<Route
path={"/results/:quizId"}
element={<LazyLoading children={<QuizAnswersPage />} />}
/>
</Route>
</Routes>

@ -1,10 +0,0 @@
import { Box, SxProps } from "@mui/material";
import PostbackDefault from "./Postback";
import PostbackPC from "./PostbackPC";
export const PostbackLogo = (sx: SxProps) => (
<Box sx={{ display: "flex", alignItems: "center", gap: 1, ...sx }}>
<PostbackPC sx={{ width: "24px", height: "20px" }} />
<PostbackDefault sx={{ width: "40px", height: "8px" }} />
</Box>
);

@ -1,15 +0,0 @@
import { Box, SxProps } from "@mui/material";
export const ZapierLogo = (sx: SxProps) => (
<Box
component="img"
src="/src/assets/icons/logo/zapier.png"
alt="Zapier"
sx={{
width: "40px",
height: "40px",
objectFit: "contain",
...sx,
}}
/>
);

@ -18,7 +18,6 @@ import { ReactComponent as NextIcon } from "@icons/Analytics/next.svg";
import { ReactComponent as LeftArrowIcon } from "@icons/Analytics/leftArrow.svg";
import { ReactComponent as RightArrowIcon } from "@icons/Analytics/rightArrow.svg";
import { extractOrder } from "@utils/extractOrder";
import { parseTitle } from "../utils/parseTitle";
type AnswerProps = {
title: string;
@ -38,9 +37,7 @@ type PaginationProps = {
const Answer = ({ title, percent, highlight }: AnswerProps) => {
const theme = useTheme();
const parsedTitle = parseTitle(title);
console.log("parsedTitle: " + parsedTitle);
return (
<Box sx={{ padding: "15px 25px" }}>
<Box
@ -54,7 +51,7 @@ const Answer = ({ title, percent, highlight }: AnswerProps) => {
>
<LinearProgress
variant="determinate"
title={parsedTitle}
title={title}
value={percent}
sx={{
width: "100%",
@ -64,7 +61,7 @@ const Answer = ({ title, percent, highlight }: AnswerProps) => {
border: `1px solid ${highlight ? theme.palette.brightPurple.main : theme.palette.grey2.main}`,
"& > span": { background: highlight ? "#D9C0F9" : "#9A9AAF1A" },
"&::before": {
content: parsedTitle ? `"${parsedTitle}"` : `"Без имени"`,
content: title ? `"${title}"` : `"Без имени"`,
position: "absolute",
zIndex: 1,
left: "20px",
@ -280,12 +277,12 @@ export const Answers: FC<AnswersProps> = ({ data }) => {
? ` ${currentAnswer?.[0]}`
: "Без заголовка"}
</Typography>
{/* <ButtonBase>
<ButtonBase>
<DoubleCheckIcon />
</ButtonBase>
<ButtonBase>
<NextIcon />
</ButtonBase> */}
</ButtonBase>
</Box>
{currentAnswerExtended.map(([title, percent], index) => (
<Answer

@ -1,43 +0,0 @@
/**
* Парсит title и извлекает Description из JSON строки
* @param title - строка, которая может быть JSON с Image и Description
* @returns значение Description или "нет названия" если парсинг не удался
*/
export const parseTitle = (title: string): string => {
if (!title || typeof title !== 'string') {
return "нет названия";
}
// Убираем обратные кавычки если они есть
const cleanTitle = title.replace(/^`|`$/g, '');
// Проверяем, начинается ли строка с { и заканчивается на }
if (!cleanTitle.trim().startsWith('{') || !cleanTitle.trim().endsWith('}')) {
return title;
}
try {
// Пытаемся распарсить как JSON
const parsed = JSON.parse(cleanTitle);
console.log("parsed object:", parsed);
console.log("parsed.Image:", parsed.Image);
console.log("parsed.Description:", parsed.Description);
// Проверяем, что это объект с полями Image и Description (специфичный для вопросов типа images и varimg)
if (parsed &&
typeof parsed === 'object' &&
'Image' in parsed &&
'Description' in parsed) {
console.log("Returning Description:", parsed.Description);
return parsed.Description || "нет названия";
}
// Если это не объект с Image и Description, возвращаем исходную строку
console.log("Not Image/Description object, returning original title");
return title;
} catch (error) {
// Если парсинг не удался, возвращаем исходную строку
console.log("JSON parse error, returning original title");
return title;
}
};

@ -132,7 +132,7 @@ export const PartnersBoard: FC<PartnersBoardProps> = ({
/>
</Box>
{/* <Typography variant="h6" sx={sectionTitleStyles}>
<Typography variant="h6" sx={sectionTitleStyles}>
Автоматизация
</Typography>
<Box sx={containerStyles}>
@ -144,7 +144,7 @@ export const PartnersBoard: FC<PartnersBoardProps> = ({
setIsModalOpen={setIsPostbackModalOpen}
setCompanyName={setCompanyName}
/>
</Box> */}
</Box>
</Box>
{companyName && (

@ -26,12 +26,6 @@ export default function Component() {
const userId = useUserStore((state) => state.userId);
const location = useLocation();
console.log("HeaderLanding debug:", {
userId,
location: location.pathname,
backgroundLocation: location.state?.backgroundLocation
});
return (
<SectionStyled
tag={"header"}
@ -83,7 +77,6 @@ export default function Component() {
to={"/signin"}
state={{ backgroundLocation: location }}
variant="outlined"
onClick={() => console.log("Signin button clicked")}
sx={{
color: "black",
border: "1px solid black",

@ -23,12 +23,10 @@ type SettingTextFieldProps = {
questionId: string;
isRequired: boolean;
isAutofill: boolean;
multi: boolean;
detailedAnswer: boolean;
answerType: Answer;
};
const SettingTextField = memo<SettingTextFieldProps>(function ({ questionId, isRequired, detailedAnswer, multi, isAutofill, answerType }) {
const SettingTextField = memo<SettingTextFieldProps>(function ({ questionId, isRequired, isAutofill, answerType }) {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(790));
const isTablet = useMediaQuery(theme.breakpoints.down(900));
@ -147,21 +145,6 @@ const SettingTextField = memo<SettingTextFieldProps>(function ({ questionId, isR
});
}}
/>
<CustomCheckbox
dataCy="checkbox-optional-question"
sx={{
display: isMobile ? "flex" : "block",
mr: isMobile ? "0px" : "16px",
alignItems: isMobile ? "flex-end" : "center",
}}
label={"Многострочный ответ"}
checked={multi}
handleChange={(e) => {
updateQuestion<QuizQuestionText>(questionId, (question) => {
question.content.multi = e.target.checked;
});
}}
/>
</Box>
</Box>
);

@ -17,8 +17,6 @@ export default function SwitchTextField({ switchState = "setting", question }: P
isRequired={question.content.required}
isAutofill={question.content.autofill}
answerType={question.content.answerType}
detailedAnswer={Boolean(question.content?.detailedAnswer)}
multi={Boolean(question.content?.multi)}
/>
);
case "image":

@ -388,7 +388,7 @@ export const CardAnswer: FC<CardAnswerProps> = ({
</>
)}
{!(typeOuestion === "file" || typeOuestion === "images" || typeOuestion === "varimg") && (
<Typography sx={{ fontSize: "18px", whiteSpace: "pre-line" }}>{answer.content}</Typography>
<Typography sx={{ fontSize: "18px" }}>{answer.content}</Typography>
)}
</Box>

@ -22,7 +22,6 @@ import { makeRequest } from "@frontend/kitui";
import { setAuthToken } from "@frontend/kitui";
import { parseAxiosError } from "@utils/parse-error";
import { recoverUser } from "@api/user";
import PasswordInput from "@/ui_kit/passwordInput";
interface Values {
password: string;
}
@ -55,8 +54,6 @@ export default function RecoverPassword() {
initialValues,
validationSchema,
onSubmit: async (values, formikHelpers) => {
console.log("tokenUser", tokenUser);
if (tokenUser) {
setAuthToken(tokenUser || "");
const [_, recoverError] = await recoverUser(values.password);
@ -77,12 +74,9 @@ export default function RecoverPassword() {
},
});
useEffect(() => {
console.log("RecoverPassword useEffect - window.location.search:", window.location.search);
console.log("RecoverPassword useEffect - window.location.href:", window.location.href);
const params = new URLSearchParams(window.location.search);
const authToken = params.get("auth");
console.log("RecoverPassword useEffect - authToken:", authToken);
setTokenUser(authToken || "");
setTokenUser(authToken);
history.pushState(null, document.title, "/changepwd");
return () => {
@ -161,15 +155,13 @@ export default function RecoverPassword() {
>
Введите новый пароль
</Typography>
<PasswordInput
<InputTextfield
TextfieldProps={{
value: formik.values.password,
placeholder: "Не менее 8 символов",
placeholder: "введите пароль",
onBlur: formik.handleBlur,
error: formik.touched.password && Boolean(formik.errors.password),
helperText: formik.touched.password && formik.errors.password,
type: "password",
"data-cy": "password",
}}
onChange={formik.handleChange}
color="#F2F3F7"

@ -875,24 +875,6 @@ export default function StartPageSettings() {
Включить защиту от копирования
</Typography>
</Box>
<Box sx={{ display: "flex", gap: "20px", alignItems: "center", mt: "20px" }}>
<CustomizedSwitch
checked={quiz.config?.backBlocked}
onChange={(e) => {
updateQuiz(quiz.id, (quiz) => {
quiz.config.backBlocked = e.target.checked;
});
}}
/>
<Typography
sx={{
fontWeight: 500,
color: theme.palette.grey3.main,
}}
>
Запретить шаг назад
</Typography>
</Box>
{!isSmallMonitor &&<SwitchAI />}
</>
)}

@ -66,18 +66,7 @@ export const setUser = (user: User) =>
}),
);
export const clearUserData = () => {
console.log("clearUserData: Clearing user data");
console.log("clearUserData: Before clearing -", useUserStore.getState());
useUserStore.setState({ ...initialState });
console.log("clearUserData: After clearing -", useUserStore.getState());
// Также очищаем localStorage напрямую
localStorage.removeItem("user");
console.log("clearUserData: localStorage cleared");
};
export const clearUserData = () => useUserStore.setState({ ...initialState });
export const setUserAccount = (userAccount: OriginalUserAccount) =>
useUserStore.setState({ userAccount });

@ -1,31 +1,8 @@
import { useUserStore } from "@root/user";
import { Navigate, Outlet } from "react-router-dom";
import { useEffect } from "react";
export default function PrivateRoute() {
const user = useUserStore((state) => state.user);
const userId = useUserStore((state) => state.userId);
console.log("PrivateRoute debug:", {
user: user ? "exists" : "null",
userId: user?._id,
userIdFromStore: userId,
currentPath: window.location.pathname,
userObject: user
});
useEffect(() => {
if (!user) {
console.log("PrivateRoute: User is null, redirecting to / via useEffect");
window.location.href = "/";
}
}, [user]);
if (!user) {
console.log("PrivateRoute: User is null, showing fallback");
return <></>;
}
console.log("PrivateRoute: User exists, rendering Outlet");
return <Outlet />;
return user ? <Outlet /> : <Navigate to="/" replace />;
}

@ -2,7 +2,7 @@ import { cartApi } from "@api/cart";
import { useUserStore } from "@/stores/user";
import { enqueueSnackbar } from "notistack";
import { useEffect } from "react";
import { useNavigate, useSearchParams, useLocation } from "react-router-dom";
import { useNavigate, useSearchParams } from "react-router-dom";
import { calcTimeOfReadyPayCart, cancelPayCartProcess, startPayCartProcess, useNotEnoughMoneyAmount } from "@/stores/notEnoughMoneyAmount";
import { startCC } from "@/stores/cc";
import { setEditQuizId, setCurrentStep } from "@root/quizes/actions";
@ -17,7 +17,6 @@ import { setEditQuizId, setCurrentStep } from "@root/quizes/actions";
export const useAfterPay = () => {
const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams();
const location = useLocation();
const userId = useUserStore(store => store.userId)
const userAccount = useUserStore(state => state.userAccount);
@ -29,28 +28,8 @@ export const useAfterPay = () => {
let URLadditionalinformation = searchParams.get("additionalinformation");//его токен
useEffect(() => {
// Проверяем, открыта ли модалка восстановления пароля
const isRecoverPasswordModal = location.state?.backgroundLocation?.pathname === "/changepwd";
// Проверяем, есть ли токен восстановления пароля в URL
const hasAuthToken = searchParams.get("auth") || window.location.search.includes("auth=");
console.log("useAfterPay debug:", {
pathname: location.pathname,
backgroundLocation: location.state?.backgroundLocation,
isRecoverPasswordModal,
searchParams: window.location.search,
authToken: searchParams.get("auth"),
hasAuthToken
});
// НЕ очищаем параметры на странице восстановления пароля, когда открыты модалки или есть токен auth
if (location.pathname !== "/changepwd" && !location.state?.backgroundLocation && !isRecoverPasswordModal && !hasAuthToken) {
console.log("Очищаем параметры URL");
setSearchParams({}, { replace: true });
} else {
console.log("НЕ очищаем параметры URL");
}
if (userId && URLuserId && userId === URLuserId) {
@ -99,7 +78,7 @@ export const useAfterPay = () => {
}
}
}, [location.pathname, location.state?.backgroundLocation]);
}, []);
//Обработка необходимости купить после пополнения
useEffect(() => {

4993
yarn.lock

File diff suppressed because it is too large Load Diff