Merge branch 'staging'
This commit is contained in:
commit
7e9613d975
@ -1,2 +0,0 @@
|
|||||||
1.0.1 Страница заявок корректно отображает мультиответ
|
|
||||||
1.0.0 Добавлены фичи "мультиответ", "перенос строки в своём ответе", "свой ответ", "плейсхолдер своего ответа"
|
|
||||||
19
.husky/pre-commit
Normal file → Executable file
19
.husky/pre-commit
Normal file → Executable file
@ -1,4 +1,21 @@
|
|||||||
#!/usr/bin/env sh
|
#!/usr/bin/env sh
|
||||||
. "$(dirname -- "$0")/_/husky.sh"
|
. "$(dirname -- "$0")/_/husky.sh"
|
||||||
|
|
||||||
yarn lint-staged --allow-empty
|
# yarn lint-staged --allow-empty
|
||||||
|
|
||||||
|
if [ "$HUSKY_SKIP_CHANGELOG" != "1" ]; then # HUSKY_SKIP_CHANGELOG=1 git commit -m "--"
|
||||||
|
|
||||||
|
# записываем в changelog инфо о коммите
|
||||||
|
bash .husky/scripts/update-changelog.sh
|
||||||
|
|
||||||
|
# Проверяем, изменился ли CHANGELOG.md
|
||||||
|
if git diff --name-only | grep -q "CHANGELOG.md"; then
|
||||||
|
# Добавляем CHANGELOG.md в staging, если он был изменен
|
||||||
|
git add CHANGELOG.md
|
||||||
|
echo "CHANGELOG.md updated and added to staging"
|
||||||
|
else
|
||||||
|
echo "CHANGELOG.md not modified"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Skipping changelog update (HUSKY_SKIP_CHANGELOG=1)"
|
||||||
|
fi
|
||||||
64
.husky/scripts/update-changelog.sh
Normal file
64
.husky/scripts/update-changelog.sh
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Проверяем, что мы в ветке staging
|
||||||
|
CURRENT_BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null)
|
||||||
|
|
||||||
|
if [ "$CURRENT_BRANCH" != "staging" ]; then
|
||||||
|
echo "Not in staging branch, skipping changelog update"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Получаем последний commit message
|
||||||
|
COMMIT_MESSAGE=$(git log -1 --pretty=%B 2>/dev/null | head -1)
|
||||||
|
|
||||||
|
if [ -z "$COMMIT_MESSAGE" ]; then
|
||||||
|
echo "No commit message found"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Получаем текущую дату
|
||||||
|
CURRENT_DATE=$(date +%Y-%m-%d)
|
||||||
|
CHANGELOG_FILE="CHANGELOG.md"
|
||||||
|
|
||||||
|
# Определяем новую версию
|
||||||
|
if [ -f "$CHANGELOG_FILE" ]; then
|
||||||
|
# Ищем последнюю версию в формате X.X.X (без v и ##)
|
||||||
|
LAST_VERSION=$(grep -E '^[0-9]+\.[0-9]+\.[0-9]+' "$CHANGELOG_FILE" | head -1 | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+')
|
||||||
|
|
||||||
|
if [ -n "$LAST_VERSION" ]; then
|
||||||
|
# Увеличиваем patch версию (третью цифру)
|
||||||
|
IFS='.' read -r MAJOR MINOR PATCH <<< "$LAST_VERSION"
|
||||||
|
NEW_VERSION="$MAJOR.$MINOR.$((PATCH + 1))"
|
||||||
|
echo "Found last version: $LAST_VERSION, new version: $NEW_VERSION"
|
||||||
|
else
|
||||||
|
# Если не нашли версию, начинаем с 1.0.1
|
||||||
|
NEW_VERSION="1.0.1"
|
||||||
|
echo "No version found, starting from: $NEW_VERSION"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
NEW_VERSION="1.0.0"
|
||||||
|
echo "CHANGELOG.md not found, starting from: $NEW_VERSION"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Создаем временный файл
|
||||||
|
TEMP_FILE=$(mktemp)
|
||||||
|
|
||||||
|
# Добавляем новую запись БЕЗ ##, БЕЗ v, БЕЗ переноса строки
|
||||||
|
echo "${NEW_VERSION} _ ${CURRENT_DATE} _ ${COMMIT_MESSAGE}" > "$TEMP_FILE"
|
||||||
|
|
||||||
|
# Добавляем существующее содержимое БЕЗ пустых строк между записями
|
||||||
|
if [ -f "$CHANGELOG_FILE" ]; then
|
||||||
|
# Убираем пустые строки между записями и добавляем содержимое
|
||||||
|
awk 'NF' "$CHANGELOG_FILE" >> "$TEMP_FILE"
|
||||||
|
else
|
||||||
|
# Создаем базовую структуру для нового файла
|
||||||
|
echo "# Changelog" >> "$TEMP_FILE"
|
||||||
|
echo "" >> "$TEMP_FILE"
|
||||||
|
echo "All notable changes to this project will be documented in this file." >> "$TEMP_FILE"
|
||||||
|
echo "" >> "$TEMP_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Заменяем оригинальный файл
|
||||||
|
mv "$TEMP_FILE" "$CHANGELOG_FILE"
|
||||||
|
|
||||||
|
echo "CHANGELOG.md updated to version ${NEW_VERSION}"
|
||||||
12
CHANGELOG.md
12
CHANGELOG.md
@ -0,0 +1,12 @@
|
|||||||
|
1.0.11 _ 2025-10-06 _ Merge branch 'staging'
|
||||||
|
1.0.10 _ 2025-10-05 _ utm
|
||||||
|
1.0.9 _ 2025-10-05 _ utm
|
||||||
|
1.0.8 _ 2025-10-05 _ замена инпут на текстареа
|
||||||
|
1.0.7 _ 2025-10-05 _ замена инпут на текстареа
|
||||||
|
1.0.6 _ 2025-09-19 _ логика включения таймера
|
||||||
|
1.0.5 _ 2025-09-18 _ особые условия для вывода картинок
|
||||||
|
1.0.4 _ 2025-09-14 _ особые условия для вывода картинок
|
||||||
|
1.0.3 _ 2025-09-12 _ среднее время не учитывает нули
|
||||||
|
1.0.2 _ 2025-09-07 _ добавлена автозапись в стейджинг
|
||||||
|
1.0.1 Страница заявок корректно отображает мультиответ
|
||||||
|
1.0.0 Добавлены фичи "мультиответ", "перенос строки в своём ответе", "свой ответ", "плейсхолдер своего ответа"
|
||||||
101
src/api/leadtarget.ts
Normal file
101
src/api/leadtarget.ts
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import { makeRequest } from "@frontend/kitui";
|
||||||
|
import { parseAxiosError } from "@utils/parse-error";
|
||||||
|
|
||||||
|
export type LeadTargetType = "mail" | "telegram" | "whatsapp" | "webhook";
|
||||||
|
|
||||||
|
export interface LeadTargetModel {
|
||||||
|
id: number;
|
||||||
|
accountID: string;
|
||||||
|
type: LeadTargetType;
|
||||||
|
quizID: number;
|
||||||
|
target: string; // содержит подстроку "zapier" или "postback"
|
||||||
|
inviteLink?: string;
|
||||||
|
deleted?: boolean;
|
||||||
|
createdAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const API_URL = `${process.env.REACT_APP_DOMAIN}/squiz`;
|
||||||
|
|
||||||
|
export const getLeadTargetsByQuiz = async (
|
||||||
|
quizId: number,
|
||||||
|
): Promise<[LeadTargetModel[] | null, string?]> => {
|
||||||
|
try {
|
||||||
|
const items = await makeRequest<unknown, LeadTargetModel[]>({
|
||||||
|
method: "GET",
|
||||||
|
url: `${API_URL}/account/leadtarget/${quizId}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
return [items];
|
||||||
|
} catch (nativeError) {
|
||||||
|
const [error] = parseAxiosError(nativeError);
|
||||||
|
return [null, `Не удалось получить цели лида. ${error}`];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createLeadTarget = async (
|
||||||
|
body: {
|
||||||
|
type: LeadTargetType;
|
||||||
|
quizID: number;
|
||||||
|
target: string;
|
||||||
|
name?: string;
|
||||||
|
},
|
||||||
|
): Promise<[LeadTargetModel | true | null, string?]> => {
|
||||||
|
try {
|
||||||
|
const response = await makeRequest<typeof body, LeadTargetModel | true>({
|
||||||
|
method: "POST",
|
||||||
|
url: `${API_URL}/account/leadtarget`,
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
|
||||||
|
return [response];
|
||||||
|
} catch (nativeError) {
|
||||||
|
const [error] = parseAxiosError(nativeError);
|
||||||
|
return [null, `Не удалось создать цель лида. ${error}`];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateLeadTarget = async (
|
||||||
|
body: {
|
||||||
|
id: number;
|
||||||
|
target: string;
|
||||||
|
},
|
||||||
|
): Promise<[LeadTargetModel | null, string?]> => {
|
||||||
|
try {
|
||||||
|
const updated = await makeRequest<typeof body, LeadTargetModel>({
|
||||||
|
method: "PUT",
|
||||||
|
url: `${API_URL}/account/leadtarget`,
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
|
||||||
|
return [updated];
|
||||||
|
} catch (nativeError) {
|
||||||
|
const [error] = parseAxiosError(nativeError);
|
||||||
|
return [null, `Не удалось обновить цель лида. ${error}`];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteLeadTarget = async (
|
||||||
|
id: number,
|
||||||
|
): Promise<[true | null, string?]> => {
|
||||||
|
try {
|
||||||
|
await makeRequest<unknown, unknown>({
|
||||||
|
method: "DELETE",
|
||||||
|
url: `${API_URL}/account/leadtarget/${id}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
return [true];
|
||||||
|
} catch (nativeError) {
|
||||||
|
const [error] = parseAxiosError(nativeError);
|
||||||
|
return [null, `Не удалось удалить цель лида. ${error}`];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const leadTargetApi = {
|
||||||
|
getByQuiz: getLeadTargetsByQuiz,
|
||||||
|
create: createLeadTarget,
|
||||||
|
update: updateLeadTarget,
|
||||||
|
delete: deleteLeadTarget,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
13
src/assets/icons/OrangeYoutube.tsx
Executable file
13
src/assets/icons/OrangeYoutube.tsx
Executable file
@ -0,0 +1,13 @@
|
|||||||
|
import { Box, SxProps } from "@mui/material";
|
||||||
|
|
||||||
|
export default function OrangeYoutube(sx: SxProps) {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={sx}
|
||||||
|
>
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M19.9756 4.36328C21.0799 4.36346 21.9754 5.25898 21.9756 6.36328V17.6367C21.9754 18.741 21.0799 19.6365 19.9756 19.6367H4.02734C2.92289 19.6367 2.02754 18.7411 2.02734 17.6367V6.36328C2.02754 5.25887 2.92289 4.36328 4.02734 4.36328H19.9756ZM10.2227 8.18262C10.1533 8.17891 10.0838 8.19583 10.0225 8.23145C9.9614 8.26705 9.9107 8.32013 9.875 8.38477C9.83921 8.44959 9.81946 8.52397 9.81934 8.59961V15.4004C9.81946 15.476 9.83921 15.5504 9.875 15.6152C9.9107 15.6799 9.9614 15.7329 10.0225 15.7686C10.0838 15.8042 10.1533 15.8211 10.2227 15.8174C10.292 15.8137 10.3592 15.79 10.417 15.748L15.1025 12.3477C15.1552 12.3095 15.1986 12.258 15.2285 12.1973C15.2584 12.1366 15.2744 12.0689 15.2744 12C15.2744 11.9311 15.2584 11.8634 15.2285 11.8027C15.1986 11.742 15.1552 11.6905 15.1025 11.6523L10.417 8.25195C10.3592 8.21003 10.292 8.18633 10.2227 8.18262Z" fill="#FA590B" />
|
||||||
|
</svg>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -18,6 +18,7 @@ import SectionWrapper from "@ui_kit/SectionWrapper";
|
|||||||
import { General } from "./General";
|
import { General } from "./General";
|
||||||
import { AnswersStatistics } from "./Answers/AnswersStatistics";
|
import { AnswersStatistics } from "./Answers/AnswersStatistics";
|
||||||
import { Devices } from "./Devices";
|
import { Devices } from "./Devices";
|
||||||
|
import AnalyticsSkeleton from "./AnalyticsSkeleton";
|
||||||
|
|
||||||
import { setQuizes } from "@root/quizes/actions";
|
import { setQuizes } from "@root/quizes/actions";
|
||||||
import { useQuizStore } from "@root/quizes/store";
|
import { useQuizStore } from "@root/quizes/store";
|
||||||
@ -32,11 +33,17 @@ import { ReactComponent as ResetIcon } from "@icons/Analytics/reset.svg";
|
|||||||
import type { Moment } from "moment";
|
import type { Moment } from "moment";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import type { Quiz } from "@model/quiz/quiz";
|
import type { Quiz } from "@model/quiz/quiz";
|
||||||
|
import { useCurrentQuiz } from "@/stores/quizes/hooks";
|
||||||
|
import { useQuestions } from "@/stores/questions/hooks";
|
||||||
|
|
||||||
export default function Analytics() {
|
export default function Analytics() {
|
||||||
const { quizes, editQuizId } = useQuizStore();
|
|
||||||
|
|
||||||
const [quiz, setQuiz] = useState<Quiz>({} as Quiz);
|
|
||||||
|
const quiz = useCurrentQuiz();
|
||||||
|
const globalQuestions = useQuestions({quizId: quiz?.backendId}).questions;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const [isOpen, setOpen] = useState<boolean>(false);
|
const [isOpen, setOpen] = useState<boolean>(false);
|
||||||
const [isOpenEnd, setOpenEnd] = useState<boolean>(false);
|
const [isOpenEnd, setOpenEnd] = useState<boolean>(false);
|
||||||
const [from, setFrom] = useState<Moment | null>(null);
|
const [from, setFrom] = useState<Moment | null>(null);
|
||||||
@ -45,30 +52,25 @@ export default function Analytics() {
|
|||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down(600));
|
const isMobile = useMediaQuery(theme.breakpoints.down(600));
|
||||||
|
|
||||||
const { devices, general, questions } = useAnalytics({
|
const { devices, general, questions, isLoading } = useAnalytics({
|
||||||
ready: Boolean(Object.keys(quiz).length),
|
ready: quiz ? Boolean(Object.keys(quiz).length) : false,
|
||||||
quizId: editQuizId?.toString() || "",
|
quizId: quiz?.backendId?.toString() || "",
|
||||||
from,
|
from,
|
||||||
to,
|
to,
|
||||||
});
|
});
|
||||||
|
|
||||||
const resetTime = () => {
|
const resetTime = () => {
|
||||||
setFrom(moment(new Date(quiz.created_at)));
|
setFrom(moment(new Date(quiz?.created_at)));
|
||||||
setTo(moment().add(1, "days"));
|
setTo(moment().add(1, "days"));
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (quizes.length > 0) {
|
if (quiz) setFrom(moment(new Date(quiz?.created_at)));
|
||||||
const quiz = quizes.find((q) => q.backendId === editQuizId);
|
}, [quiz]);
|
||||||
if (quiz === undefined) throw new Error("Не удалось получить квиз");
|
|
||||||
setQuiz(quiz);
|
|
||||||
setFrom(moment(new Date(quiz.created_at)));
|
|
||||||
}
|
|
||||||
}, [quizes]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const getData = async (): Promise<void> => {
|
const getData = async (): Promise<void> => {
|
||||||
if (editQuizId !== null) {
|
if (quiz?.backendId !== null) {
|
||||||
const [gottenQuizes, gottenQuizesError] = await quizApi.getList();
|
const [gottenQuizes, gottenQuizesError] = await quizApi.getList();
|
||||||
|
|
||||||
if (gottenQuizesError) {
|
if (gottenQuizesError) {
|
||||||
@ -85,8 +87,8 @@ export default function Analytics() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (editQuizId === undefined) redirect("/list");
|
if (quiz?.backendId === undefined) redirect("/list");
|
||||||
}, [editQuizId]);
|
}, [quiz?.backendId]);
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
@ -255,12 +257,18 @@ export default function Analytics() {
|
|||||||
{isMobile ? <ResetIcon /> : "Сбросить"}
|
{isMobile ? <ResetIcon /> : "Сбросить"}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
{isLoading ? (
|
||||||
|
<AnalyticsSkeleton />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<General
|
<General
|
||||||
data={general}
|
data={general}
|
||||||
day={86400 - moment(to).unix() - moment(from).unix() > 0}
|
day={86400 - moment(to).unix() - moment(from).unix() > 0}
|
||||||
/>
|
/>
|
||||||
<AnswersStatistics data={questions} />
|
<AnswersStatistics data={questions} globalQuestions={globalQuestions}/>
|
||||||
<Devices data={devices} />
|
<Devices data={devices} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</SectionWrapper>
|
</SectionWrapper>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
102
src/pages/Analytics/AnalyticsSkeleton.tsx
Normal file
102
src/pages/Analytics/AnalyticsSkeleton.tsx
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import { Box, Paper, Skeleton, Typography, useMediaQuery, useTheme } from "@mui/material";
|
||||||
|
|
||||||
|
export default function AnalyticsSkeleton() {
|
||||||
|
const theme = useTheme();
|
||||||
|
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
|
||||||
|
const isMobile = useMediaQuery(theme.breakpoints.down(700));
|
||||||
|
|
||||||
|
const card = (
|
||||||
|
<Paper
|
||||||
|
sx={{
|
||||||
|
overflow: "hidden",
|
||||||
|
borderRadius: "12px",
|
||||||
|
boxShadow: "0 0 20px rgba(0, 0, 0, 0.15)",
|
||||||
|
padding: "20px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Skeleton variant="text" sx={{ width: "40%", height: 28, mb: 1 }} />
|
||||||
|
<Skeleton variant="text" sx={{ width: "20%", height: 24, mb: 2 }} />
|
||||||
|
<Skeleton variant="rectangular" sx={{ width: "100%", height: 220, borderRadius: "8px" }} />
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Box sx={{ marginTop: "45px" }}>
|
||||||
|
<Typography component="h3" sx={{ fontSize: "24px", fontWeight: "bold", color: theme.palette.text.primary }}>
|
||||||
|
Ключевые метрики
|
||||||
|
</Typography>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: isTablet ? (isMobile ? "1fr" : "1fr 1fr") : "1fr 1fr 1fr",
|
||||||
|
gap: "20px",
|
||||||
|
marginTop: "40px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{card}
|
||||||
|
{card}
|
||||||
|
{card}
|
||||||
|
{card}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ marginTop: "120px" }}>
|
||||||
|
<Typography component="h3" sx={{ fontSize: "24px", fontWeight: "bold", color: theme.palette.text.primary }}>
|
||||||
|
Статистика по ответам
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ display: isTablet && !isMobile ? "flex" : "block", gap: "40px", mt: "20px" }}>
|
||||||
|
<Paper sx={{ borderRadius: "12px", boxShadow: "0 0 20px rgba(0, 0, 0, 0.15)", p: 2, mb: 2 }}>
|
||||||
|
<Skeleton variant="text" sx={{ width: "60%", height: 28, mb: 1 }} />
|
||||||
|
<Skeleton variant="rectangular" sx={{ width: "100%", height: 260, borderRadius: "8px" }} />
|
||||||
|
</Paper>
|
||||||
|
<Paper sx={{ borderRadius: "12px", boxShadow: "0 0 20px rgba(0, 0, 0, 0.15)", p: 2 }}>
|
||||||
|
<Skeleton variant="text" sx={{ width: "60%", height: 28, mb: 1 }} />
|
||||||
|
<Skeleton variant="rectangular" sx={{ width: "100%", height: 260, borderRadius: "8px" }} />
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ mt: "30px" }}>
|
||||||
|
<Skeleton variant="text" sx={{ width: 300, height: 28, mb: 2 }} />
|
||||||
|
<Paper sx={{ borderRadius: "12px", boxShadow: "0 0 20px rgba(0, 0, 0, 0.15)", p: 2 }}>
|
||||||
|
{[...Array(4)].map((_, idx) => (
|
||||||
|
<Box key={idx} sx={{ p: "15px 25px" }}>
|
||||||
|
<Skeleton variant="rectangular" sx={{ width: "100%", height: 44, borderRadius: "10px" }} />
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ marginTop: "120px" }}>
|
||||||
|
<Typography component="h3" sx={{ fontSize: "24px", fontWeight: "bold", color: theme.palette.text.primary }}>
|
||||||
|
Статистика пользователей
|
||||||
|
</Typography>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: isTablet ? (isMobile ? "1fr" : "1fr 1fr") : "1fr 1fr 1fr",
|
||||||
|
gap: "20px",
|
||||||
|
marginTop: "30px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{[...Array(3)].map((_, i) => (
|
||||||
|
<Paper key={i} sx={{ overflow: "hidden", minHeight: "500px", display: "flex", flexDirection: "column", gap: "30px", borderRadius: "12px", boxShadow: "0 0 20px rgba(0, 0, 0, 0.15)", p: 2 }}>
|
||||||
|
<Skeleton variant="text" sx={{ width: "50%", height: 28 }} />
|
||||||
|
<Skeleton variant="circular" width={245} height={245} sx={{ alignSelf: "center" }} />
|
||||||
|
<Box sx={{ background: theme.palette.background.default, padding: "20px" }}>
|
||||||
|
{[...Array(4)].map((_, idx) => (
|
||||||
|
<Box key={idx} sx={{ display: "flex", mb: "10px" }}>
|
||||||
|
<Skeleton variant="text" sx={{ flexGrow: 1, height: 20 }} />
|
||||||
|
<Skeleton variant="text" sx={{ width: 60, height: 20, ml: 2 }} />
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -3,6 +3,7 @@ import type { PaginationRenderItemParams } from "@mui/material";
|
|||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
ButtonBase,
|
ButtonBase,
|
||||||
|
IconButton,
|
||||||
Input,
|
Input,
|
||||||
LinearProgress,
|
LinearProgress,
|
||||||
Pagination as MuiPagination,
|
Pagination as MuiPagination,
|
||||||
@ -19,6 +20,10 @@ import { ReactComponent as LeftArrowIcon } from "@icons/Analytics/leftArrow.svg"
|
|||||||
import { ReactComponent as RightArrowIcon } from "@icons/Analytics/rightArrow.svg";
|
import { ReactComponent as RightArrowIcon } from "@icons/Analytics/rightArrow.svg";
|
||||||
import { extractOrder } from "@utils/extractOrder";
|
import { extractOrder } from "@utils/extractOrder";
|
||||||
import { parseTitle } from "../utils/parseTitle";
|
import { parseTitle } from "../utils/parseTitle";
|
||||||
|
import { AnyTypedQuizQuestion } from "@frontend/squzanswerer";
|
||||||
|
import InsertDriveFileIcon from '@mui/icons-material/InsertDriveFile';
|
||||||
|
import { timewebContent, timewebContentFile } from "@/pages/QuizAnswersPage/cardAnswers/helper";
|
||||||
|
import { useCurrentQuiz } from "@/stores/quizes/hooks";
|
||||||
|
|
||||||
type AnswerProps = {
|
type AnswerProps = {
|
||||||
title: string;
|
title: string;
|
||||||
@ -28,6 +33,7 @@ type AnswerProps = {
|
|||||||
|
|
||||||
type AnswersProps = {
|
type AnswersProps = {
|
||||||
data: Record<string, Record<string, number>> | null;
|
data: Record<string, Record<string, number>> | null;
|
||||||
|
globalQuestions: AnyTypedQuizQuestion[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type PaginationProps = {
|
type PaginationProps = {
|
||||||
@ -40,9 +46,13 @@ const Answer = ({ title, percent, highlight }: AnswerProps) => {
|
|||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const parsedTitle = parseTitle(title);
|
const parsedTitle = parseTitle(title);
|
||||||
|
|
||||||
console.log("parsedTitle: " + parsedTitle);
|
console.log("Привет, я Answer. И вот что я о себе знаю:")
|
||||||
|
console.log("-------------------------------------------------------------------")
|
||||||
|
console.log("{ title, percent, highlight }")
|
||||||
|
console.log({ title, percent, highlight })
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ padding: "15px 25px" }}>
|
<Box sx={{ padding: "15px 25px", width: "100%" }}>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
position: "relative",
|
position: "relative",
|
||||||
@ -200,7 +210,8 @@ const Pagination = ({ page, setPage, pagesAmount }: PaginationProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Answers: FC<AnswersProps> = ({ data }) => {
|
export const Answers: FC<AnswersProps> = ({ data, globalQuestions }) => {
|
||||||
|
const quiz = useCurrentQuiz();
|
||||||
const [page, setPage] = useState<number>(1);
|
const [page, setPage] = useState<number>(1);
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const answers = useMemo(() => {
|
const answers = useMemo(() => {
|
||||||
@ -210,10 +221,25 @@ export const Answers: FC<AnswersProps> = ({ data }) => {
|
|||||||
);
|
);
|
||||||
}, [data]);
|
}, [data]);
|
||||||
const currentAnswer = answers[page - 1];
|
const currentAnswer = answers[page - 1];
|
||||||
|
const globalQuestion = globalQuestions.find(e => {
|
||||||
|
console.log("---")
|
||||||
|
console.log("Привет, я ищу глоб вопро ", currentAnswer[0].slice(0, -4))
|
||||||
|
console.log(" c ", e.title)
|
||||||
|
console.log("---")
|
||||||
|
console.log(e.title === currentAnswer[0].slice(0, -4))
|
||||||
|
return e.title === currentAnswer[0].slice(0, -4)
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("globalQuestionglobalQuestionglobalQuestionglobalQuestionglobalQuestionglobalQuestion")
|
||||||
|
console.log(globalQuestion)
|
||||||
|
|
||||||
const percentsSum = Object.values(currentAnswer?.[1] ?? {}).reduce(
|
const percentsSum = Object.values(currentAnswer?.[1] ?? {}).reduce(
|
||||||
(total, item) => (total += item),
|
(total, item) => (total += item),
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
console.log("currentAnswer")
|
||||||
|
console.log(currentAnswer)
|
||||||
const currentAnswerExtended =
|
const currentAnswerExtended =
|
||||||
percentsSum >= 100
|
percentsSum >= 100
|
||||||
? Object.entries(currentAnswer?.[1] ?? {})
|
? Object.entries(currentAnswer?.[1] ?? {})
|
||||||
@ -287,16 +313,92 @@ export const Answers: FC<AnswersProps> = ({ data }) => {
|
|||||||
<NextIcon />
|
<NextIcon />
|
||||||
</ButtonBase> */}
|
</ButtonBase> */}
|
||||||
</Box>
|
</Box>
|
||||||
{currentAnswerExtended.map(([title, percent], index) => (
|
{currentAnswerExtended.map(([title, percent], index) => {
|
||||||
|
console.log("kdgjhskjdfhkhsdgksfdhgjsdhfgkjsfdhgkj")
|
||||||
|
console.log("ewrqwrwr")
|
||||||
|
console.log("checkFileExtension")
|
||||||
|
console.log(checkFileExtension(title))
|
||||||
|
console.log("quiz?.backendId")
|
||||||
|
console.log(quiz?.backendId)
|
||||||
|
console.log("globalQuestion?.backendId")
|
||||||
|
console.log(globalQuestion?.backendId)
|
||||||
|
console.log("data")
|
||||||
|
console.log(data)
|
||||||
|
if (checkFileExtension(title) && quiz?.backendId && globalQuestion?.backendId) {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "inline - flex",
|
||||||
|
width: "100 %",
|
||||||
|
alignItems: "center",
|
||||||
|
padding: "0 48px",
|
||||||
|
paddingLeft: "26px"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
// <IconButton target="_blank" href={`/image/${title}`}
|
||||||
|
sx={{
|
||||||
|
width: "50px",
|
||||||
|
height: "40px",
|
||||||
|
bgcolor: "#e9eaf0",
|
||||||
|
borderRadius: "5px"
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
console.log(timewebContentFile(quiz?.qid.toString(), title, globalQuestion?.backendId.toString()))
|
||||||
|
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = timewebContentFile(quiz?.qid.toString(), title, globalQuestion?.backendId.toString());
|
||||||
|
link.download = title
|
||||||
|
link.style.display = 'none';
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<InsertDriveFileIcon />
|
||||||
|
</IconButton>
|
||||||
< Answer
|
< Answer
|
||||||
key={index}
|
key={index}
|
||||||
title={title}
|
title={title}
|
||||||
percent={percent}
|
percent={percent}
|
||||||
highlight={!index}
|
highlight={!index}
|
||||||
/>
|
/>
|
||||||
))}
|
</Box>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
< Answer
|
||||||
|
key={index}
|
||||||
|
title={title}
|
||||||
|
percent={percent}
|
||||||
|
highlight={!index}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})}
|
||||||
</Paper >
|
</Paper >
|
||||||
<Pagination page={page} setPage={setPage} pagesAmount={answers.length} />
|
<Pagination page={page} setPage={setPage} pagesAmount={answers.length} />
|
||||||
</Box >
|
</Box >
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
function checkFileExtension(filename: string, maxLength = 6) {
|
||||||
|
if (typeof filename !== 'string') return false;
|
||||||
|
|
||||||
|
// Ищем последнюю точку в строке
|
||||||
|
const lastDotIndex = filename.lastIndexOf('.');
|
||||||
|
|
||||||
|
// Если точки нет или она в конце строки - возвращаем false
|
||||||
|
if (lastDotIndex === -1 || lastDotIndex === filename.length - 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем расширение (часть после последней точки)
|
||||||
|
const extension = filename.slice(lastDotIndex + 1);
|
||||||
|
|
||||||
|
// Проверяем что расширение состоит только из букв и не превышает максимальную длину
|
||||||
|
return /^[a-zA-Zа-яА-ЯёЁ]+$/.test(extension) && extension.length <= maxLength;
|
||||||
|
}
|
||||||
@ -5,12 +5,14 @@ import { QuestionsResponse } from "@api/statistic";
|
|||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import { Funnel } from "./FunnelAnswers/Funnel";
|
import { Funnel } from "./FunnelAnswers/Funnel";
|
||||||
import { Results } from "./Results";
|
import { Results } from "./Results";
|
||||||
|
import { AnyTypedQuizQuestion } from "@frontend/squzanswerer";
|
||||||
|
|
||||||
type AnswersStatisticsProps = {
|
type AnswersStatisticsProps = {
|
||||||
data: QuestionsResponse | null;
|
data: QuestionsResponse | null;
|
||||||
|
globalQuestions: AnyTypedQuizQuestion[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AnswersStatistics: FC<AnswersStatisticsProps> = ({ data }) => {
|
export const AnswersStatistics: FC<AnswersStatisticsProps> = ({ data, globalQuestions }) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isSmallMonitor = useMediaQuery(theme.breakpoints.down(1150));
|
const isSmallMonitor = useMediaQuery(theme.breakpoints.down(1150));
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down(850));
|
const isMobile = useMediaQuery(theme.breakpoints.down(850));
|
||||||
@ -33,7 +35,7 @@ export const AnswersStatistics: FC<AnswersStatisticsProps> = ({ data }) => {
|
|||||||
gap: "40px",
|
gap: "40px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Answers data={data?.Questions || null} />
|
<Answers data={data?.Questions || null} globalQuestions={globalQuestions} />
|
||||||
<Funnel data={data?.Funnel || []} funnelData={data?.FunnelData || []} />
|
<Funnel data={data?.Funnel || []} funnelData={data?.FunnelData || []} />
|
||||||
</Box>
|
</Box>
|
||||||
<Results data={data?.Results || null} />
|
<Results data={data?.Results || null} />
|
||||||
|
|||||||
@ -41,14 +41,20 @@ const GeneralItem = ({
|
|||||||
const data = Object.entries(general).sort(
|
const data = Object.entries(general).sort(
|
||||||
([nextValue], [currentValue]) => Number(nextValue) - Number(currentValue),
|
([nextValue], [currentValue]) => Number(nextValue) - Number(currentValue),
|
||||||
);
|
);
|
||||||
const days = data.map(([value]) => value);
|
|
||||||
|
|
||||||
const numberValue = calculateTime
|
|
||||||
? Object.values(general).reduce((total, value) => total + value, 0) / Object.values(general).length
|
let numberValue = 0
|
||||||
: conversionValue
|
|
||||||
|
numberValue = conversionValue
|
||||||
? conversionValue
|
? conversionValue
|
||||||
: Object.values(general).reduce((total, item) => total + item, 0);
|
: Object.values(general).reduce((total, item) => total + item, 0);
|
||||||
|
|
||||||
|
if (calculateTime) {
|
||||||
|
const values = Object.values(general).filter(e => e);
|
||||||
|
numberValue = values.reduce((total, value) => total + value, 0) / Object.values(values).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
Object.keys(general).length === 0 ||
|
Object.keys(general).length === 0 ||
|
||||||
Object.values(general).every((item) => item === 0)
|
Object.values(general).every((item) => item === 0)
|
||||||
@ -150,7 +156,7 @@ export const General: FC<GeneralProps> = ({ data, day }) => {
|
|||||||
(total, item) => total + item,
|
(total, item) => total + item,
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
const openSum = Object.values(generalResponse.Open).reduce(
|
const openSum = Object.values(generalResponse.Open).filter(e => e).reduce(
|
||||||
(total, item) => total + item,
|
(total, item) => total + item,
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -19,25 +19,19 @@ export const parseTitle = (title: string): string => {
|
|||||||
try {
|
try {
|
||||||
// Пытаемся распарсить как JSON
|
// Пытаемся распарсить как JSON
|
||||||
const parsed = JSON.parse(cleanTitle);
|
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)
|
// Проверяем, что это объект с полями Image и Description (специфичный для вопросов типа images и varimg)
|
||||||
if (parsed &&
|
if (parsed &&
|
||||||
typeof parsed === 'object' &&
|
typeof parsed === 'object' &&
|
||||||
'Image' in parsed &&
|
'Image' in parsed &&
|
||||||
'Description' in parsed) {
|
'Description' in parsed) {
|
||||||
console.log("Returning Description:", parsed.Description);
|
|
||||||
return parsed.Description || "нет названия";
|
return parsed.Description || "нет названия";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Если это не объект с Image и Description, возвращаем исходную строку
|
// Если это не объект с Image и Description, возвращаем исходную строку
|
||||||
console.log("Not Image/Description object, returning original title");
|
|
||||||
return title;
|
return title;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Если парсинг не удался, возвращаем исходную строку
|
// Если парсинг не удался, возвращаем исходную строку
|
||||||
console.log("JSON parse error, returning original title");
|
|
||||||
return title;
|
return title;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -10,6 +10,7 @@ import { updateQuiz } from "@root/quizes/actions";
|
|||||||
import { useCurrentQuiz } from "@root/quizes/hooks";
|
import { useCurrentQuiz } from "@root/quizes/hooks";
|
||||||
import type { DesignItem } from "./DesignGroup";
|
import type { DesignItem } from "./DesignGroup";
|
||||||
import { DesignGroup } from "./DesignGroup";
|
import { DesignGroup } from "./DesignGroup";
|
||||||
|
import pinkScrollbar from "@utils/pinkScrollbar";
|
||||||
|
|
||||||
import Desgin1 from "@icons/designs/smallSize/design1.jpg";
|
import Desgin1 from "@icons/designs/smallSize/design1.jpg";
|
||||||
import Desgin2 from "@icons/designs/smallSize/design2.jpg";
|
import Desgin2 from "@icons/designs/smallSize/design2.jpg";
|
||||||
@ -133,15 +134,7 @@ export const DesignFilling = ({
|
|||||||
padding: "20px",
|
padding: "20px",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
overflow: "auto",
|
overflow: "auto",
|
||||||
scrollbarWidth: "auto",
|
...pinkScrollbar(theme)
|
||||||
"&::-webkit-scrollbar": {
|
|
||||||
display: "block",
|
|
||||||
width: "8px",
|
|
||||||
},
|
|
||||||
"&::-webkit-scrollbar-thumb": {
|
|
||||||
backgroundColor: theme.palette.brightPurple.main,
|
|
||||||
borderRadius: "4px",
|
|
||||||
},
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box sx={{ display: "flex", gap: "20px", flexWrap: "wrap" }}>
|
<Box sx={{ display: "flex", gap: "20px", flexWrap: "wrap" }}>
|
||||||
@ -199,3 +192,4 @@ export const DesignFilling = ({
|
|||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,36 @@
|
|||||||
|
import { FC } from "react";
|
||||||
|
import { Box, Link, BoxProps } from "@mui/material";
|
||||||
|
import OrangeYoutube from "@/assets/icons/OrangeYoutube";
|
||||||
|
|
||||||
|
interface InstructionYoutubeLinkProps extends BoxProps {}
|
||||||
|
|
||||||
|
const InstructionYoutubeLink: FC<InstructionYoutubeLinkProps> = ({ sx, ...props }) => {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "right",
|
||||||
|
...sx,
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
href="https://youtube.com"
|
||||||
|
underline="hover"
|
||||||
|
sx={{
|
||||||
|
color: "#FA590B",
|
||||||
|
display: "inline-flex",
|
||||||
|
gap: "10px",
|
||||||
|
fontSize: "16px"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<OrangeYoutube sx={{
|
||||||
|
width: "24px",
|
||||||
|
height: "24px",
|
||||||
|
}} /> Смотреть инструкцию
|
||||||
|
</Link>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InstructionYoutubeLink;
|
||||||
@ -1,7 +1,13 @@
|
|||||||
import { FC } from "react";
|
import { FC, useState, useEffect } from "react";
|
||||||
import { Dialog, IconButton, Typography, useMediaQuery, useTheme, Box } from "@mui/material";
|
import { Dialog, IconButton, Typography, useMediaQuery, useTheme, Box, Button } from "@mui/material";
|
||||||
import CloseIcon from "@mui/icons-material/Close";
|
import CloseIcon from "@mui/icons-material/Close";
|
||||||
import { Quiz } from "@/model/quiz/quiz";
|
import { Quiz } from "@/model/quiz/quiz";
|
||||||
|
import CustomTextField from "@/ui_kit/CustomTextField";
|
||||||
|
import { createLeadTarget, getLeadTargetsByQuiz, deleteLeadTarget, updateLeadTarget } from "@/api/leadtarget";
|
||||||
|
import { useFormik } from "formik";
|
||||||
|
import InstructionYoutubeLink from "@/pages/IntegrationsPage/IntegrationsModal/InstructionYoutubeLink";
|
||||||
|
import { useSnackbar } from "notistack";
|
||||||
|
import { useLeadTargets } from "@/pages/IntegrationsPage/hooks/useLeadTargets";
|
||||||
|
|
||||||
type PostbackModalProps = {
|
type PostbackModalProps = {
|
||||||
isModalOpen: boolean;
|
isModalOpen: boolean;
|
||||||
@ -19,6 +25,65 @@ export const PostbackModal: FC<PostbackModalProps> = ({
|
|||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down(600));
|
const isMobile = useMediaQuery(theme.breakpoints.down(600));
|
||||||
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
|
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
|
||||||
|
const [isSaving, setIsSaving] = useState<boolean>(false);
|
||||||
|
const { enqueueSnackbar } = useSnackbar();
|
||||||
|
|
||||||
|
const handleSubmit = async (values: { token: string; domain: string }) => {
|
||||||
|
const tokenValue = (values.token || "").trim();
|
||||||
|
const target = (values.domain || "").trim();
|
||||||
|
try {
|
||||||
|
setIsSaving(true);
|
||||||
|
// 1) Асинхронно получаем текущие цели
|
||||||
|
const [items] = await getLeadTargetsByQuiz(quiz.backendId);
|
||||||
|
const existing = (items ?? []).filter((t) => t.type === "webhook");
|
||||||
|
if (!tokenValue && !target) {
|
||||||
|
const deletePromises = existing.map((t) => deleteLeadTarget(t.id));
|
||||||
|
await Promise.all(deletePromises);
|
||||||
|
enqueueSnackbar("Postback удален", { variant: "success" });
|
||||||
|
} else if (existing.length > 0) {
|
||||||
|
const [first, ...extra] = existing;
|
||||||
|
await Promise.all([
|
||||||
|
updateLeadTarget({ id: first.id, target }),
|
||||||
|
...extra.map((t) => deleteLeadTarget(t.id)),
|
||||||
|
]);
|
||||||
|
enqueueSnackbar("Postback обновлен", { variant: "success" });
|
||||||
|
} else {
|
||||||
|
await createLeadTarget({
|
||||||
|
type: "webhook",
|
||||||
|
quizID: quiz.backendId,
|
||||||
|
target,
|
||||||
|
name: tokenValue || undefined,
|
||||||
|
});
|
||||||
|
enqueueSnackbar("Postback сохранен", { variant: "success" });
|
||||||
|
}
|
||||||
|
|
||||||
|
await refresh();
|
||||||
|
} catch (error) {
|
||||||
|
enqueueSnackbar("Ошибка при сохранении", { variant: "error" });
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const { isLoading, postbackTarget, refresh } = useLeadTargets(quiz?.backendId, isModalOpen);
|
||||||
|
|
||||||
|
const formik = useFormik<{ token: string; domain: string }>({
|
||||||
|
initialValues: { token: "", domain: postbackTarget?.target ?? "" },
|
||||||
|
onSubmit: handleSubmit,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
formik.setFieldValue("domain", postbackTarget?.target ?? "");
|
||||||
|
}, [postbackTarget?.target]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isModalOpen) {
|
||||||
|
setTimeout(() => {
|
||||||
|
const input = document.getElementById("postback-domain") as HTMLInputElement | null;
|
||||||
|
input?.focus();
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}, [isModalOpen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
@ -28,12 +93,12 @@ export const PostbackModal: FC<PostbackModalProps> = ({
|
|||||||
PaperProps={{
|
PaperProps={{
|
||||||
sx: {
|
sx: {
|
||||||
maxWidth: isTablet ? "100%" : "919px",
|
maxWidth: isTablet ? "100%" : "919px",
|
||||||
height: "658px",
|
height: isMobile ? "303px" : "214px",
|
||||||
|
// height: "314px",
|
||||||
borderRadius: "12px",
|
borderRadius: "12px",
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box>
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
width: "100%",
|
width: "100%",
|
||||||
@ -52,7 +117,7 @@ export const PostbackModal: FC<PostbackModalProps> = ({
|
|||||||
color: theme.palette.grey2.main,
|
color: theme.palette.grey2.main,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Интеграция с {companyName ? companyName : "Postback"}
|
Автоматизация с {companyName ? companyName : "Postback"}
|
||||||
</Typography>
|
</Typography>
|
||||||
<IconButton onClick={handleCloseModal}>
|
<IconButton onClick={handleCloseModal}>
|
||||||
<CloseIcon />
|
<CloseIcon />
|
||||||
@ -65,10 +130,66 @@ export const PostbackModal: FC<PostbackModalProps> = ({
|
|||||||
overflow: "auto",
|
overflow: "auto",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography variant="body1">
|
{!isMobile && <InstructionYoutubeLink />}
|
||||||
Интеграция с Postback находится в разработке.
|
|
||||||
|
<Box
|
||||||
|
component="form"
|
||||||
|
onSubmit={formik.handleSubmit}
|
||||||
|
sx={{
|
||||||
|
marginTop: isMobile ? 0 : "-43px",
|
||||||
|
display: isMobile ? "flex" : "inline-flex",
|
||||||
|
flexDirection: isMobile ? "column" : "row",
|
||||||
|
alignItems: isMobile ? "center" : "end",
|
||||||
|
gap: isMobile ? "10px" : "38px",
|
||||||
|
width: "100%"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: "100%"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontWeight: 500,
|
||||||
|
color: "black",
|
||||||
|
mt: isMobile ? 0 : "11px",
|
||||||
|
mb: "14px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Домен
|
||||||
</Typography>
|
</Typography>
|
||||||
|
{isLoading ? (
|
||||||
|
<Box sx={{ width: "100%", height: 44, borderRadius: "8px", bgcolor: "action.hover" }} />
|
||||||
|
) : (
|
||||||
|
<CustomTextField
|
||||||
|
id="postback-domain"
|
||||||
|
placeholder="токен в формате ХХХХХХ"
|
||||||
|
value={formik.values.domain}
|
||||||
|
onChange={(e) => formik.setFieldValue("domain", e.target.value)}
|
||||||
|
maxLength={150}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
disabled={isSaving || isLoading}
|
||||||
|
type="submit"
|
||||||
|
variant="contained"
|
||||||
|
sx={{
|
||||||
|
backgroundColor: "#7E2AEA",
|
||||||
|
fontSize: "18px",
|
||||||
|
lineHeight: "18px",
|
||||||
|
width: isMobile ? "100%" : "216px",
|
||||||
|
height: "44px",
|
||||||
|
|
||||||
|
p: "10px 20px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Сохранить
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
{isMobile && <InstructionYoutubeLink sx={{ mt: "40px", justifyContent: "center" }} />}
|
||||||
</Box>
|
</Box>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,7 +1,13 @@
|
|||||||
import { FC } from "react";
|
import { FC, useState, useEffect } from "react";
|
||||||
import { Dialog, IconButton, Typography, useMediaQuery, useTheme, Box } from "@mui/material";
|
import { Dialog, IconButton, Typography, useMediaQuery, useTheme, Box, Button } from "@mui/material";
|
||||||
import CloseIcon from "@mui/icons-material/Close";
|
import CloseIcon from "@mui/icons-material/Close";
|
||||||
import { Quiz } from "@/model/quiz/quiz";
|
import { Quiz } from "@/model/quiz/quiz";
|
||||||
|
import CustomTextField from "@/ui_kit/CustomTextField";
|
||||||
|
import { createLeadTarget, getLeadTargetsByQuiz, deleteLeadTarget, updateLeadTarget } from "@/api/leadtarget";
|
||||||
|
import { useFormik } from "formik";
|
||||||
|
import InstructionYoutubeLink from "@/pages/IntegrationsPage/IntegrationsModal/InstructionYoutubeLink";
|
||||||
|
import { useSnackbar } from "notistack";
|
||||||
|
import { useLeadTargets } from "@/pages/IntegrationsPage/hooks/useLeadTargets";
|
||||||
|
|
||||||
type ZapierModalProps = {
|
type ZapierModalProps = {
|
||||||
isModalOpen: boolean;
|
isModalOpen: boolean;
|
||||||
@ -19,6 +25,67 @@ export const ZapierModal: FC<ZapierModalProps> = ({
|
|||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down(600));
|
const isMobile = useMediaQuery(theme.breakpoints.down(600));
|
||||||
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
|
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
|
||||||
|
const [isSaving, setIsSaving] = useState<boolean>(false);
|
||||||
|
const { enqueueSnackbar } = useSnackbar();
|
||||||
|
|
||||||
|
const handleSubmit = async (values: { webhookUrl: string }) => {
|
||||||
|
const target = (values.webhookUrl || "").trim();
|
||||||
|
try {
|
||||||
|
setIsSaving(true);
|
||||||
|
// 1) Асинхронно получаем текущие цели
|
||||||
|
const [items] = await getLeadTargetsByQuiz(quiz.backendId);
|
||||||
|
const existing = (items ?? []).filter((t) => t.type === "webhook");
|
||||||
|
|
||||||
|
if (!target) {
|
||||||
|
// Пустое значение — удаляем все
|
||||||
|
const deletePromises = existing.map((t) => deleteLeadTarget(t.id));
|
||||||
|
await Promise.all(deletePromises);
|
||||||
|
enqueueSnackbar("Webhook удален", { variant: "success" });
|
||||||
|
} else if (existing.length > 0) {
|
||||||
|
// Уже существует — обновляем первый и удаляем все лишние
|
||||||
|
const [first, ...extra] = existing;
|
||||||
|
await Promise.all([
|
||||||
|
updateLeadTarget({ id: first.id, target }),
|
||||||
|
...extra.map((t) => deleteLeadTarget(t.id)),
|
||||||
|
]);
|
||||||
|
enqueueSnackbar("Webhook обновлен", { variant: "success" });
|
||||||
|
} else {
|
||||||
|
// Не существует — создаем
|
||||||
|
await createLeadTarget({
|
||||||
|
type: "webhook",
|
||||||
|
quizID: quiz.backendId,
|
||||||
|
target,
|
||||||
|
});
|
||||||
|
enqueueSnackbar("Webhook сохранен", { variant: "success" });
|
||||||
|
}
|
||||||
|
|
||||||
|
await refresh();
|
||||||
|
} catch (error) {
|
||||||
|
enqueueSnackbar("Ошибка при сохранении", { variant: "error" });
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const { isLoading, zapierTarget, refresh } = useLeadTargets(quiz?.backendId, isModalOpen);
|
||||||
|
|
||||||
|
const formik = useFormik<{ webhookUrl: string }>({
|
||||||
|
initialValues: { webhookUrl: zapierTarget?.target ?? "" },
|
||||||
|
onSubmit: handleSubmit,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
formik.setFieldValue("webhookUrl", zapierTarget?.target ?? "");
|
||||||
|
}, [zapierTarget?.target]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isModalOpen) {
|
||||||
|
setTimeout(() => {
|
||||||
|
const input = document.getElementById("zapier-webhook-url") as HTMLInputElement | null;
|
||||||
|
input?.focus();
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}, [isModalOpen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
@ -28,7 +95,7 @@ export const ZapierModal: FC<ZapierModalProps> = ({
|
|||||||
PaperProps={{
|
PaperProps={{
|
||||||
sx: {
|
sx: {
|
||||||
maxWidth: isTablet ? "100%" : "919px",
|
maxWidth: isTablet ? "100%" : "919px",
|
||||||
height: "658px",
|
height: isMobile ? "303px" : "195px",
|
||||||
borderRadius: "12px",
|
borderRadius: "12px",
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
@ -52,12 +119,13 @@ export const ZapierModal: FC<ZapierModalProps> = ({
|
|||||||
color: theme.palette.grey2.main,
|
color: theme.palette.grey2.main,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Интеграция с {companyName ? companyName : "Zapier"}
|
Автоматизация с {companyName ? companyName : "Zapier"}
|
||||||
</Typography>
|
</Typography>
|
||||||
<IconButton onClick={handleCloseModal}>
|
<IconButton onClick={handleCloseModal}>
|
||||||
<CloseIcon />
|
<CloseIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
padding: "20px",
|
padding: "20px",
|
||||||
@ -65,9 +133,74 @@ export const ZapierModal: FC<ZapierModalProps> = ({
|
|||||||
overflow: "auto",
|
overflow: "auto",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography variant="body1">
|
{!isMobile && <InstructionYoutubeLink />}
|
||||||
Интеграция с Zapier находится в разработке.
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
marginTop: isMobile ? 0 : "-33px",
|
||||||
|
display: isMobile ? "flex" : "block",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: "100%"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontWeight: 500,
|
||||||
|
color: "black",
|
||||||
|
mt: isMobile ? 0 : "11px",
|
||||||
|
mb: "14px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
URL webhook
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
component="form"
|
||||||
|
onSubmit={formik.handleSubmit}
|
||||||
|
sx={{
|
||||||
|
display: "inline-flex",
|
||||||
|
flexDirection: isMobile ? "column" : "row",
|
||||||
|
width: "100%",
|
||||||
|
gap: isMobile ? "10px" : "38px"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<Box sx={{ width: "100%", height: 44, borderRadius: "8px", bgcolor: "action.hover" }} />
|
||||||
|
) : (
|
||||||
|
<CustomTextField
|
||||||
|
id="zapier-webhook-url"
|
||||||
|
placeholder="введите url здесь"
|
||||||
|
value={formik.values.webhookUrl}
|
||||||
|
onChange={(e) => formik.setFieldValue("webhookUrl", e.target.value)}
|
||||||
|
maxLength={150}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
disabled={isSaving || isLoading}
|
||||||
|
type="submit"
|
||||||
|
variant="contained"
|
||||||
|
sx={{
|
||||||
|
backgroundColor: "#7E2AEA",
|
||||||
|
fontSize: "18px",
|
||||||
|
lineHeight: "18px",
|
||||||
|
width: isMobile ? "100%" : "216px",
|
||||||
|
height: "44px",
|
||||||
|
|
||||||
|
p: "10px 20px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Сохранить
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
</Box>
|
||||||
|
{isMobile && <InstructionYoutubeLink sx={{ mt: "40px" }} />}
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { useCurrentQuiz } from "@root/quizes/hooks";
|
|||||||
import { useQuizStore } from "@root/quizes/store";
|
import { useQuizStore } from "@root/quizes/store";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { PartnersBoard } from "./PartnersBoard/PartnersBoard";
|
import { PartnersBoard } from "./PartnersBoard/PartnersBoard";
|
||||||
|
import { getLeadTargetsByQuiz, LeadTargetModel } from "@/api/leadtarget";
|
||||||
import { QuizMetricType } from "@model/quizSettings";
|
import { QuizMetricType } from "@model/quizSettings";
|
||||||
|
|
||||||
interface IntegrationsPageProps {
|
interface IntegrationsPageProps {
|
||||||
@ -29,10 +30,42 @@ export const IntegrationsPage = ({
|
|||||||
const [isAmoCrmModalOpen, setIsAmoCrmModalOpen] = useState<boolean>(false);
|
const [isAmoCrmModalOpen, setIsAmoCrmModalOpen] = useState<boolean>(false);
|
||||||
const [isZapierModalOpen, setIsZapierModalOpen] = useState<boolean>(false);
|
const [isZapierModalOpen, setIsZapierModalOpen] = useState<boolean>(false);
|
||||||
const [isPostbackModalOpen, setIsPostbackModalOpen] = useState<boolean>(false);
|
const [isPostbackModalOpen, setIsPostbackModalOpen] = useState<boolean>(false);
|
||||||
|
const [leadTargetsLoaded, setLeadTargetsLoaded] = useState<boolean>(false);
|
||||||
|
const [leadTargets, setLeadTargets] = useState<LeadTargetModel[] | null>(null);
|
||||||
|
const [zapierTarget, setZapierTarget] = useState<LeadTargetModel | null>(null);
|
||||||
|
const [postbackTarget, setPostbackTarget] = useState<LeadTargetModel | null>(null);
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (editQuizId === null) navigate("/list");
|
if (editQuizId === null) navigate("/list");
|
||||||
}, [navigate, editQuizId]);
|
}, [navigate, editQuizId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const load = async () => {
|
||||||
|
if (!leadTargetsLoaded && quiz?.id) {
|
||||||
|
const [items] = await getLeadTargetsByQuiz(quiz.backendId);
|
||||||
|
const list = items ?? [];
|
||||||
|
setLeadTargets(list);
|
||||||
|
const webhookOnly = list.filter((t) => t.type === "webhook");
|
||||||
|
const zapier = webhookOnly.find((t) => (t.target || "").toLowerCase().includes("zapier")) ?? null;
|
||||||
|
const postback = webhookOnly.find((t) => (t.target || "").toLowerCase().includes("postback")) ?? null;
|
||||||
|
setZapierTarget(zapier);
|
||||||
|
setPostbackTarget(postback);
|
||||||
|
setLeadTargetsLoaded(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
load();
|
||||||
|
}, [leadTargetsLoaded, quiz?.id]);
|
||||||
|
|
||||||
|
const refreshLeadTargets = async () => {
|
||||||
|
if (!quiz?.id) return;
|
||||||
|
const [items] = await getLeadTargetsByQuiz(quiz.backendId);
|
||||||
|
const list = items ?? [];
|
||||||
|
setLeadTargets(list);
|
||||||
|
const webhookOnly = list.filter((t) => t.type === "webhook");
|
||||||
|
setZapierTarget(webhookOnly.find((t) => (t.target || "").toLowerCase().includes("zapier")) ?? null);
|
||||||
|
setPostbackTarget(webhookOnly.find((t) => (t.target || "").toLowerCase().includes("postback")) ?? null);
|
||||||
|
};
|
||||||
const heightBar = heightSidebar + 51 + 88 + 36 + 25;
|
const heightBar = heightSidebar + 51 + 88 + 36 + 25;
|
||||||
|
|
||||||
if (quiz === undefined)
|
if (quiz === undefined)
|
||||||
@ -87,6 +120,8 @@ export const IntegrationsPage = ({
|
|||||||
setIsPostbackModalOpen={setIsPostbackModalOpen}
|
setIsPostbackModalOpen={setIsPostbackModalOpen}
|
||||||
isPostbackModalOpen={isPostbackModalOpen}
|
isPostbackModalOpen={isPostbackModalOpen}
|
||||||
handleClosePostbackModal={handleClosePostbackModal}
|
handleClosePostbackModal={handleClosePostbackModal}
|
||||||
|
zapierTarget={zapierTarget}
|
||||||
|
postbackTarget={postbackTarget}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import { YandexMetricaLogo } from "../mocks/YandexMetricaLogo";
|
|||||||
import { VKPixelLogo } from "../mocks/VKPixelLogo";
|
import { VKPixelLogo } from "../mocks/VKPixelLogo";
|
||||||
import { QuizMetricType } from "@model/quizSettings";
|
import { QuizMetricType } from "@model/quizSettings";
|
||||||
import { AmoCRMLogo } from "../mocks/AmoCRMLogo";
|
import { AmoCRMLogo } from "../mocks/AmoCRMLogo";
|
||||||
|
import type { LeadTargetModel } from "@/api/leadtarget";
|
||||||
import { useCurrentQuiz } from "@/stores/quizes/hooks";
|
import { useCurrentQuiz } from "@/stores/quizes/hooks";
|
||||||
|
|
||||||
const AnalyticsModal = lazy(() =>
|
const AnalyticsModal = lazy(() =>
|
||||||
@ -48,6 +49,8 @@ type PartnersBoardProps = {
|
|||||||
setIsPostbackModalOpen: (value: boolean) => void;
|
setIsPostbackModalOpen: (value: boolean) => void;
|
||||||
isPostbackModalOpen: boolean;
|
isPostbackModalOpen: boolean;
|
||||||
handleClosePostbackModal: () => void;
|
handleClosePostbackModal: () => void;
|
||||||
|
zapierTarget?: LeadTargetModel | null;
|
||||||
|
postbackTarget?: LeadTargetModel | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PartnersBoard: FC<PartnersBoardProps> = ({
|
export const PartnersBoard: FC<PartnersBoardProps> = ({
|
||||||
@ -65,6 +68,8 @@ export const PartnersBoard: FC<PartnersBoardProps> = ({
|
|||||||
setIsPostbackModalOpen,
|
setIsPostbackModalOpen,
|
||||||
isPostbackModalOpen,
|
isPostbackModalOpen,
|
||||||
handleClosePostbackModal,
|
handleClosePostbackModal,
|
||||||
|
zapierTarget,
|
||||||
|
postbackTarget,
|
||||||
}) => {
|
}) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const quiz = useCurrentQuiz();
|
const quiz = useCurrentQuiz();
|
||||||
@ -132,7 +137,7 @@ export const PartnersBoard: FC<PartnersBoardProps> = ({
|
|||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* <Typography variant="h6" sx={sectionTitleStyles}>
|
<Typography variant="h6" sx={sectionTitleStyles}>
|
||||||
Автоматизация
|
Автоматизация
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box sx={containerStyles}>
|
<Box sx={containerStyles}>
|
||||||
@ -144,7 +149,7 @@ export const PartnersBoard: FC<PartnersBoardProps> = ({
|
|||||||
setIsModalOpen={setIsPostbackModalOpen}
|
setIsModalOpen={setIsPostbackModalOpen}
|
||||||
setCompanyName={setCompanyName}
|
setCompanyName={setCompanyName}
|
||||||
/>
|
/>
|
||||||
</Box> */}
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{companyName && (
|
{companyName && (
|
||||||
@ -173,6 +178,7 @@ export const PartnersBoard: FC<PartnersBoardProps> = ({
|
|||||||
handleCloseModal={handleCloseZapierModal}
|
handleCloseModal={handleCloseZapierModal}
|
||||||
companyName={companyName}
|
companyName={companyName}
|
||||||
quiz={quiz!}
|
quiz={quiz!}
|
||||||
|
currentTarget={zapierTarget ?? null}
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
)}
|
)}
|
||||||
@ -183,6 +189,7 @@ export const PartnersBoard: FC<PartnersBoardProps> = ({
|
|||||||
handleCloseModal={handleClosePostbackModal}
|
handleCloseModal={handleClosePostbackModal}
|
||||||
companyName={companyName}
|
companyName={companyName}
|
||||||
quiz={quiz!}
|
quiz={quiz!}
|
||||||
|
currentTarget={postbackTarget ?? null}
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
)}
|
)}
|
||||||
|
|||||||
47
src/pages/IntegrationsPage/hooks/useLeadTargets.ts
Normal file
47
src/pages/IntegrationsPage/hooks/useLeadTargets.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import { getLeadTargetsByQuiz, LeadTargetModel } from "@/api/leadtarget";
|
||||||
|
|
||||||
|
type UseLeadTargetsResult = {
|
||||||
|
isLoading: boolean;
|
||||||
|
zapierTarget: LeadTargetModel | null;
|
||||||
|
postbackTarget: LeadTargetModel | null;
|
||||||
|
refresh: () => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useLeadTargets(quizBackendId: number | undefined, isOpen: boolean): UseLeadTargetsResult {
|
||||||
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||||
|
const [zapierTarget, setZapierTarget] = useState<LeadTargetModel | null>(null);
|
||||||
|
const [postbackTarget, setPostbackTarget] = useState<LeadTargetModel | null>(null);
|
||||||
|
|
||||||
|
const load = useCallback(async () => {
|
||||||
|
if (!quizBackendId) return;
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const [items] = await getLeadTargetsByQuiz(quizBackendId);
|
||||||
|
const list = items ?? [];
|
||||||
|
const webhookOnly = list.filter((t) => t.type === "webhook");
|
||||||
|
const zapier = webhookOnly.find((t) => (t.target || "").toLowerCase().includes("zapier")) ?? null;
|
||||||
|
const postback = webhookOnly.find((t) => (t.target || "").toLowerCase().includes("postback")) ?? null;
|
||||||
|
setZapierTarget(zapier);
|
||||||
|
setPostbackTarget(postback);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}, [quizBackendId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
load();
|
||||||
|
}
|
||||||
|
}, [isOpen, load]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isLoading,
|
||||||
|
zapierTarget,
|
||||||
|
postbackTarget,
|
||||||
|
refresh: load,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -2,7 +2,6 @@ import React from "react";
|
|||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
import SectionStyled from "./SectionStyled";
|
import SectionStyled from "./SectionStyled";
|
||||||
import NavMenuItem from "@ui_kit/Header/NavMenuItem";
|
|
||||||
import QuizLogo from "./images/icons/QuizLogo";
|
import QuizLogo from "./images/icons/QuizLogo";
|
||||||
import { useMediaQuery, useTheme } from "@mui/material";
|
import { useMediaQuery, useTheme } from "@mui/material";
|
||||||
import { setIsContactFormOpen } from "../../stores/contactForm";
|
import { setIsContactFormOpen } from "../../stores/contactForm";
|
||||||
@ -26,12 +25,6 @@ export default function Component() {
|
|||||||
const userId = useUserStore((state) => state.userId);
|
const userId = useUserStore((state) => state.userId);
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
console.log("HeaderLanding debug:", {
|
|
||||||
userId,
|
|
||||||
location: location.pathname,
|
|
||||||
backgroundLocation: location.state?.backgroundLocation
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionStyled
|
<SectionStyled
|
||||||
tag={"header"}
|
tag={"header"}
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import {
|
|||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { addQuestionVariant, deleteQuestionVariant, setQuestionVariantField, updateQuestion } from "@root/questions/actions";
|
import { addQuestionVariant, deleteQuestionVariant, setQuestionVariantField, updateQuestion } from "@root/questions/actions";
|
||||||
import { enqueueSnackbar } from "notistack";
|
import { enqueueSnackbar } from "notistack";
|
||||||
import { memo, type ChangeEvent, type FC, type KeyboardEvent, type ReactNode } from "react";
|
import { memo, useCallback, type ChangeEvent, type FC, type KeyboardEvent, type ReactNode } from "react";
|
||||||
import { Draggable } from "react-beautiful-dnd";
|
import { Draggable } from "react-beautiful-dnd";
|
||||||
import type { QuestionVariant, QuizQuestionVariant } from "@frontend/squzanswerer";
|
import type { QuestionVariant, QuizQuestionVariant } from "@frontend/squzanswerer";
|
||||||
|
|
||||||
@ -28,19 +28,51 @@ type AnswerItemProps = {
|
|||||||
additionalMobile?: ReactNode;
|
additionalMobile?: ReactNode;
|
||||||
isOwn: boolean;
|
isOwn: boolean;
|
||||||
ownPlaceholder: string;
|
ownPlaceholder: string;
|
||||||
|
shouldAutoFocus?: boolean;
|
||||||
|
onFocusHandled?: () => void;
|
||||||
|
onEnterKeyPress?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const AnswerItem = memo<AnswerItemProps>(
|
const AnswerItem = memo<AnswerItemProps>(
|
||||||
({ index, variant, questionId, largeCheck = false, additionalContent, additionalMobile, disableKeyDown, isOwn, ownPlaceholder }) => {
|
({ index, variant, questionId, largeCheck = false, additionalContent, additionalMobile, disableKeyDown, isOwn, ownPlaceholder, shouldAutoFocus, onFocusHandled, onEnterKeyPress }) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isTablet = useMediaQuery(theme.breakpoints.down(790));
|
const isTablet = useMediaQuery(theme.breakpoints.down(790));
|
||||||
|
|
||||||
const setOwnPlaceholder = (replText: string) => {
|
const setOwnPlaceholder = (replText: string) => {
|
||||||
updateQuestion(questionId, (question) => {
|
updateQuestion<QuizQuestionVariant>(questionId, (question) => {
|
||||||
question.content.ownPlaceholder = replText;
|
question.content.ownPlaceholder = replText;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const inputRefCallback = useCallback((element: HTMLInputElement | HTMLTextAreaElement | null) => {
|
||||||
|
if (element && shouldAutoFocus) {
|
||||||
|
element.focus();
|
||||||
|
onFocusHandled?.();
|
||||||
|
}
|
||||||
|
}, [shouldAutoFocus, onFocusHandled]);
|
||||||
|
|
||||||
|
const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
|
||||||
|
// Shift+Enter — новая строка, ничего не делаем (даём браузеру вставить перенос)
|
||||||
|
if (event.key === "Enter" && event.shiftKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enter — добавить новый вариант
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
event.preventDefault();
|
||||||
|
if (disableKeyDown) {
|
||||||
|
enqueueSnackbar("100 максимальное количество");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (onEnterKeyPress) {
|
||||||
|
onEnterKeyPress();
|
||||||
|
} else {
|
||||||
|
// Fallback если onEnterKeyPress не передан
|
||||||
|
addQuestionVariant(questionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Draggable
|
<Draggable
|
||||||
@ -64,12 +96,14 @@ const AnswerItem = memo<AnswerItemProps>(
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TextField
|
<TextField
|
||||||
|
inputRef={inputRefCallback}
|
||||||
value={isOwn ? ownPlaceholder : variant.answer}
|
value={isOwn ? ownPlaceholder : variant.answer}
|
||||||
fullWidth
|
fullWidth
|
||||||
focused={false}
|
focused={false}
|
||||||
placeholder={isOwn ? "Добавьте текст-подсказку для ввода “своего ответа”" : "Добавьте ответ"}
|
placeholder={isOwn ? "Добавьте текст-подсказку для ввода \"своего ответа\"" : "Добавьте ответ"}
|
||||||
multiline={largeCheck}
|
multiline
|
||||||
onChange={({ target }: ChangeEvent<HTMLInputElement>) => {
|
rows={1}
|
||||||
|
onChange={({ target }: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||||
if (target.value.length <= 1000) {
|
if (target.value.length <= 1000) {
|
||||||
isOwn ?
|
isOwn ?
|
||||||
setOwnPlaceholder(target.value || " ")
|
setOwnPlaceholder(target.value || " ")
|
||||||
@ -79,13 +113,7 @@ const AnswerItem = memo<AnswerItemProps>(
|
|||||||
enqueueSnackbar("Превышена длина вводимого текста")
|
enqueueSnackbar("Превышена длина вводимого текста")
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onKeyDown={(event: KeyboardEvent<HTMLInputElement>) => {
|
onKeyDown={handleKeyDown}
|
||||||
if (disableKeyDown) {
|
|
||||||
enqueueSnackbar("100 максимальное количество");
|
|
||||||
} else if (event.code === "Enter" && !largeCheck) {
|
|
||||||
addQuestionVariant(questionId);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
InputProps={{
|
InputProps={{
|
||||||
startAdornment: (
|
startAdornment: (
|
||||||
<>
|
<>
|
||||||
@ -130,6 +158,9 @@ const AnswerItem = memo<AnswerItemProps>(
|
|||||||
},
|
},
|
||||||
"& textarea.MuiInputBase-input": {
|
"& textarea.MuiInputBase-input": {
|
||||||
marginTop: "1px",
|
marginTop: "1px",
|
||||||
|
resize: "none",
|
||||||
|
// удерживаем стартовую высоту визуально как у однострочного
|
||||||
|
lineHeight: "21px",
|
||||||
},
|
},
|
||||||
"& .MuiOutlinedInput-notchedOutline": {
|
"& .MuiOutlinedInput-notchedOutline": {
|
||||||
border: "none",
|
border: "none",
|
||||||
|
|||||||
@ -16,6 +16,9 @@ type Props = Omit<
|
|||||||
openImageUploadModal: () => void;
|
openImageUploadModal: () => void;
|
||||||
isOwn: boolean;
|
isOwn: boolean;
|
||||||
ownPlaceholder: string;
|
ownPlaceholder: string;
|
||||||
|
shouldAutoFocus?: boolean;
|
||||||
|
onFocusHandled?: () => void;
|
||||||
|
onEnterKeyPress?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ImageEditAnswerItem({
|
export default function ImageEditAnswerItem({
|
||||||
@ -31,6 +34,9 @@ export default function ImageEditAnswerItem({
|
|||||||
openImageUploadModal,
|
openImageUploadModal,
|
||||||
isOwn,
|
isOwn,
|
||||||
ownPlaceholder,
|
ownPlaceholder,
|
||||||
|
shouldAutoFocus,
|
||||||
|
onFocusHandled,
|
||||||
|
onEnterKeyPress,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const addOrEditImageButton = useMemo(() => {
|
const addOrEditImageButton = useMemo(() => {
|
||||||
return (
|
return (
|
||||||
@ -111,6 +117,9 @@ export default function ImageEditAnswerItem({
|
|||||||
additionalMobile={addOrEditImageButtonMobile}
|
additionalMobile={addOrEditImageButtonMobile}
|
||||||
isOwn={isOwn}
|
isOwn={isOwn}
|
||||||
ownPlaceholder={ownPlaceholder}
|
ownPlaceholder={ownPlaceholder}
|
||||||
|
shouldAutoFocus={shouldAutoFocus}
|
||||||
|
onFocusHandled={onFocusHandled}
|
||||||
|
onEnterKeyPress={onEnterKeyPress}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { Box, Link, Typography, useMediaQuery, useTheme } from "@mui/material";
|
import { Box, Link, Typography, useMediaQuery, useTheme } from "@mui/material";
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { EnterIcon } from "@/assets/icons/questionsPage/enterIcon";
|
import { EnterIcon } from "@/assets/icons/questionsPage/enterIcon";
|
||||||
import { useAddAnswer } from "../../../utils/hooks/useAddAnswer";
|
import { useQuestionVariantsWithFocus } from "../../../utils/questionVariants";
|
||||||
import { AnswerDraggableList } from "../AnswerDraggableList";
|
import { AnswerDraggableList } from "../AnswerDraggableList";
|
||||||
import ButtonsOptions from "../QuestionOptions/ButtonsLayout/ButtonsOptions";
|
import ButtonsOptions from "../QuestionOptions/ButtonsLayout/ButtonsOptions";
|
||||||
import SwitchDropDown from "./switchDropDown";
|
import SwitchDropDown from "./switchDropDown";
|
||||||
@ -16,7 +16,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function DropDown({ question, openBranchingPage, setOpenBranchingPage }: Props) {
|
export default function DropDown({ question, openBranchingPage, setOpenBranchingPage }: Props) {
|
||||||
const {onClickAddAnAnswer} = useAddAnswer();
|
const {addVariantWithFocus, addVariantOnEnter, focusedVariantId, clearFocusedVariant} = useQuestionVariantsWithFocus();
|
||||||
const [switchState, setSwitchState] = useState("setting");
|
const [switchState, setSwitchState] = useState("setting");
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down(790));
|
const isMobile = useMediaQuery(theme.breakpoints.down(790));
|
||||||
@ -50,17 +50,17 @@ export default function DropDown({ question, openBranchingPage, setOpenBranching
|
|||||||
disableKeyDown={question.content.variants.length >= 100}
|
disableKeyDown={question.content.variants.length >= 100}
|
||||||
questionId={question.id}
|
questionId={question.id}
|
||||||
variant={variant}
|
variant={variant}
|
||||||
|
isOwn={Boolean(variant?.isOwn)}
|
||||||
|
ownPlaceholder={""}
|
||||||
|
shouldAutoFocus={focusedVariantId === variant.id}
|
||||||
|
onFocusHandled={clearFocusedVariant}
|
||||||
|
onEnterKeyPress={() => addVariantOnEnter(question.id)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Box
|
<Box sx={{ display: "flex", flexDirection: "column", alignItems: "flex-start", marginBottom: "20px" }}>
|
||||||
sx={{
|
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
marginBottom: "20px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Link
|
<Link
|
||||||
component="button"
|
component="button"
|
||||||
variant="body2"
|
variant="body2"
|
||||||
@ -71,7 +71,7 @@ export default function DropDown({ question, openBranchingPage, setOpenBranching
|
|||||||
mr: "4px",
|
mr: "4px",
|
||||||
height: "19px",
|
height: "19px",
|
||||||
}}
|
}}
|
||||||
onClick={() => onClickAddAnAnswer(question)}
|
onClick={() => addVariantWithFocus(question)}
|
||||||
>
|
>
|
||||||
Добавьте ответ
|
Добавьте ответ
|
||||||
</Link>
|
</Link>
|
||||||
@ -97,6 +97,20 @@ export default function DropDown({ question, openBranchingPage, setOpenBranching
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
{isMobile ? null : (
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontWeight: 400,
|
||||||
|
lineHeight: "21.33px",
|
||||||
|
color: theme.palette.grey2.main,
|
||||||
|
fontSize: "16px",
|
||||||
|
mt: "4px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
нажмите shift + enter для переноса строки
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<ButtonsOptions
|
<ButtonsOptions
|
||||||
switchState={switchState}
|
switchState={switchState}
|
||||||
@ -105,6 +119,7 @@ export default function DropDown({ question, openBranchingPage, setOpenBranching
|
|||||||
questionContentId={question.content.id}
|
questionContentId={question.content.id}
|
||||||
questionType={question.type}
|
questionType={question.type}
|
||||||
questionHasParent={question.content.rule.parentId?.length !== 0}
|
questionHasParent={question.content.rule.parentId?.length !== 0}
|
||||||
|
openBranchingPage={openBranchingPage}
|
||||||
setOpenBranchingPage={setOpenBranchingPage}
|
setOpenBranchingPage={setOpenBranchingPage}
|
||||||
/>
|
/>
|
||||||
<SwitchDropDown
|
<SwitchDropDown
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { EmojiPicker } from "@ui_kit/EmojiPicker";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { EnterIcon } from "@/assets/icons/questionsPage/enterIcon";
|
import { EnterIcon } from "@/assets/icons/questionsPage/enterIcon";
|
||||||
import type { QuizQuestionEmoji } from "@frontend/squzanswerer";
|
import type { QuizQuestionEmoji } from "@frontend/squzanswerer";
|
||||||
import { useAddAnswer } from "../../../utils/hooks/useAddAnswer";
|
import { useQuestionVariantsWithFocus } from "../../../utils/questionVariants";
|
||||||
import { AnswerDraggableList } from "../AnswerDraggableList";
|
import { AnswerDraggableList } from "../AnswerDraggableList";
|
||||||
import ButtonsOptions from "../QuestionOptions/ButtonsLayout/ButtonsOptions";
|
import ButtonsOptions from "../QuestionOptions/ButtonsLayout/ButtonsOptions";
|
||||||
import EmojiAnswerItem from "./EmojiAnswerItem/EmojiAnswerItem";
|
import EmojiAnswerItem from "./EmojiAnswerItem/EmojiAnswerItem";
|
||||||
@ -19,7 +19,7 @@ interface Props {
|
|||||||
|
|
||||||
export default function Emoji({ question, openBranchingPage, setOpenBranchingPage }: Props) {
|
export default function Emoji({ question, openBranchingPage, setOpenBranchingPage }: Props) {
|
||||||
const [switchState, setSwitchState] = useState<string>("setting");
|
const [switchState, setSwitchState] = useState<string>("setting");
|
||||||
const {onClickAddAnAnswer} = useAddAnswer();
|
const {addVariantWithFocus, addVariantOnEnter, focusedVariantId, clearFocusedVariant} = useQuestionVariantsWithFocus();
|
||||||
const [open, setOpen] = useState<boolean>(false);
|
const [open, setOpen] = useState<boolean>(false);
|
||||||
const [anchorElement, setAnchorElement] = useState<HTMLDivElement | null>(null);
|
const [anchorElement, setAnchorElement] = useState<HTMLDivElement | null>(null);
|
||||||
const [selectedVariant, setSelectedVariant] = useState<string | null>(null);
|
const [selectedVariant, setSelectedVariant] = useState<string | null>(null);
|
||||||
@ -47,7 +47,10 @@ export default function Emoji({ question, openBranchingPage, setOpenBranchingPag
|
|||||||
setOpen={setOpen}
|
setOpen={setOpen}
|
||||||
setSelectedVariant={setSelectedVariant}
|
setSelectedVariant={setSelectedVariant}
|
||||||
isOwn={Boolean(variant?.isOwn)}
|
isOwn={Boolean(variant?.isOwn)}
|
||||||
ownPlaceholder={question.content.ownPlaceholder}
|
ownPlaceholder={question.content.ownPlaceholder || ""}
|
||||||
|
shouldAutoFocus={focusedVariantId === variant.id}
|
||||||
|
onFocusHandled={clearFocusedVariant}
|
||||||
|
onEnterKeyPress={() => addVariantOnEnter(question.id)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
/>
|
/>
|
||||||
@ -81,19 +84,13 @@ export default function Emoji({ question, openBranchingPage, setOpenBranchingPag
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Popover>
|
</Popover>
|
||||||
<Box
|
<Box sx={{ display: "flex", flexDirection: "column", alignItems: "flex-start", marginBottom: isMobile ? "17px" : "20px" }}>
|
||||||
sx={{
|
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: "10px",
|
|
||||||
marginBottom: isMobile ? "17px" : "20px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Link
|
<Link
|
||||||
component="button"
|
component="button"
|
||||||
variant="body2"
|
variant="body2"
|
||||||
sx={{ color: theme.palette.brightPurple.main }}
|
sx={{ color: theme.palette.brightPurple.main }}
|
||||||
onClick={() => onClickAddAnAnswer(question)}
|
onClick={() => addVariantWithFocus(question)}
|
||||||
>
|
>
|
||||||
Добавьте ответ
|
Добавьте ответ
|
||||||
</Link>
|
</Link>
|
||||||
@ -119,6 +116,20 @@ export default function Emoji({ question, openBranchingPage, setOpenBranchingPag
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
{!isTablet && (
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontWeight: 400,
|
||||||
|
lineHeight: "21.33px",
|
||||||
|
color: theme.palette.grey2.main,
|
||||||
|
fontSize: "16px",
|
||||||
|
mt: "4px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
для переноса строки нажмите shift + enter
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<ButtonsOptions
|
<ButtonsOptions
|
||||||
switchState={switchState}
|
switchState={switchState}
|
||||||
@ -127,6 +138,7 @@ export default function Emoji({ question, openBranchingPage, setOpenBranchingPag
|
|||||||
questionContentId={question.content.id}
|
questionContentId={question.content.id}
|
||||||
questionType={question.type}
|
questionType={question.type}
|
||||||
questionHasParent={question.content.rule.parentId?.length !== 0}
|
questionHasParent={question.content.rule.parentId?.length !== 0}
|
||||||
|
openBranchingPage={openBranchingPage}
|
||||||
setOpenBranchingPage={setOpenBranchingPage}
|
setOpenBranchingPage={setOpenBranchingPage}
|
||||||
/>
|
/>
|
||||||
<SwitchEmoji
|
<SwitchEmoji
|
||||||
|
|||||||
@ -14,6 +14,9 @@ type Props = Omit<
|
|||||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
isOwn: boolean;
|
isOwn: boolean;
|
||||||
ownPlaceholder: string;
|
ownPlaceholder: string;
|
||||||
|
shouldAutoFocus?: boolean;
|
||||||
|
onFocusHandled?: () => void;
|
||||||
|
onEnterKeyPress?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function EmojiAnswerItem({
|
export default function EmojiAnswerItem({
|
||||||
@ -28,6 +31,9 @@ export default function EmojiAnswerItem({
|
|||||||
setOpen,
|
setOpen,
|
||||||
isOwn,
|
isOwn,
|
||||||
ownPlaceholder,
|
ownPlaceholder,
|
||||||
|
shouldAutoFocus,
|
||||||
|
onFocusHandled,
|
||||||
|
onEnterKeyPress,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
|
|
||||||
|
|
||||||
@ -99,6 +105,9 @@ export default function EmojiAnswerItem({
|
|||||||
additionalMobile={addOrEditImageButtonMobile}
|
additionalMobile={addOrEditImageButtonMobile}
|
||||||
isOwn={isOwn}
|
isOwn={isOwn}
|
||||||
ownPlaceholder={ownPlaceholder}
|
ownPlaceholder={ownPlaceholder}
|
||||||
|
shouldAutoFocus={shouldAutoFocus}
|
||||||
|
onFocusHandled={onFocusHandled}
|
||||||
|
onEnterKeyPress={onEnterKeyPress}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { updateQuestion } from "@root/questions/actions";
|
|||||||
import CustomCheckbox from "@ui_kit/CustomCheckbox";
|
import CustomCheckbox from "@ui_kit/CustomCheckbox";
|
||||||
import { memo } from "react";
|
import { memo } from "react";
|
||||||
import CustomTextField from "@ui_kit/CustomTextField";
|
import CustomTextField from "@ui_kit/CustomTextField";
|
||||||
import { useAddAnswer } from "@/utils/hooks/useAddAnswer";
|
import { useQuestionVariantsWithFocus } from "@/utils/questionVariants";
|
||||||
|
|
||||||
type SettingEmojiProps = {
|
type SettingEmojiProps = {
|
||||||
question: QuizQuestionEmoji;
|
question: QuizQuestionEmoji;
|
||||||
@ -17,7 +17,7 @@ type SettingEmojiProps = {
|
|||||||
|
|
||||||
const SettingEmoji = memo<SettingEmojiProps>(function ({ question, questionId, isRequired, isLargeCheck, isMulti, isOwn }) {
|
const SettingEmoji = memo<SettingEmojiProps>(function ({ question, questionId, isRequired, isLargeCheck, isMulti, isOwn }) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const {switchOwn} = useAddAnswer();
|
const {switchOwnVariant} = useQuestionVariantsWithFocus();
|
||||||
|
|
||||||
const isWrappColumn = useMediaQuery(theme.breakpoints.down(980));
|
const isWrappColumn = useMediaQuery(theme.breakpoints.down(980));
|
||||||
const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990));
|
const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990));
|
||||||
@ -92,7 +92,7 @@ const SettingEmoji = memo<SettingEmojiProps>(function ({ question, questionId, i
|
|||||||
label={'Вариант "свой ответ"'}
|
label={'Вариант "свой ответ"'}
|
||||||
checked={isOwn}
|
checked={isOwn}
|
||||||
handleChange={({ target }) => {
|
handleChange={({ target }) => {
|
||||||
switchOwn({question, checked:target.checked})
|
switchOwnVariant({question, checked:target.checked})
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/* <Box sx={{ mt: isMobile ? "11px" : "6px", width: "100%" }}>
|
{/* <Box sx={{ mt: isMobile ? "11px" : "6px", width: "100%" }}>
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import { Box, Link, Typography, useMediaQuery, useTheme } from "@mui/material";
|
import { Box, Link, Typography, useMediaQuery, useTheme } from "@mui/material";
|
||||||
import {
|
import {
|
||||||
addQuestionVariant,
|
|
||||||
clearQuestionImages,
|
clearQuestionImages,
|
||||||
uploadQuestionImage,
|
uploadQuestionImage,
|
||||||
} from "@root/questions/actions";
|
} from "@root/questions/actions";
|
||||||
|
import { useQuestionVariantsWithFocus } from "@/utils/questionVariants";
|
||||||
import { useCurrentQuiz } from "@root/quizes/hooks";
|
import { useCurrentQuiz } from "@root/quizes/hooks";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { EnterIcon } from "@/assets/icons/questionsPage/enterIcon";
|
import { EnterIcon } from "@/assets/icons/questionsPage/enterIcon";
|
||||||
@ -31,6 +31,7 @@ export default function OptionsAndPicture({
|
|||||||
const [switchState, setSwitchState] = useState("setting");
|
const [switchState, setSwitchState] = useState("setting");
|
||||||
const [pictureUploding, setPictureUploading] = useState<boolean>(false);
|
const [pictureUploding, setPictureUploading] = useState<boolean>(false);
|
||||||
const [openCropModal, setOpenCropModal] = useState(false);
|
const [openCropModal, setOpenCropModal] = useState(false);
|
||||||
|
const {addVariantWithFocus, addVariantOnEnter, focusedVariantId, clearFocusedVariant} = useQuestionVariantsWithFocus();
|
||||||
|
|
||||||
const [selectedVariantId, setSelectedVariantId] = useState<string | null>(
|
const [selectedVariantId, setSelectedVariantId] = useState<string | null>(
|
||||||
null,
|
null,
|
||||||
@ -111,6 +112,9 @@ export default function OptionsAndPicture({
|
|||||||
setSelectedVariantId={setSelectedVariantId}
|
setSelectedVariantId={setSelectedVariantId}
|
||||||
isOwn={Boolean(variant?.isOwn)}
|
isOwn={Boolean(variant?.isOwn)}
|
||||||
ownPlaceholder={question.content.ownPlaceholder}
|
ownPlaceholder={question.content.ownPlaceholder}
|
||||||
|
shouldAutoFocus={focusedVariantId === variant.id}
|
||||||
|
onFocusHandled={clearFocusedVariant}
|
||||||
|
onEnterKeyPress={() => addVariantOnEnter(question.id)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
/>
|
/>
|
||||||
@ -130,13 +134,8 @@ export default function OptionsAndPicture({
|
|||||||
selfClose={() => setOpenCropModal(false)}
|
selfClose={() => setOpenCropModal(false)}
|
||||||
setPictureUploading={setPictureUploading}
|
setPictureUploading={setPictureUploading}
|
||||||
/>
|
/>
|
||||||
<Box
|
<Box sx={{ display: "flex", flexDirection: "column", alignItems: "flex-start", marginBottom: "17px" }}>
|
||||||
sx={{
|
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
marginBottom: "17px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Link
|
<Link
|
||||||
component="button"
|
component="button"
|
||||||
variant="body2"
|
variant="body2"
|
||||||
@ -148,7 +147,7 @@ export default function OptionsAndPicture({
|
|||||||
height: "19px",
|
height: "19px",
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
addQuestionVariant(question.id);
|
addVariantWithFocus(question);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Добавьте ответ
|
Добавьте ответ
|
||||||
@ -175,6 +174,20 @@ export default function OptionsAndPicture({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
{isMobile ? null : (
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontWeight: 400,
|
||||||
|
lineHeight: "21.33px",
|
||||||
|
color: theme.palette.grey2.main,
|
||||||
|
fontSize: "16px",
|
||||||
|
mt: "4px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
для переноса строки нажмите shift + enter
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<ButtonsOptions
|
<ButtonsOptions
|
||||||
switchState={switchState}
|
switchState={switchState}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { useAddAnswer } from "@/utils/hooks/useAddAnswer";
|
import { useQuestionVariantsWithFocus } from "@/utils/questionVariants";
|
||||||
import type { QuizQuestionVarImg, QuizQuestionVariant } from "@frontend/squzanswerer";
|
import type { QuizQuestionVarImg, QuizQuestionVariant } from "@frontend/squzanswerer";
|
||||||
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
|
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
|
||||||
import { updateQuestion } from "@root/questions/actions";
|
import { updateQuestion } from "@root/questions/actions";
|
||||||
@ -19,7 +19,7 @@ type SettingOptionsAndPictProps = {
|
|||||||
|
|
||||||
const SettingOptionsAndPict = memo<SettingOptionsAndPictProps>(function ({ question, questionId, ownPlaceholder, isMulti, isLargeCheck, replText, isRequired, isOwn }) {
|
const SettingOptionsAndPict = memo<SettingOptionsAndPictProps>(function ({ question, questionId, ownPlaceholder, isMulti, isLargeCheck, replText, isRequired, isOwn }) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { switchOwn } = useAddAnswer();
|
const { switchOwnVariant } = useQuestionVariantsWithFocus();
|
||||||
|
|
||||||
const isWrappColumn = useMediaQuery(theme.breakpoints.down(980));
|
const isWrappColumn = useMediaQuery(theme.breakpoints.down(980));
|
||||||
const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990));
|
const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990));
|
||||||
@ -73,7 +73,7 @@ const SettingOptionsAndPict = memo<SettingOptionsAndPictProps>(function ({ quest
|
|||||||
label={'Вариант "свой ответ"'}
|
label={'Вариант "свой ответ"'}
|
||||||
checked={isOwn}
|
checked={isOwn}
|
||||||
handleChange={({ target }) => {
|
handleChange={({ target }) => {
|
||||||
switchOwn({ question, checked: target.checked })
|
switchOwnVariant({ question, checked: target.checked })
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<CustomCheckbox
|
<CustomCheckbox
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import { EnterIcon } from "@/assets/icons/questionsPage/enterIcon";
|
|||||||
import type { QuizQuestionVarImg } from "@frontend/squzanswerer/dist-package/model/questionTypes/varimg";
|
import type { QuizQuestionVarImg } from "@frontend/squzanswerer/dist-package/model/questionTypes/varimg";
|
||||||
|
|
||||||
//@/model/questionTypes/images";
|
//@/model/questionTypes/images";
|
||||||
import { useAddAnswer } from "@/utils/hooks/useAddAnswer";
|
import { useQuestionVariantsWithFocus } from "@/utils/questionVariants";
|
||||||
import { useDisclosure } from "@/utils/useDisclosure";
|
import { useDisclosure } from "@/utils/useDisclosure";
|
||||||
import { AnswerDraggableList } from "../../AnswerDraggableList";
|
import { AnswerDraggableList } from "../../AnswerDraggableList";
|
||||||
import ImageEditAnswerItem from "../../AnswerDraggableList/ImageEditAnswerItem";
|
import ImageEditAnswerItem from "../../AnswerDraggableList/ImageEditAnswerItem";
|
||||||
@ -31,7 +31,7 @@ export default function OptionsPicture({
|
|||||||
setOpenBranchingPage,
|
setOpenBranchingPage,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const {onClickAddAnAnswer} = useAddAnswer();
|
const {addVariantWithFocus, addVariantOnEnter, focusedVariantId, clearFocusedVariant} = useQuestionVariantsWithFocus();
|
||||||
const quizQid = useCurrentQuiz()?.qid;
|
const quizQid = useCurrentQuiz()?.qid;
|
||||||
const [pictureUploding, setPictureUploading] = useState<boolean>(false);
|
const [pictureUploding, setPictureUploading] = useState<boolean>(false);
|
||||||
const [openCropModal, setOpenCropModal] = useState(false);
|
const [openCropModal, setOpenCropModal] = useState(false);
|
||||||
@ -87,12 +87,15 @@ export default function OptionsPicture({
|
|||||||
largeCheck={question.content.largeCheck}
|
largeCheck={question.content.largeCheck}
|
||||||
variant={variant}
|
variant={variant}
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
openCropModal={() => {setOpenCropModal(true)}}
|
openCropModal={() => { setOpenCropModal(true); return Promise.resolve(); }}
|
||||||
openImageUploadModal={openImageUploadModal}
|
openImageUploadModal={openImageUploadModal}
|
||||||
pictureUploding={pictureUploding}
|
pictureUploding={pictureUploding}
|
||||||
setSelectedVariantId={setSelectedVariantId}
|
setSelectedVariantId={setSelectedVariantId}
|
||||||
isOwn={Boolean(variant?.isOwn)}
|
isOwn={Boolean(variant?.isOwn)}
|
||||||
ownPlaceholder={question.content.ownPlaceholder}
|
ownPlaceholder={question.content.ownPlaceholder || ""}
|
||||||
|
shouldAutoFocus={focusedVariantId === variant.id}
|
||||||
|
onFocusHandled={clearFocusedVariant}
|
||||||
|
onEnterKeyPress={() => addVariantOnEnter(question.id)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
/>
|
/>
|
||||||
@ -102,22 +105,23 @@ export default function OptionsPicture({
|
|||||||
handleImageChange={handleImageUpload}
|
handleImageChange={handleImageUpload}
|
||||||
/>
|
/>
|
||||||
<CropModalInit
|
<CropModalInit
|
||||||
originalImageUrl={variant?.originalImageUrl}
|
originalImageUrl={variant?.originalImageUrl ?? ""}
|
||||||
editedUrlImagesList={variant?.editedUrlImagesList}
|
editedUrlImagesList={(variant?.editedUrlImagesList as any) ?? undefined}
|
||||||
questionId={question.id.toString()}
|
questionId={question.id.toString()}
|
||||||
questionType={question.type}
|
questionType={question.type as any}
|
||||||
quizId={quizQid}
|
quizId={quizQid ?? ""}
|
||||||
variantId={variant?.id}
|
variantId={variant?.id ?? ""}
|
||||||
open={openCropModal}
|
open={openCropModal}
|
||||||
selfClose={() => setOpenCropModal(false)}
|
selfClose={() => setOpenCropModal(false)}
|
||||||
setPictureUploading={setPictureUploading}
|
setPictureUploading={setPictureUploading as any}
|
||||||
/>
|
/>
|
||||||
<Box sx={{ display: "flex", alignItems: "center", gap: "10px" }}>
|
<Box sx={{ display: "flex", flexDirection: "column", alignItems: "flex-start" }}>
|
||||||
|
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||||
<Link
|
<Link
|
||||||
component="button"
|
component="button"
|
||||||
variant="body2"
|
variant="body2"
|
||||||
sx={{ color: theme.palette.brightPurple.main }}
|
sx={{ color: theme.palette.brightPurple.main }}
|
||||||
onClick={() => onClickAddAnAnswer(question)}
|
onClick={() => addVariantWithFocus(question)}
|
||||||
>
|
>
|
||||||
Добавьте ответ
|
Добавьте ответ
|
||||||
</Link>
|
</Link>
|
||||||
@ -143,6 +147,20 @@ export default function OptionsPicture({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
{isMobile ? null : (
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontWeight: 400,
|
||||||
|
lineHeight: "21.33px",
|
||||||
|
color: theme.palette.grey2.main,
|
||||||
|
fontSize: "16px",
|
||||||
|
mt: "4px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
для переноса строки нажмите shift + enter
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<ButtonsOptions
|
<ButtonsOptions
|
||||||
switchState={switchState}
|
switchState={switchState}
|
||||||
@ -151,11 +169,12 @@ export default function OptionsPicture({
|
|||||||
questionContentId={question.content.id}
|
questionContentId={question.content.id}
|
||||||
questionType={question.type}
|
questionType={question.type}
|
||||||
questionHasParent={question.content.rule.parentId?.length !== 0}
|
questionHasParent={question.content.rule.parentId?.length !== 0}
|
||||||
|
openBranchingPage={openBranchingPage}
|
||||||
setOpenBranchingPage={setOpenBranchingPage}
|
setOpenBranchingPage={setOpenBranchingPage}
|
||||||
/>
|
/>
|
||||||
<SwitchAnswerOptionsPict
|
<SwitchAnswerOptionsPict
|
||||||
switchState={switchState}
|
switchState={switchState}
|
||||||
question={question}
|
question={question as any}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import ProportionsIcon11 from "@/assets/icons/questionsPage/ProportionsIcon11";
|
|||||||
import ProportionsIcon12 from "@/assets/icons/questionsPage/ProportionsIcon12";
|
import ProportionsIcon12 from "@/assets/icons/questionsPage/ProportionsIcon12";
|
||||||
import ProportionsIcon21 from "@/assets/icons/questionsPage/ProportionsIcon21";
|
import ProportionsIcon21 from "@/assets/icons/questionsPage/ProportionsIcon21";
|
||||||
import CustomTextField from "@ui_kit/CustomTextField";
|
import CustomTextField from "@ui_kit/CustomTextField";
|
||||||
import { useAddAnswer } from "@/utils/hooks/useAddAnswer";
|
import { useQuestionVariantsWithFocus } from "@/utils/questionVariants";
|
||||||
|
|
||||||
type Proportion = "1:1" | "1:2" | "2:1";
|
type Proportion = "1:1" | "1:2" | "2:1";
|
||||||
|
|
||||||
@ -69,7 +69,7 @@ const SettingOptionsPict = memo<SettingOptionsPictProps>(function ({
|
|||||||
question.content.ownPlaceholder = replText;
|
question.content.ownPlaceholder = replText;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
const {switchOwn} = useAddAnswer();
|
const {switchOwnVariant} = useQuestionVariantsWithFocus();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@ -175,7 +175,7 @@ const SettingOptionsPict = memo<SettingOptionsPictProps>(function ({
|
|||||||
label={'Вариант "свой ответ"'}
|
label={'Вариант "свой ответ"'}
|
||||||
checked={isOwn}
|
checked={isOwn}
|
||||||
handleChange={({ target }) => {
|
handleChange={({ target }) => {
|
||||||
switchOwn({question, checked:target.checked})
|
switchOwnVariant({question, checked:target.checked})
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/* <Box sx={{ mt: isMobile ? "11px" : "6px", width: "100%" }}>
|
{/* <Box sx={{ mt: isMobile ? "11px" : "6px", width: "100%" }}>
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import ProportionsIcon11 from "@/assets/icons/questionsPage/ProportionsIcon11";
|
|||||||
import ProportionsIcon12 from "@/assets/icons/questionsPage/ProportionsIcon12";
|
import ProportionsIcon12 from "@/assets/icons/questionsPage/ProportionsIcon12";
|
||||||
import ProportionsIcon21 from "@/assets/icons/questionsPage/ProportionsIcon21";
|
import ProportionsIcon21 from "@/assets/icons/questionsPage/ProportionsIcon21";
|
||||||
import CustomTextField from "@ui_kit/CustomTextField";
|
import CustomTextField from "@ui_kit/CustomTextField";
|
||||||
import { useAddAnswer } from "@/utils/hooks/useAddAnswer";
|
import { useQuestionVariantsWithFocus } from "@/utils/questionVariants";
|
||||||
|
|
||||||
type Proportion = "1:1" | "1:2" | "2:1";
|
type Proportion = "1:1" | "1:2" | "2:1";
|
||||||
|
|
||||||
@ -69,7 +69,7 @@ const SettingOptionsPict = memo<SettingOptionsPictProps>(function ({
|
|||||||
question.content.ownPlaceholder = replText;
|
question.content.ownPlaceholder = replText;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
const {switchOwn} = useAddAnswer();
|
const {switchOwnVariant} = useQuestionVariantsWithFocus();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@ -175,7 +175,7 @@ const SettingOptionsPict = memo<SettingOptionsPictProps>(function ({
|
|||||||
label={'Вариант "свой ответ"'}
|
label={'Вариант "свой ответ"'}
|
||||||
checked={isOwn}
|
checked={isOwn}
|
||||||
handleChange={({ target }) => {
|
handleChange={({ target }) => {
|
||||||
switchOwn({question, checked:target.checked})
|
switchOwnVariant({question, checked:target.checked})
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/* <Box sx={{ mt: isMobile ? "11px" : "6px", width: "100%" }}>
|
{/* <Box sx={{ mt: isMobile ? "11px" : "6px", width: "100%" }}>
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { Box, Link, Typography, useMediaQuery, useTheme } from "@mui/material";
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { EnterIcon } from "@/assets/icons/questionsPage/enterIcon";
|
import { EnterIcon } from "@/assets/icons/questionsPage/enterIcon";
|
||||||
import type { QuizQuestionVariant } from "@frontend/squzanswerer";
|
import type { QuizQuestionVariant } from "@frontend/squzanswerer";
|
||||||
import { useAddAnswer } from "@/utils/hooks/useAddAnswer";
|
import { useQuestionVariantsWithFocus } from "@/utils/questionVariants";
|
||||||
import { AnswerDraggableList } from "../../AnswerDraggableList";
|
import { AnswerDraggableList } from "../../AnswerDraggableList";
|
||||||
import AnswerItem from "../../AnswerDraggableList/AnswerItem";
|
import AnswerItem from "../../AnswerDraggableList/AnswerItem";
|
||||||
import ButtonsOptions from "../ButtonsLayout/ButtonsOptions";
|
import ButtonsOptions from "../ButtonsLayout/ButtonsOptions";
|
||||||
@ -15,7 +15,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function AnswerOptions({ question, openBranchingPage, setOpenBranchingPage }: Props) {
|
export default function AnswerOptions({ question, openBranchingPage, setOpenBranchingPage }: Props) {
|
||||||
const {onClickAddAnAnswer} = useAddAnswer();
|
const {addVariantWithFocus, addVariantOnEnter, focusedVariantId, clearFocusedVariant} = useQuestionVariantsWithFocus();
|
||||||
const [switchState, setSwitchState] = useState("setting");
|
const [switchState, setSwitchState] = useState("setting");
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down(790));
|
const isMobile = useMediaQuery(theme.breakpoints.down(790));
|
||||||
@ -54,19 +54,17 @@ export default function AnswerOptions({ question, openBranchingPage, setOpenBran
|
|||||||
questionId={question.id}
|
questionId={question.id}
|
||||||
variant={variant}
|
variant={variant}
|
||||||
isOwn={Boolean(variant.isOwn)}
|
isOwn={Boolean(variant.isOwn)}
|
||||||
ownPlaceholder={question.content.ownPlaceholder}
|
ownPlaceholder={question.content.ownPlaceholder || ""}
|
||||||
|
shouldAutoFocus={focusedVariantId === variant.id}
|
||||||
|
onFocusHandled={clearFocusedVariant}
|
||||||
|
onEnterKeyPress={() => addVariantOnEnter(question.id)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Box
|
<Box sx={{ display: "flex", flexDirection: "column", alignItems: "flex-start", marginBottom: "17px" }}>
|
||||||
sx={{
|
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
marginBottom: "17px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Link
|
<Link
|
||||||
component="button"
|
component="button"
|
||||||
variant="body2"
|
variant="body2"
|
||||||
@ -77,7 +75,7 @@ export default function AnswerOptions({ question, openBranchingPage, setOpenBran
|
|||||||
mr: "4px",
|
mr: "4px",
|
||||||
height: "19px",
|
height: "19px",
|
||||||
}}
|
}}
|
||||||
onClick={() => onClickAddAnAnswer(question)}
|
onClick={() => addVariantWithFocus(question)}
|
||||||
>
|
>
|
||||||
Добавьте ответ
|
Добавьте ответ
|
||||||
</Link>
|
</Link>
|
||||||
@ -103,6 +101,20 @@ export default function AnswerOptions({ question, openBranchingPage, setOpenBran
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
{isMobile ? null : (
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontWeight: 400,
|
||||||
|
lineHeight: "21.33px",
|
||||||
|
color: theme.palette.grey2.main,
|
||||||
|
fontSize: "16px",
|
||||||
|
mt: "4px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
для переноса строки нажмите shift + enter
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<ButtonsOptions
|
<ButtonsOptions
|
||||||
switchState={switchState}
|
switchState={switchState}
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import CustomCheckbox from "@ui_kit/CustomCheckbox";
|
|||||||
import type { QuizQuestionVariant } from "@frontend/squzanswerer";
|
import type { QuizQuestionVariant } from "@frontend/squzanswerer";
|
||||||
import { memo } from "react";
|
import { memo } from "react";
|
||||||
import CustomTextField from "@ui_kit/CustomTextField";
|
import CustomTextField from "@ui_kit/CustomTextField";
|
||||||
import { useAddAnswer } from "@/utils/hooks/useAddAnswer";
|
import { useQuestionVariantsWithFocus } from "@/utils/questionVariants";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
question: QuizQuestionVariant;
|
question: QuizQuestionVariant;
|
||||||
@ -21,7 +21,7 @@ const ResponseSettings = memo<Props>(function ({question, questionId, ownPlaceho
|
|||||||
const isTablet = useMediaQuery(theme.breakpoints.down(900));
|
const isTablet = useMediaQuery(theme.breakpoints.down(900));
|
||||||
const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990));
|
const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990));
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down(790));
|
const isMobile = useMediaQuery(theme.breakpoints.down(790));
|
||||||
const {switchOwn} = useAddAnswer();
|
const {switchOwnVariant} = useQuestionVariantsWithFocus();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@ -84,7 +84,7 @@ const ResponseSettings = memo<Props>(function ({question, questionId, ownPlaceho
|
|||||||
label={'Вариант "свой ответ"'}
|
label={'Вариант "свой ответ"'}
|
||||||
checked={isOwn}
|
checked={isOwn}
|
||||||
handleChange={({ target }) => {
|
handleChange={({ target }) => {
|
||||||
switchOwn({question, checked:target.checked})
|
switchOwnVariant({question, checked:target.checked})
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@ -55,7 +55,6 @@ export default function RecoverPassword() {
|
|||||||
initialValues,
|
initialValues,
|
||||||
validationSchema,
|
validationSchema,
|
||||||
onSubmit: async (values, formikHelpers) => {
|
onSubmit: async (values, formikHelpers) => {
|
||||||
console.log("tokenUser", tokenUser);
|
|
||||||
|
|
||||||
if (tokenUser) {
|
if (tokenUser) {
|
||||||
setAuthToken(tokenUser || "");
|
setAuthToken(tokenUser || "");
|
||||||
@ -77,11 +76,8 @@ export default function RecoverPassword() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
useEffect(() => {
|
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 params = new URLSearchParams(window.location.search);
|
||||||
const authToken = params.get("auth");
|
const authToken = params.get("auth");
|
||||||
console.log("RecoverPassword useEffect - authToken:", authToken);
|
|
||||||
setTokenUser(authToken || "");
|
setTokenUser(authToken || "");
|
||||||
|
|
||||||
history.pushState(null, document.title, "/changepwd");
|
history.pushState(null, document.title, "/changepwd");
|
||||||
|
|||||||
@ -22,7 +22,7 @@ import { ModalInfoWhyCantCreate } from "./ModalInfoWhyCantCreate";
|
|||||||
import { ConfirmLeaveModal } from "./ConfirmLeaveModal";
|
import { ConfirmLeaveModal } from "./ConfirmLeaveModal";
|
||||||
import { checkQuestionHint } from "@utils/checkQuestionHint";
|
import { checkQuestionHint } from "@utils/checkQuestionHint";
|
||||||
import AmoTokenExpiredDialog from "../IntegrationsPage/IntegrationsModal/Amo/AmoTokenExpiredDialog";
|
import AmoTokenExpiredDialog from "../IntegrationsPage/IntegrationsModal/Amo/AmoTokenExpiredDialog";
|
||||||
import { useAmoAccount } from "@/api/integration";
|
import pinkScrollbar from "@utils/pinkScrollbar";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
openBranchingPage: boolean;
|
openBranchingPage: boolean;
|
||||||
@ -121,6 +121,7 @@ export default function EditPage({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
|
id="lol"
|
||||||
onScroll={onScroll}
|
onScroll={onScroll}
|
||||||
ref={mainBlock}
|
ref={mainBlock}
|
||||||
sx={{
|
sx={{
|
||||||
@ -140,6 +141,7 @@ export default function EditPage({
|
|||||||
boxSizing: "border-box",
|
boxSizing: "border-box",
|
||||||
transition: "transform 0.3s",
|
transition: "transform 0.3s",
|
||||||
paddingBottom: scrollDown && isMobile ? "120px" : undefined,
|
paddingBottom: scrollDown && isMobile ? "120px" : undefined,
|
||||||
|
...pinkScrollbar(theme)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{quizConfig && (
|
{quizConfig && (
|
||||||
|
|||||||
@ -22,6 +22,7 @@ import {
|
|||||||
MenuItem,
|
MenuItem,
|
||||||
Select,
|
Select,
|
||||||
Skeleton,
|
Skeleton,
|
||||||
|
CircularProgress,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Typography,
|
Typography,
|
||||||
useMediaQuery,
|
useMediaQuery,
|
||||||
@ -35,7 +36,7 @@ import QuestionTimerSettings from "./QuestionTimerSettings";
|
|||||||
import SelectableButton from "@ui_kit/SelectableButton";
|
import SelectableButton from "@ui_kit/SelectableButton";
|
||||||
import { StartPagePreview } from "@ui_kit/StartPagePreview";
|
import { StartPagePreview } from "@ui_kit/StartPagePreview";
|
||||||
import { resizeFavIcon } from "@ui_kit/reactImageFileResizer";
|
import { resizeFavIcon } from "@ui_kit/reactImageFileResizer";
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import FaviconDropZone from "../FaviconDropZone";
|
import FaviconDropZone from "../FaviconDropZone";
|
||||||
import ModalSizeImage from "../ModalSizeImage";
|
import ModalSizeImage from "../ModalSizeImage";
|
||||||
@ -69,23 +70,37 @@ export default function StartPageSettings() {
|
|||||||
|
|
||||||
if (!quiz) return null;
|
if (!quiz) return null;
|
||||||
|
|
||||||
|
// Диагностика видимости скелетона загрузки видео
|
||||||
|
useEffect(() => {
|
||||||
|
console.log("[StartPage] backgroundUploding state:", backgroundUploding, {
|
||||||
|
hasVideo: Boolean(quiz?.config.startpage.background.video),
|
||||||
|
type: quiz?.config.startpage.background.type,
|
||||||
|
});
|
||||||
|
}, [backgroundUploding, quiz?.config.startpage.background.video, quiz?.config.startpage.background.type]);
|
||||||
|
|
||||||
async function handleVideoUpload(videoUrl: string) {
|
async function handleVideoUpload(videoUrl: string) {
|
||||||
if (!quiz) return;
|
if (!quiz) return;
|
||||||
|
|
||||||
setBackgroundUploading(true);
|
console.log("[StartPage] Video upload start", { videoUrl });
|
||||||
|
|
||||||
if (videoUrl.startsWith("blob:")) {
|
if (videoUrl.startsWith("blob:")) {
|
||||||
|
setBackgroundUploading(true);
|
||||||
const videoBlob = await (await fetch(videoUrl)).blob();
|
const videoBlob = await (await fetch(videoUrl)).blob();
|
||||||
|
console.log("[StartPage] Uploading blob to backend", { size: videoBlob.size, type: videoBlob.type });
|
||||||
uploadQuizImage(quiz.id, videoBlob, (quiz, url) => {
|
uploadQuizImage(quiz.id, videoBlob, (quiz, url) => {
|
||||||
quiz.config.startpage.background.video = url;
|
quiz.config.startpage.background.video = url;
|
||||||
|
console.log("[StartPage] Backend returned video URL", { url });
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
// для ссылок скелетон не нужен
|
||||||
|
setBackgroundUploading(false);
|
||||||
updateQuiz(quiz.id, (quiz) => {
|
updateQuiz(quiz.id, (quiz) => {
|
||||||
quiz.config.startpage.background.video = videoUrl;
|
quiz.config.startpage.background.video = videoUrl;
|
||||||
});
|
});
|
||||||
|
console.log("[StartPage] Set external video URL", { videoUrl });
|
||||||
}
|
}
|
||||||
|
|
||||||
setBackgroundUploading(false);
|
// Не сбрасываем backgroundUploding здесь — ждём onLoaded от VideoElement
|
||||||
|
console.log("[StartPage] Waiting for video preview to load...");
|
||||||
}
|
}
|
||||||
|
|
||||||
const designType = quiz?.config?.startpageType;
|
const designType = quiz?.config?.startpageType;
|
||||||
@ -376,14 +391,7 @@ export default function StartPageSettings() {
|
|||||||
|
|
||||||
setBackgroundUploading(false);
|
setBackgroundUploading(false);
|
||||||
}}
|
}}
|
||||||
onImageSaveClick={async (file) => {
|
|
||||||
setBackgroundUploading(true);
|
|
||||||
await uploadQuizImage(quiz.id, file, (quiz, url) => {
|
|
||||||
quiz.config.startpage.background.desktop = url;
|
|
||||||
});
|
|
||||||
|
|
||||||
setBackgroundUploading(false);
|
|
||||||
}}
|
|
||||||
onDeleteClick={() => {
|
onDeleteClick={() => {
|
||||||
updateQuiz(quiz.id, (quiz) => {
|
updateQuiz(quiz.id, (quiz) => {
|
||||||
quiz.config.startpage.background.desktop = null;
|
quiz.config.startpage.background.desktop = null;
|
||||||
@ -433,11 +441,12 @@ export default function StartPageSettings() {
|
|||||||
</Box>
|
</Box>
|
||||||
{backgroundUploding ? (
|
{backgroundUploding ? (
|
||||||
<Skeleton
|
<Skeleton
|
||||||
|
variant="rounded"
|
||||||
sx={{
|
sx={{
|
||||||
width: "48px",
|
width: "300px",
|
||||||
height: "48px",
|
height: "168px",
|
||||||
transform: "none",
|
transform: "none",
|
||||||
margin: "20px 0",
|
my: "20px",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
@ -465,16 +474,37 @@ export default function StartPageSettings() {
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Box sx={{ marginTop: "20px" }}>
|
<Box sx={{ position: "relative", marginTop: "20px", overflow: "hidden" }}>
|
||||||
<VideoElement
|
<VideoElement
|
||||||
videoSrc={quiz.config.startpage.background.video}
|
videoSrc={quiz.config.startpage.background.video}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
|
onLoaded={() => {
|
||||||
|
console.log('[StartPage] VideoElement reported loaded');
|
||||||
|
setBackgroundUploading(false);
|
||||||
|
}}
|
||||||
|
showSkeleton={backgroundUploding}
|
||||||
onDeleteClick={() => {
|
onDeleteClick={() => {
|
||||||
updateQuiz(quiz.id, (quiz) => {
|
updateQuiz(quiz.id, (quiz) => {
|
||||||
quiz.config.startpage.background.video = null;
|
quiz.config.startpage.background.video = null;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{backgroundUploding && (
|
||||||
|
<Skeleton
|
||||||
|
variant="rounded"
|
||||||
|
animation="wave"
|
||||||
|
sx={{
|
||||||
|
position: "absolute",
|
||||||
|
inset: 0,
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
borderRadius: "8px",
|
||||||
|
zIndex: (t) => t.zIndex.modal + 1,
|
||||||
|
bgcolor: (t) => t.palette.action.hover,
|
||||||
|
pointerEvents: "none",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@ -570,14 +600,7 @@ export default function StartPageSettings() {
|
|||||||
|
|
||||||
setLogoUploading(false);
|
setLogoUploading(false);
|
||||||
}}
|
}}
|
||||||
onImageSaveClick={async (file) => {
|
|
||||||
setLogoUploading(true);
|
|
||||||
await uploadQuizImage(quiz.id, file, (quiz, url) => {
|
|
||||||
quiz.config.startpage.logo = url;
|
|
||||||
});
|
|
||||||
|
|
||||||
setLogoUploading(false);
|
|
||||||
}}
|
|
||||||
onDeleteClick={() => {
|
onDeleteClick={() => {
|
||||||
updateQuiz(quiz.id, (quiz) => {
|
updateQuiz(quiz.id, (quiz) => {
|
||||||
quiz.config.startpage.logo = null;
|
quiz.config.startpage.logo = null;
|
||||||
@ -650,14 +673,7 @@ export default function StartPageSettings() {
|
|||||||
|
|
||||||
setLogoUploading(false);
|
setLogoUploading(false);
|
||||||
}}
|
}}
|
||||||
onImageSaveClick={async (file) => {
|
|
||||||
setLogoUploading(true);
|
|
||||||
await uploadQuizImage(quiz.id, file, (quiz, url) => {
|
|
||||||
quiz.config.startpage.logo = url;
|
|
||||||
});
|
|
||||||
|
|
||||||
setLogoUploading(false);
|
|
||||||
}}
|
|
||||||
onDeleteClick={() => {
|
onDeleteClick={() => {
|
||||||
updateQuiz(quiz.id, (quiz) => {
|
updateQuiz(quiz.id, (quiz) => {
|
||||||
quiz.config.startpage.logo = null;
|
quiz.config.startpage.logo = null;
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import { FC } from "react";
|
import { FC, useEffect, useRef, useState } from "react";
|
||||||
|
import Skeleton from "@mui/material/Skeleton";
|
||||||
import DeleteIcon from "@mui/icons-material/Delete";
|
import DeleteIcon from "@mui/icons-material/Delete";
|
||||||
import { IconButton, SxProps, Theme } from "@mui/material";
|
import { IconButton, SxProps, Theme } from "@mui/material";
|
||||||
import { QuizVideo } from "@frontend/squzanswerer";
|
import { QuizVideo } from "@frontend/squzanswerer";
|
||||||
@ -10,6 +11,8 @@ type VideoElementProps = {
|
|||||||
theme: Theme;
|
theme: Theme;
|
||||||
onDeleteClick: () => void;
|
onDeleteClick: () => void;
|
||||||
deleteIconSx?: SxProps<Theme>;
|
deleteIconSx?: SxProps<Theme>;
|
||||||
|
onLoaded?: () => void;
|
||||||
|
showSkeleton?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const VideoElement: FC<VideoElementProps> = ({
|
export const VideoElement: FC<VideoElementProps> = ({
|
||||||
@ -18,10 +21,62 @@ export const VideoElement: FC<VideoElementProps> = ({
|
|||||||
theme,
|
theme,
|
||||||
onDeleteClick,
|
onDeleteClick,
|
||||||
deleteIconSx,
|
deleteIconSx,
|
||||||
|
onLoaded,
|
||||||
|
showSkeleton = true,
|
||||||
}) => {
|
}) => {
|
||||||
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const [isLoaded, setIsLoaded] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log("[VideoElement] init for", { videoSrc });
|
||||||
|
setIsLoaded(false);
|
||||||
|
|
||||||
|
const container = containerRef.current;
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const attach = (videoEl: HTMLVideoElement | null) => {
|
||||||
|
if (!videoEl) return;
|
||||||
|
console.log("[VideoElement] attach video element", videoEl);
|
||||||
|
const markLoaded = (e?: Event) => {
|
||||||
|
console.log("[VideoElement] markLoaded via", e?.type, { readyState: (videoEl as any).readyState });
|
||||||
|
setIsLoaded(true);
|
||||||
|
try { onLoaded && onLoaded(); } catch (err) { console.warn('[VideoElement] onLoaded error', err); }
|
||||||
|
};
|
||||||
|
if (typeof (videoEl as any).readyState === "number" && (videoEl as any).readyState >= 2) {
|
||||||
|
console.log("[VideoElement] already loaded", { readyState: (videoEl as any).readyState });
|
||||||
|
setIsLoaded(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
videoEl.addEventListener("loadeddata", markLoaded, { once: true });
|
||||||
|
videoEl.addEventListener("canplay", markLoaded, { once: true });
|
||||||
|
videoEl.addEventListener("loadedmetadata", markLoaded, { once: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
const tryAttach = () => {
|
||||||
|
const videoEl = container.querySelector("video");
|
||||||
|
attach(videoEl as HTMLVideoElement | null);
|
||||||
|
};
|
||||||
|
|
||||||
|
tryAttach();
|
||||||
|
|
||||||
|
const observer = new MutationObserver((mutations) => {
|
||||||
|
console.log("[VideoElement] mutations", mutations.map(m => m.type));
|
||||||
|
tryAttach();
|
||||||
|
});
|
||||||
|
observer.observe(container, { childList: true, subtree: true, attributes: true });
|
||||||
|
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, [videoSrc]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ position: "relative", width: `${width}px` }}>
|
<Box ref={containerRef} sx={{ position: "relative", width: `${width}px`, minHeight: "168px" }}>
|
||||||
<QuizVideo videoUrl={videoSrc} />
|
<QuizVideo videoUrl={videoSrc} />
|
||||||
|
{!isLoaded && showSkeleton && (
|
||||||
|
<Skeleton
|
||||||
|
sx={{ position: "absolute", inset: 0, width: "100%", height: "100%", borderRadius: "8px", zIndex: 1000, backgroundColor: 'rgba(255,255,255,0.8)' }}
|
||||||
|
variant="rounded"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={onDeleteClick}
|
onClick={onDeleteClick}
|
||||||
sx={{
|
sx={{
|
||||||
|
|||||||
@ -327,7 +327,8 @@ export const updateQuestion = async <T = AnyTypedQuizQuestion>(
|
|||||||
requestQueue.enqueue(`updateQuestion-${questionId}`, request);
|
requestQueue.enqueue(`updateQuestion-${questionId}`, request);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const addQuestionVariant = (questionId: string) => {
|
export const addQuestionVariant = (questionId: string): string => {
|
||||||
|
const newVariant = createQuestionVariant();
|
||||||
updateQuestion(questionId, (question) => {
|
updateQuestion(questionId, (question) => {
|
||||||
switch (question.type) {
|
switch (question.type) {
|
||||||
case "variant":
|
case "variant":
|
||||||
@ -335,12 +336,13 @@ export const addQuestionVariant = (questionId: string) => {
|
|||||||
case "select":
|
case "select":
|
||||||
case "images":
|
case "images":
|
||||||
case "varimg":
|
case "varimg":
|
||||||
question.content.variants.push(createQuestionVariant());
|
question.content.variants.push(newVariant);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error(`Cannot add variant to question of type "${question.type}"`);
|
throw new Error(`Cannot add variant to question of type "${question.type}"`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
return newVariant.id;
|
||||||
};
|
};
|
||||||
export const addQuestionOwnVariant = (questionId: string) => {
|
export const addQuestionOwnVariant = (questionId: string) => {
|
||||||
updateQuestion(questionId, (question) => {
|
updateQuestion(questionId, (question) => {
|
||||||
|
|||||||
@ -8,13 +8,20 @@ import { useQuestionsStore } from "./store";
|
|||||||
import { useCurrentQuiz } from "@root/quizes/hooks";
|
import { useCurrentQuiz } from "@root/quizes/hooks";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
export function useQuestions() {
|
export function useQuestions({ quizId }: { quizId?: number } = {}) {
|
||||||
const quiz = useCurrentQuiz();
|
const currentQuiz = useCurrentQuiz();
|
||||||
const { isLoading, error, isValidating } = useSWR(
|
const currentQuizId = quizId ?? currentQuiz?.backendId;
|
||||||
["questions", quiz?.backendId],
|
|
||||||
|
const { data, isLoading, error, isValidating } = useSWR(
|
||||||
|
currentQuizId ? ["questions", currentQuizId] : null,
|
||||||
([, id]) => questionApi.getList({ quiz_id: id }),
|
([, id]) => questionApi.getList({ quiz_id: id }),
|
||||||
{
|
{
|
||||||
onSuccess: ([questions]) => setQuestions(questions),
|
onSuccess: (data) => {
|
||||||
|
// Добавляем проверку на существование данных
|
||||||
|
if (data && Array.isArray(data[0])) {
|
||||||
|
setQuestions(data[0]);
|
||||||
|
}
|
||||||
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
const message = isAxiosError<string>(error)
|
const message = isAxiosError<string>(error)
|
||||||
? error.response?.data ?? ""
|
? error.response?.data ?? ""
|
||||||
@ -25,7 +32,13 @@ export function useQuestions() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const questions = useQuestionsStore((state) => state.questions);
|
const questions = useQuestionsStore((state) => state.questions);
|
||||||
|
|
||||||
return { questions, isLoading, error, isValidating };
|
return {
|
||||||
|
questions: questions || [], // Гарантируем возврат массива
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
isValidating
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -67,16 +67,12 @@ export const setUser = (user: User) =>
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const clearUserData = () => {
|
export const clearUserData = () => {
|
||||||
console.log("clearUserData: Clearing user data");
|
|
||||||
console.log("clearUserData: Before clearing -", useUserStore.getState());
|
|
||||||
|
|
||||||
useUserStore.setState({ ...initialState });
|
useUserStore.setState({ ...initialState });
|
||||||
|
|
||||||
console.log("clearUserData: After clearing -", useUserStore.getState());
|
|
||||||
|
|
||||||
// Также очищаем localStorage напрямую
|
// Также очищаем localStorage напрямую
|
||||||
localStorage.removeItem("user");
|
localStorage.removeItem("user");
|
||||||
console.log("clearUserData: localStorage cleared");
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setUserAccount = (userAccount: OriginalUserAccount) =>
|
export const setUserAccount = (userAccount: OriginalUserAccount) =>
|
||||||
|
|||||||
@ -17,12 +17,12 @@ interface CustomTextFieldProps {
|
|||||||
value?: string;
|
value?: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
emptyError?: boolean;
|
emptyError?: boolean;
|
||||||
onChange?: (event: ChangeEvent<HTMLInputElement>) => void;
|
onChange?: (event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
|
||||||
onKeyDown?: (event: KeyboardEvent<HTMLInputElement>) => void;
|
onKeyDown?: (event: KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
|
||||||
onBlur?: (event: FocusEvent<HTMLInputElement>) => void;
|
onBlur?: (event: FocusEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
|
||||||
onFocus?: (event: FocusEvent<HTMLInputElement>) => void;
|
onFocus?: (event: FocusEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
|
||||||
onClick?: (event: MouseEvent<HTMLInputElement>) => void;
|
onClick?: (event: MouseEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
|
||||||
onPaste?: (event: ClipboardEvent<HTMLInputElement>) => void;
|
onPaste?: (event: ClipboardEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
|
||||||
text?: string;
|
text?: string;
|
||||||
maxLength?: number;
|
maxLength?: number;
|
||||||
sx?: SxProps<Theme>;
|
sx?: SxProps<Theme>;
|
||||||
@ -32,7 +32,7 @@ interface CustomTextFieldProps {
|
|||||||
rows?: number;
|
rows?: number;
|
||||||
className?: string;
|
className?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
inputRef?: Ref<HTMLInputElement>;
|
inputRef?: Ref<HTMLInputElement | HTMLTextAreaElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CustomTextField({
|
export default function CustomTextField({
|
||||||
@ -67,7 +67,9 @@ export default function CustomTextField({
|
|||||||
setInputValue(value);
|
setInputValue(value);
|
||||||
}, [value]);
|
}, [value]);
|
||||||
|
|
||||||
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleInputChange = (
|
||||||
|
event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
||||||
|
) => {
|
||||||
if (event.target.value.length <= maxLength) {
|
if (event.target.value.length <= maxLength) {
|
||||||
const inputValue = event.target.value;
|
const inputValue = event.target.value;
|
||||||
|
|
||||||
@ -85,12 +87,16 @@ export default function CustomTextField({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleInputFocus = (event: React.FocusEvent<HTMLInputElement>) => {
|
const handleInputFocus = (
|
||||||
|
event: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>
|
||||||
|
) => {
|
||||||
setIsInputActive(true);
|
setIsInputActive(true);
|
||||||
if (onFocus) onFocus(event);
|
if (onFocus) onFocus(event);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleInputBlur = (event: React.FocusEvent<HTMLInputElement>) => {
|
const handleInputBlur = (
|
||||||
|
event: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>
|
||||||
|
) => {
|
||||||
setIsInputActive(false);
|
setIsInputActive(false);
|
||||||
|
|
||||||
if (onBlur) {
|
if (onBlur) {
|
||||||
@ -98,6 +104,12 @@ export default function CustomTextField({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mergedInputElementProps = {
|
||||||
|
...(InputProps?.inputProps as any),
|
||||||
|
onClick,
|
||||||
|
onPaste,
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormControl
|
<FormControl
|
||||||
fullWidth
|
fullWidth
|
||||||
@ -126,14 +138,13 @@ export default function CustomTextField({
|
|||||||
onFocus={handleInputFocus}
|
onFocus={handleInputFocus}
|
||||||
onBlur={handleInputBlur}
|
onBlur={handleInputBlur}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
onClick={onClick}
|
multiline
|
||||||
onPaste={onPaste}
|
rows={rows > 0 ? rows : 1}
|
||||||
multiline={rows > 0}
|
|
||||||
rows={rows}
|
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
disableUnderline
|
disableUnderline
|
||||||
inputRef={inputRef}
|
inputRef={inputRef}
|
||||||
{...InputProps}
|
{...InputProps}
|
||||||
|
inputProps={mergedInputElementProps}
|
||||||
sx={{
|
sx={{
|
||||||
maxLength: maxLength,
|
maxLength: maxLength,
|
||||||
borderRadius: "10px",
|
borderRadius: "10px",
|
||||||
@ -143,6 +154,8 @@ export default function CustomTextField({
|
|||||||
border: `${isInputActive ? "black 2px" : "#9A9AAF 1px"} solid`,
|
border: `${isInputActive ? "black 2px" : "#9A9AAF 1px"} solid`,
|
||||||
backgroundColor: theme.palette.background.default,
|
backgroundColor: theme.palette.background.default,
|
||||||
height: "48px",
|
height: "48px",
|
||||||
|
// Prevent resize handle to keep visuals unchanged
|
||||||
|
'& textarea': { resize: 'none' },
|
||||||
...sx,
|
...sx,
|
||||||
}}
|
}}
|
||||||
data-cy="textfield"
|
data-cy="textfield"
|
||||||
|
|||||||
@ -68,14 +68,17 @@ export const MediaSelectionAndDisplay: FC<Iprops> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleVideoUpload(videoUrl: string) {
|
async function handleVideoUpload(videoUrl: string) {
|
||||||
|
console.log("[QuestionMedia] Video upload start", { videoUrl });
|
||||||
setBackgroundUploading(true);
|
setBackgroundUploading(true);
|
||||||
|
|
||||||
if (videoUrl.startsWith("blob:")) {
|
if (videoUrl.startsWith("blob:")) {
|
||||||
const videoBlob = await (await fetch(videoUrl)).blob();
|
const videoBlob = await (await fetch(videoUrl)).blob();
|
||||||
|
console.log("[QuestionMedia] Uploading blob to backend", { size: videoBlob.size, type: videoBlob.type });
|
||||||
uploadQuestionImage(question.id, quizQid, videoBlob, (question, url) => {
|
uploadQuestionImage(question.id, quizQid, videoBlob, (question, url) => {
|
||||||
if (!("video" in question.content)) return;
|
if (!("video" in question.content)) return;
|
||||||
|
|
||||||
question.content.video = url;
|
question.content.video = url;
|
||||||
|
console.log("[QuestionMedia] Backend returned video URL", { url });
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
updateQuestion(question.id, (question) => {
|
updateQuestion(question.id, (question) => {
|
||||||
@ -83,9 +86,10 @@ export const MediaSelectionAndDisplay: FC<Iprops> = ({
|
|||||||
|
|
||||||
question.content.video = videoUrl;
|
question.content.video = videoUrl;
|
||||||
});
|
});
|
||||||
|
console.log("[QuestionMedia] Set external video URL", { videoUrl });
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => {setBackgroundUploading(false)},7000);
|
setTimeout(() => { setBackgroundUploading(false); console.log("[QuestionMedia] Video upload end"); }, 7000);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -6,26 +6,15 @@ export default function PrivateRoute() {
|
|||||||
const user = useUserStore((state) => state.user);
|
const user = useUserStore((state) => state.user);
|
||||||
const userId = useUserStore((state) => state.userId);
|
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(() => {
|
useEffect(() => {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
console.log("PrivateRoute: User is null, redirecting to / via useEffect");
|
|
||||||
window.location.href = "/";
|
window.location.href = "/";
|
||||||
}
|
}
|
||||||
}, [user]);
|
}, [user]);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
console.log("PrivateRoute: User is null, showing fallback");
|
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("PrivateRoute: User exists, rendering Outlet");
|
|
||||||
return <Outlet />;
|
return <Outlet />;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,8 +18,6 @@ export const generateHubWalletRequestURL = ({
|
|||||||
let currentDomain = window.location.host;
|
let currentDomain = window.location.host;
|
||||||
if (currentDomain === "localhost") currentDomain += ":3000";
|
if (currentDomain === "localhost") currentDomain += ":3000";
|
||||||
|
|
||||||
console.log("Я здесь для отладки и спешу сообщить, что деплой был успешно завершен!")
|
|
||||||
|
|
||||||
// Используем более надежный способ генерации URL
|
// Используем более надежный способ генерации URL
|
||||||
const baseUrl = `http://${isTestServer ? "s" : ""}hub.pena.digital/anyservicepayment`;
|
const baseUrl = `http://${isTestServer ? "s" : ""}hub.pena.digital/anyservicepayment`;
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
|
|||||||
@ -25,10 +25,8 @@ export function handleComponentError(error: Error, info: ErrorInfo, getTickets:
|
|||||||
if (!getAuthToken()) return;
|
if (!getAuthToken()) return;
|
||||||
// Проверяем разрешение на отправку ошибок (по домену)
|
// Проверяем разрешение на отправку ошибок (по домену)
|
||||||
if (!isErrorReportingAllowed(error)) {
|
if (!isErrorReportingAllowed(error)) {
|
||||||
console.log('❌ Отправка ошибки заблокирована:', error.message);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log(`✅ Обработка ошибки: ${error.message}`);
|
|
||||||
// Копируем __forceSend если есть
|
// Копируем __forceSend если есть
|
||||||
const componentError: ComponentError & { __forceSend?: boolean } = {
|
const componentError: ComponentError & { __forceSend?: boolean } = {
|
||||||
timestamp: Math.floor(Date.now() / 1000),
|
timestamp: Math.floor(Date.now() / 1000),
|
||||||
@ -56,7 +54,6 @@ export async function sendErrorsToServer(getTickets: () => Ticket[]) {
|
|||||||
// Если хотя бы одна ошибка в очереди с __forceSend, отправляем всё
|
// Если хотя бы одна ошибка в очереди с __forceSend, отправляем всё
|
||||||
const forceSend = errorsQueue.some(e => (e as any).__forceSend);
|
const forceSend = errorsQueue.some(e => (e as any).__forceSend);
|
||||||
if (!forceSend && !isErrorReportingAllowed()) {
|
if (!forceSend && !isErrorReportingAllowed()) {
|
||||||
console.log('❌ Отправка ошибок заблокирована, очищаем очередь');
|
|
||||||
errorsQueue = [];
|
errorsQueue = [];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -93,10 +90,8 @@ export async function sendErrorsToServer(getTickets: () => Ticket[]) {
|
|||||||
// Ищет существующий тикет с system: true
|
// Ищет существующий тикет с system: true
|
||||||
export async function findSystemTicket(tickets: Ticket[]) {
|
export async function findSystemTicket(tickets: Ticket[]) {
|
||||||
for (const ticket of tickets) {
|
for (const ticket of tickets) {
|
||||||
console.log("[findSystemTicket] Проверяем тикет:", ticket);
|
|
||||||
if (!('messages' in ticket)) {
|
if (!('messages' in ticket)) {
|
||||||
if (ticket.top_message && ticket.top_message.system === true) {
|
if (ticket.top_message && ticket.top_message.system === true) {
|
||||||
console.log("[findSystemTicket] Найден тикет по top_message.system:true:", ticket.id);
|
|
||||||
return ticket.id;
|
return ticket.id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,26 +0,0 @@
|
|||||||
import { QuizQuestionsWithVariants } from "@frontend/squzanswerer";
|
|
||||||
import { addQuestionOwnVariant, addQuestionVariant, updateQuestion } from "@root/questions/actions";
|
|
||||||
|
|
||||||
export const useAddAnswer = () => {
|
|
||||||
const onClickAddAnAnswer = (question: QuizQuestionsWithVariants) => {
|
|
||||||
addQuestionVariant(question.id);
|
|
||||||
};
|
|
||||||
interface SwitchOwnProps {
|
|
||||||
question: QuizQuestionsWithVariants;
|
|
||||||
checked: boolean
|
|
||||||
}
|
|
||||||
const switchOwn = ({ question, checked }: SwitchOwnProps) => {
|
|
||||||
if (!question.content.variants.some(v => v.isOwn) && checked) {
|
|
||||||
addQuestionOwnVariant(question.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
updateQuestion<QuizQuestionVariant>(question.id, (question) => {
|
|
||||||
question.content.own = checked;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
onClickAddAnAnswer,
|
|
||||||
switchOwn
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -25,20 +25,28 @@ export function useAnalytics({ ready, quizId, to, from }: useAnalyticsProps) {
|
|||||||
const [devices, setDevices] = useState<DevicesResponse | null>(null);
|
const [devices, setDevices] = useState<DevicesResponse | null>(null);
|
||||||
const [general, setGeneral] = useState<GeneralResponse | null>(null);
|
const [general, setGeneral] = useState<GeneralResponse | null>(null);
|
||||||
const [questions, setQuestions] = useState<QuestionsResponse | null>(null);
|
const [questions, setQuestions] = useState<QuestionsResponse | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!quizId || !ready) return;
|
if (!quizId || !ready) {
|
||||||
|
setIsLoading(true);
|
||||||
const requestStatistics = async () => {
|
|
||||||
if (!formatTo || !formatFrom) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const requestStatistics = async () => {
|
||||||
|
if (!formatTo || !formatFrom) {
|
||||||
|
setIsLoading(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
const [gottenGeneral] = await getGeneral(quizId, formatTo, formatFrom);
|
const [gottenGeneral] = await getGeneral(quizId, formatTo, formatFrom);
|
||||||
const [gottenDevices] = await getDevices(quizId, formatTo, formatFrom);
|
const [gottenDevices] = await getDevices(quizId, formatTo, formatFrom);
|
||||||
const [gottenQuestions] = await getQuestions(quizId, formatTo, formatFrom);
|
const [gottenQuestions] = await getQuestions(quizId, formatTo, formatFrom);
|
||||||
|
|
||||||
getGraphics(quizId, formatTo, formatFrom)
|
getGraphics(quizId, formatTo, formatFrom);
|
||||||
|
|
||||||
if (gottenGeneral) {
|
if (gottenGeneral) {
|
||||||
setGeneral(gottenGeneral);
|
setGeneral(gottenGeneral);
|
||||||
@ -51,10 +59,13 @@ export function useAnalytics({ ready, quizId, to, from }: useAnalyticsProps) {
|
|||||||
if (gottenQuestions) {
|
if (gottenQuestions) {
|
||||||
setQuestions(gottenQuestions);
|
setQuestions(gottenQuestions);
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
requestStatistics();
|
requestStatistics();
|
||||||
}, [ready, quizId, to, from]);
|
}, [ready, quizId, to, from]);
|
||||||
|
|
||||||
return { devices, general, questions };
|
return { devices, general, questions, isLoading };
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,21 +35,10 @@ export const useAfterPay = () => {
|
|||||||
// Проверяем, есть ли токен восстановления пароля в URL
|
// Проверяем, есть ли токен восстановления пароля в URL
|
||||||
const hasAuthToken = searchParams.get("auth") || window.location.search.includes("auth=");
|
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
|
// НЕ очищаем параметры на странице восстановления пароля, когда открыты модалки или есть токен auth
|
||||||
if (location.pathname !== "/changepwd" && !location.state?.backgroundLocation && !isRecoverPasswordModal && !hasAuthToken) {
|
if (location.pathname !== "/changepwd" && !location.state?.backgroundLocation && !isRecoverPasswordModal && !hasAuthToken) {
|
||||||
console.log("Очищаем параметры URL");
|
|
||||||
setSearchParams({}, { replace: true });
|
setSearchParams({}, { replace: true });
|
||||||
} else {
|
|
||||||
console.log("НЕ очищаем параметры URL");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userId && URLuserId && userId === URLuserId) {
|
if (userId && URLuserId && userId === URLuserId) {
|
||||||
|
|||||||
13
src/utils/pinkScrollbar.ts
Normal file
13
src/utils/pinkScrollbar.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { Theme } from "@mui/material";
|
||||||
|
|
||||||
|
export default (theme: Theme) => ({
|
||||||
|
scrollbarWidth: "auto",
|
||||||
|
"&::-webkit-scrollbar": {
|
||||||
|
display: "block",
|
||||||
|
width: "8px",
|
||||||
|
},
|
||||||
|
"&::-webkit-scrollbar-thumb": {
|
||||||
|
backgroundColor: theme.palette.brightPurple.main,
|
||||||
|
borderRadius: "4px",
|
||||||
|
}
|
||||||
|
});
|
||||||
47
src/utils/questionVariants.ts
Normal file
47
src/utils/questionVariants.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { QuizQuestionsWithVariants, QuizQuestionVariant } from "@frontend/squzanswerer";
|
||||||
|
import { addQuestionOwnVariant, addQuestionVariant, updateQuestion } from "@root/questions/actions";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Утилита для управления вариантами ответов с автофокусом
|
||||||
|
*/
|
||||||
|
export const useQuestionVariantsWithFocus = () => {
|
||||||
|
const [focusedVariantId, setFocusedVariantId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const addVariantWithFocus = (question: QuizQuestionsWithVariants) => {
|
||||||
|
const newVariantId = addQuestionVariant(question.id);
|
||||||
|
setFocusedVariantId(newVariantId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addVariantOnEnter = (questionId: string) => {
|
||||||
|
const newVariantId = addQuestionVariant(questionId);
|
||||||
|
setFocusedVariantId(newVariantId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearFocusedVariant = () => {
|
||||||
|
setFocusedVariantId(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface SwitchOwnProps {
|
||||||
|
question: QuizQuestionsWithVariants;
|
||||||
|
checked: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const switchOwnVariant = ({ question, checked }: SwitchOwnProps) => {
|
||||||
|
if (!question.content.variants.some(v => v.isOwn) && checked) {
|
||||||
|
addQuestionOwnVariant(question.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateQuestion<QuizQuestionVariant>(question.id, (question) => {
|
||||||
|
question.content.own = checked;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
addVariantWithFocus,
|
||||||
|
addVariantOnEnter,
|
||||||
|
switchOwnVariant,
|
||||||
|
focusedVariantId,
|
||||||
|
clearFocusedVariant
|
||||||
|
};
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue
Block a user