Enhance API utility with user region detection and error handling. Integrate region headers into API requests across components, improving localization and error management. Update package dependencies for better compatibility.
This commit is contained in:
@@ -8,6 +8,8 @@
|
|||||||
"@epicgames-ps/lib-pixelstreamingfrontend-ue5.3": "^1.0.8",
|
"@epicgames-ps/lib-pixelstreamingfrontend-ue5.3": "^1.0.8",
|
||||||
"@uidotdev/usehooks": "^2.4.1",
|
"@uidotdev/usehooks": "^2.4.1",
|
||||||
"ahooks": "^3.7.10",
|
"ahooks": "^3.7.10",
|
||||||
|
"baseline-browser-mapping": "^2.9.14",
|
||||||
|
"caniuse-lite": "^1.0.30001764",
|
||||||
"date-fns": "^2.30.0",
|
"date-fns": "^2.30.0",
|
||||||
"i18next": "^23.8.2",
|
"i18next": "^23.8.2",
|
||||||
"i18next-browser-languagedetector": "^8.2.0",
|
"i18next-browser-languagedetector": "^8.2.0",
|
||||||
@@ -253,6 +255,8 @@
|
|||||||
|
|
||||||
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||||
|
|
||||||
|
"baseline-browser-mapping": ["baseline-browser-mapping@2.9.14", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg=="],
|
||||||
|
|
||||||
"binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
|
"binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
|
||||||
|
|
||||||
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||||
@@ -265,7 +269,7 @@
|
|||||||
|
|
||||||
"camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="],
|
"camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="],
|
||||||
|
|
||||||
"caniuse-lite": ["caniuse-lite@1.0.30001727", "", {}, "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q=="],
|
"caniuse-lite": ["caniuse-lite@1.0.30001764", "", {}, "sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g=="],
|
||||||
|
|
||||||
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||||
|
|
||||||
@@ -781,6 +785,10 @@
|
|||||||
|
|
||||||
"@typescript-eslint/utils/eslint-scope": ["eslint-scope@5.1.1", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw=="],
|
"@typescript-eslint/utils/eslint-scope": ["eslint-scope@5.1.1", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw=="],
|
||||||
|
|
||||||
|
"autoprefixer/caniuse-lite": ["caniuse-lite@1.0.30001727", "", {}, "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q=="],
|
||||||
|
|
||||||
|
"browserslist/caniuse-lite": ["caniuse-lite@1.0.30001727", "", {}, "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q=="],
|
||||||
|
|
||||||
"chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
"chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||||
|
|
||||||
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||||
|
|||||||
@@ -13,6 +13,8 @@
|
|||||||
"@epicgames-ps/lib-pixelstreamingfrontend-ue5.3": "^1.0.8",
|
"@epicgames-ps/lib-pixelstreamingfrontend-ue5.3": "^1.0.8",
|
||||||
"@uidotdev/usehooks": "^2.4.1",
|
"@uidotdev/usehooks": "^2.4.1",
|
||||||
"ahooks": "^3.7.10",
|
"ahooks": "^3.7.10",
|
||||||
|
"baseline-browser-mapping": "^2.9.14",
|
||||||
|
"caniuse-lite": "^1.0.30001764",
|
||||||
"date-fns": "^2.30.0",
|
"date-fns": "^2.30.0",
|
||||||
"i18next": "^23.8.2",
|
"i18next": "^23.8.2",
|
||||||
"i18next-browser-languagedetector": "^8.2.0",
|
"i18next-browser-languagedetector": "^8.2.0",
|
||||||
|
|||||||
+30
-31
@@ -21,6 +21,8 @@ import { useNavigate, useSearchParams } from "react-router-dom";
|
|||||||
import { Bounce, ToastContainer, toast } from "react-toastify";
|
import { Bounce, ToastContainer, toast } from "react-toastify";
|
||||||
import "react-toastify/dist/ReactToastify.css";
|
import "react-toastify/dist/ReactToastify.css";
|
||||||
import InfoIcon from "./components/icons/InfoIcon";
|
import InfoIcon from "./components/icons/InfoIcon";
|
||||||
|
import { detectUserRegion, getRegionHeaders } from "./utils/api";
|
||||||
|
import { handleApiError, isErrorResponse } from "./utils/errorHandler";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -36,6 +38,7 @@ function App() {
|
|||||||
const type = searchParams.get("type") || "demo";
|
const type = searchParams.get("type") || "demo";
|
||||||
const endAt = searchParams.get("endAt");
|
const endAt = searchParams.get("endAt");
|
||||||
const [streamUrl, setStreamUrl] = useState<string>();
|
const [streamUrl, setStreamUrl] = useState<string>();
|
||||||
|
const [regionDetected, setRegionDetected] = useState<boolean>(false);
|
||||||
|
|
||||||
function toastError(text: string) {
|
function toastError(text: string) {
|
||||||
toast.error(text, {
|
toast.error(text, {
|
||||||
@@ -54,59 +57,39 @@ function App() {
|
|||||||
|
|
||||||
|
|
||||||
async function startStream(build: string) {
|
async function startStream(build: string) {
|
||||||
// const { countryCode, error }: { countryCode: string; error: string } =
|
|
||||||
// await api.get("getCountryCode").json();
|
|
||||||
|
|
||||||
// if (!countryCode) {
|
|
||||||
// toastError("Неизвестная ошибка при получении кода страны");
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (error) {
|
|
||||||
// toastError(error);
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
let location = "a1";
|
let location = "a1";
|
||||||
|
|
||||||
if (searchParams.has("location")) {
|
if (searchParams.has("location")) {
|
||||||
location = searchParams.get("location") as string;
|
location = searchParams.get("location") as string;
|
||||||
}
|
}
|
||||||
// else if (countryCode !== "RU") {
|
|
||||||
// location = "a2";
|
|
||||||
// }
|
|
||||||
|
|
||||||
// console.log("location", location);
|
|
||||||
|
|
||||||
// setLoading(true);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response: any = await ky
|
const response: any = await ky
|
||||||
.get(
|
.get(
|
||||||
`${
|
`${
|
||||||
import.meta.env.VITE_COORD_URL
|
import.meta.env.VITE_COORD_URL
|
||||||
}/start?location=${location}&build=${build}&type=${type}&endAt=${endAt}`
|
}/start?location=${location}&build=${build}&type=${type}&endAt=${endAt}`,
|
||||||
|
{ headers: getRegionHeaders() }
|
||||||
)
|
)
|
||||||
.json();
|
.json();
|
||||||
|
|
||||||
|
// Проверяем, является ли ответ ошибкой
|
||||||
|
if (isErrorResponse(response)) {
|
||||||
|
handleApiError(response, t, navigate);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (response.stream) {
|
if (response.stream) {
|
||||||
setStreamUrl(`/stream/${response.stream}`);
|
setStreamUrl(`/stream/${response.stream}`);
|
||||||
|
|
||||||
// setInterval(() => {
|
|
||||||
// setCountdownTimer((prev) => prev - 1);
|
|
||||||
// }, 1000);
|
|
||||||
} else if (response.error) {
|
} else if (response.error) {
|
||||||
toastError(response.error);
|
toastError(response.error);
|
||||||
// setLoading(false);
|
|
||||||
} else {
|
} else {
|
||||||
toastError(t("errors.unknownError"));
|
toastError(t("errors.unknownError"));
|
||||||
// setLoading(false);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
toastError(`Неизвестная ошибка 2: ${error.message}`);
|
toastError(t("errors.networkError") + `: ${error.message}`);
|
||||||
}
|
}
|
||||||
// setLoading(false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,11 +104,27 @@ function App() {
|
|||||||
navigate(streamUrl);
|
navigate(streamUrl);
|
||||||
}, [streamUrl]);
|
}, [streamUrl]);
|
||||||
|
|
||||||
|
// Определяем регион пользователя при первой загрузке
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (build) {
|
async function initializeRegion() {
|
||||||
|
try {
|
||||||
|
await detectUserRegion();
|
||||||
|
setRegionDetected(true);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to detect user region:", error);
|
||||||
|
// Даже при ошибке продолжаем работу с дефолтным регионом
|
||||||
|
setRegionDetected(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void initializeRegion();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (build && regionDetected) {
|
||||||
void startStream(build);
|
void startStream(build);
|
||||||
}
|
}
|
||||||
}, []);
|
}, [regionDetected]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = t("title");
|
document.title = t("title");
|
||||||
|
|||||||
+15
-2
@@ -18,11 +18,16 @@ import {
|
|||||||
} from "date-fns";
|
} from "date-fns";
|
||||||
import ru from "date-fns/locale/ru";
|
import ru from "date-fns/locale/ru";
|
||||||
import ky from "ky";
|
import ky from "ky";
|
||||||
import { useParams } from "react-router-dom";
|
import { getRegionHeaders } from "./utils/api";
|
||||||
|
import { handleApiError, isErrorResponse } from "./utils/errorHandler";
|
||||||
|
import { useParams, useNavigate } from "react-router-dom";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import InputMask from "react-input-mask";
|
import InputMask from "react-input-mask";
|
||||||
|
|
||||||
function CalendarPage() {
|
function CalendarPage() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const navigate = useNavigate();
|
||||||
const [step, setStep] = useState<number>(1);
|
const [step, setStep] = useState<number>(1);
|
||||||
const [date, setDate] = useState<Date>(new Date());
|
const [date, setDate] = useState<Date>(new Date());
|
||||||
const [name, setName] = useState<string>("");
|
const [name, setName] = useState<string>("");
|
||||||
@@ -42,7 +47,8 @@ function CalendarPage() {
|
|||||||
async function getScheduledSessions(buildId: string, date: Date) {
|
async function getScheduledSessions(buildId: string, date: Date) {
|
||||||
const result: any[] = await ky
|
const result: any[] = await ky
|
||||||
.get(
|
.get(
|
||||||
`https://crm.stream.graff.tech/api/scheduledSessions/${buildId}?date=${date.toISOString()}`
|
`https://crm.stream.graff.tech/api/scheduledSessions/${buildId}?date=${date.toISOString()}`,
|
||||||
|
{ headers: getRegionHeaders() }
|
||||||
)
|
)
|
||||||
.json();
|
.json();
|
||||||
|
|
||||||
@@ -78,9 +84,16 @@ function CalendarPage() {
|
|||||||
title,
|
title,
|
||||||
startAt,
|
startAt,
|
||||||
},
|
},
|
||||||
|
headers: getRegionHeaders(),
|
||||||
})
|
})
|
||||||
.json();
|
.json();
|
||||||
|
|
||||||
|
// Проверяем, является ли ответ ошибкой
|
||||||
|
if (isErrorResponse(result)) {
|
||||||
|
handleApiError(result, t, navigate);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!result.userInviteLink) {
|
if (!result.userInviteLink) {
|
||||||
alert(result.error);
|
alert(result.error);
|
||||||
return;
|
return;
|
||||||
|
|||||||
+22
-4
@@ -1,17 +1,35 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import ky from "ky";
|
import ky from "ky";
|
||||||
|
import { getRegionHeaders } from "./utils/api";
|
||||||
|
import { handleApiError, isErrorResponse } from "./utils/errorHandler";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { parseUserAgent } from "react-device-detect";
|
import { parseUserAgent } from "react-device-detect";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
function HistoryPage() {
|
function HistoryPage() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const navigate = useNavigate();
|
||||||
const [history, setHistory] = useState([]);
|
const [history, setHistory] = useState([]);
|
||||||
|
|
||||||
async function getHistory() {
|
async function getHistory() {
|
||||||
const result: any = await ky
|
try {
|
||||||
.get(`${import.meta.env.VITE_COORD_URL}/session_history`)
|
const result: any = await ky
|
||||||
.json();
|
.get(`${import.meta.env.VITE_COORD_URL}/session_history`, {
|
||||||
|
headers: getRegionHeaders(),
|
||||||
|
})
|
||||||
|
.json();
|
||||||
|
|
||||||
setHistory(result);
|
// Проверяем, является ли ответ ошибкой
|
||||||
|
if (isErrorResponse(result)) {
|
||||||
|
handleApiError(result, t, navigate);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setHistory(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch history:", error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import ky from "ky";
|
import ky from "ky";
|
||||||
|
import { getRegionHeaders } from "./utils/api";
|
||||||
|
import { handleApiError, isErrorResponse } from "./utils/errorHandler";
|
||||||
import useAuthStore from "./stores/useAuthStore";
|
import useAuthStore from "./stores/useAuthStore";
|
||||||
import { FormEvent, useRef, useState } from "react";
|
import { FormEvent, useRef, useState } from "react";
|
||||||
import { Trans, useTranslation } from "react-i18next";
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
type User = {
|
type User = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -17,6 +20,7 @@ interface IResult {
|
|||||||
|
|
||||||
function PersonalAreaLoginPage() {
|
function PersonalAreaLoginPage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const navigate = useNavigate();
|
||||||
const [setAccessToken, setUser] = useAuthStore((state) => [
|
const [setAccessToken, setUser] = useAuthStore((state) => [
|
||||||
state.setAccessToken,
|
state.setAccessToken,
|
||||||
state.setUser,
|
state.setUser,
|
||||||
@@ -38,14 +42,26 @@ function PersonalAreaLoginPage() {
|
|||||||
const result: IResult = await ky
|
const result: IResult = await ky
|
||||||
.post(import.meta.env.VITE_COORD_URL + "/login", {
|
.post(import.meta.env.VITE_COORD_URL + "/login", {
|
||||||
json: { username, password },
|
json: { username, password },
|
||||||
|
headers: getRegionHeaders(),
|
||||||
})
|
})
|
||||||
.json();
|
.json();
|
||||||
|
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
|
||||||
|
// Проверяем, является ли ответ ошибкой с errorCode
|
||||||
|
if (isErrorResponse(result)) {
|
||||||
|
handleApiError(result, t, navigate, {
|
||||||
|
callback: (_, errorMessage) => {
|
||||||
|
passwordRef.current?.focus();
|
||||||
|
setPassword("");
|
||||||
|
setError(errorMessage);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
passwordRef.current?.focus();
|
passwordRef.current?.focus();
|
||||||
|
|
||||||
setPassword("");
|
setPassword("");
|
||||||
setError(t("errors.invalidCredentials"));
|
setError(t("errors.invalidCredentials"));
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import useModalStore from "../../stores/useModalStore";
|
import useModalStore from "../../stores/useModalStore";
|
||||||
import ky from "ky";
|
import ky from "ky";
|
||||||
|
import { getRegionHeaders } from "../../utils/api";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
|
|
||||||
function AFKTimerModal() {
|
function AFKTimerModal() {
|
||||||
@@ -13,7 +14,8 @@ function AFKTimerModal() {
|
|||||||
|
|
||||||
async function endSession() {
|
async function endSession() {
|
||||||
await ky.post(
|
await ky.post(
|
||||||
`${import.meta.env.VITE_COORD_URL}/active_sessions/${params.id}/end`
|
`${import.meta.env.VITE_COORD_URL}/active_sessions/${params.id}/end`,
|
||||||
|
{ headers: getRegionHeaders() }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import api from "../utils/api";
|
import { detectUserRegion } from "../utils/api";
|
||||||
|
|
||||||
export function useLanguageDetection() {
|
export function useLanguageDetection() {
|
||||||
const { i18n } = useTranslation();
|
const { i18n } = useTranslation();
|
||||||
@@ -8,12 +8,11 @@ export function useLanguageDetection() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function detectLanguage() {
|
async function detectLanguage() {
|
||||||
try {
|
try {
|
||||||
const { countryCode, error }: { countryCode: string; error: string } =
|
const countryCode = await detectUserRegion();
|
||||||
await api.get("getCountryCode").json();
|
|
||||||
|
|
||||||
if (!error && countryCode && countryCode !== "RU") {
|
if (countryCode && countryCode !== "RU") {
|
||||||
await i18n.changeLanguage("en");
|
await i18n.changeLanguage("en");
|
||||||
} else if (!error && countryCode === "RU") {
|
} else if (countryCode === "RU") {
|
||||||
await i18n.changeLanguage("ru");
|
await i18n.changeLanguage("ru");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
+22
@@ -170,6 +170,17 @@ const resources = {
|
|||||||
invalidCredentials: "Неверное имя пользователя или пароль",
|
invalidCredentials: "Неверное имя пользователя или пароль",
|
||||||
failedToFetchData: "Не удалось получить данные",
|
failedToFetchData: "Не удалось получить данные",
|
||||||
noConnection: "Нет соединения с сервером, попробуйте позже",
|
noConnection: "Нет соединения с сервером, попробуйте позже",
|
||||||
|
networkError: "Ошибка сети",
|
||||||
|
// Error codes from server
|
||||||
|
INTERNAL_ERROR: "Внутренняя ошибка сервера",
|
||||||
|
INVALID_OBJECT_ID: "Некорректный идентификатор объекта",
|
||||||
|
SESSION_NOT_FOUND: "Сессия не найдена. Возвращаемся на главную страницу...",
|
||||||
|
SESSION_FETCH_ERROR: "Ошибка при получении данных сессии",
|
||||||
|
IP_ADDRESS_ERROR: "Не удалось определить IP-адрес",
|
||||||
|
COUNTRY_CODE_FETCH_ERROR: "Ошибка при определении страны",
|
||||||
|
EMAIL_REQUIRED: "Необходимо указать email",
|
||||||
|
LINK_REQUIRED: "Необходимо указать ссылку",
|
||||||
|
EMAIL_SEND_ERROR: "Не удалось отправить письмо",
|
||||||
},
|
},
|
||||||
userActions: {
|
userActions: {
|
||||||
transferControl: "Передать управление",
|
transferControl: "Передать управление",
|
||||||
@@ -387,6 +398,17 @@ const resources = {
|
|||||||
invalidCredentials: "Invalid username or password",
|
invalidCredentials: "Invalid username or password",
|
||||||
failedToFetchData: "Failed to fetch data",
|
failedToFetchData: "Failed to fetch data",
|
||||||
noConnection: "No connection to server, please try again later",
|
noConnection: "No connection to server, please try again later",
|
||||||
|
networkError: "Network error",
|
||||||
|
// Error codes from server
|
||||||
|
INTERNAL_ERROR: "Internal server error",
|
||||||
|
INVALID_OBJECT_ID: "Invalid object ID",
|
||||||
|
SESSION_NOT_FOUND: "Session not found. Returning to home page...",
|
||||||
|
SESSION_FETCH_ERROR: "Error fetching session data",
|
||||||
|
IP_ADDRESS_ERROR: "Unable to determine IP address",
|
||||||
|
COUNTRY_CODE_FETCH_ERROR: "Error determining country",
|
||||||
|
EMAIL_REQUIRED: "Email is required",
|
||||||
|
LINK_REQUIRED: "Link is required",
|
||||||
|
EMAIL_SEND_ERROR: "Failed to send email",
|
||||||
},
|
},
|
||||||
userActions: {
|
userActions: {
|
||||||
transferControl: "Transfer control",
|
transferControl: "Transfer control",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { PixelStreamingWrapper2 } from "../components/PixelStreamingWrapper2";
|
import { PixelStreamingWrapper2 } from "../components/PixelStreamingWrapper2";
|
||||||
import api from "../utils/api";
|
import api from "../utils/api";
|
||||||
|
import { isErrorResponse } from "../utils/errorHandler";
|
||||||
import { useParams, useSearchParams } from "react-router-dom";
|
import { useParams, useSearchParams } from "react-router-dom";
|
||||||
import useStateRef from "react-usestateref";
|
import useStateRef from "react-usestateref";
|
||||||
import Peer from "peerjs";
|
import Peer from "peerjs";
|
||||||
@@ -322,15 +323,27 @@ function StreamPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function getActiveSession() {
|
async function getActiveSession() {
|
||||||
const activeSession: any = await api
|
try {
|
||||||
.get(`activeSessions/${params.id}`)
|
const activeSession: any = await api
|
||||||
.json();
|
.get(`activeSessions/${params.id}`)
|
||||||
|
.json();
|
||||||
|
|
||||||
if (activeSession?.endAt) {
|
// Проверяем, является ли ответ ошибкой
|
||||||
setEndAt(activeSession.endAt);
|
if (isErrorResponse(activeSession)) {
|
||||||
|
// Для SESSION_NOT_FOUND не показываем toast и не делаем редирект
|
||||||
|
// Просто возвращаем null, чтобы показать сообщение о завершении
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeSession?.endAt) {
|
||||||
|
setEndAt(activeSession.endAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
return activeSession;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch active session:", error);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return activeSession;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkSessionStatus() {
|
async function checkSessionStatus() {
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
export enum ErrorCode {
|
||||||
|
// General errors
|
||||||
|
INTERNAL_ERROR = "INTERNAL_ERROR",
|
||||||
|
|
||||||
|
// Active Session errors
|
||||||
|
INVALID_OBJECT_ID = "INVALID_OBJECT_ID",
|
||||||
|
SESSION_NOT_FOUND = "SESSION_NOT_FOUND",
|
||||||
|
SESSION_FETCH_ERROR = "SESSION_FETCH_ERROR",
|
||||||
|
|
||||||
|
// Country Code errors
|
||||||
|
IP_ADDRESS_ERROR = "IP_ADDRESS_ERROR",
|
||||||
|
COUNTRY_CODE_FETCH_ERROR = "COUNTRY_CODE_FETCH_ERROR",
|
||||||
|
|
||||||
|
// Email/Invite errors
|
||||||
|
EMAIL_REQUIRED = "EMAIL_REQUIRED",
|
||||||
|
LINK_REQUIRED = "LINK_REQUIRED",
|
||||||
|
EMAIL_SEND_ERROR = "EMAIL_SEND_ERROR",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ErrorResponse {
|
||||||
|
error: string;
|
||||||
|
errorCode: ErrorCode;
|
||||||
|
}
|
||||||
+142
-1
@@ -1,7 +1,148 @@
|
|||||||
import ky from "ky";
|
import ky from "ky";
|
||||||
|
|
||||||
const api = ky.extend({
|
/**
|
||||||
|
* API Utility с автоматическим управлением регионом пользователя
|
||||||
|
*
|
||||||
|
* Как это работает:
|
||||||
|
* 1. При первом запросе определяется регион пользователя через detectUserRegion()
|
||||||
|
* 2. Регион сохраняется в state (userRegion)
|
||||||
|
* 3. Все последующие запросы через api автоматически включают заголовок X-User-Region
|
||||||
|
* 4. Сервер возвращает ошибки на нужном языке в зависимости от региона
|
||||||
|
* 5. Ошибки с сервера содержат errorCode и обрабатываются через errorHandler
|
||||||
|
*
|
||||||
|
* Пример использования:
|
||||||
|
*
|
||||||
|
* // В App.tsx или главном компоненте при инициализации:
|
||||||
|
* useEffect(() => {
|
||||||
|
* async function init() {
|
||||||
|
* try {
|
||||||
|
* const region = await detectUserRegion();
|
||||||
|
* console.log('User region:', region); // RU, EN, NL и т.д.
|
||||||
|
* } catch (error) {
|
||||||
|
* console.error('Failed to detect region:', error);
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* init();
|
||||||
|
* }, []);
|
||||||
|
*
|
||||||
|
* // Во всех остальных местах просто используйте api с обработкой ошибок:
|
||||||
|
* import api from './utils/api';
|
||||||
|
* import { handleApiError, isErrorResponse } from './utils/errorHandler';
|
||||||
|
* import { useTranslation } from 'react-i18next';
|
||||||
|
*
|
||||||
|
* const { t } = useTranslation();
|
||||||
|
* const response = await api.get('endpoint').json();
|
||||||
|
*
|
||||||
|
* // Проверяем и обрабатываем ошибки
|
||||||
|
* if (isErrorResponse(response)) {
|
||||||
|
* handleApiError(response, t);
|
||||||
|
* return;
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* // Работаем с успешным ответом
|
||||||
|
* console.log(response);
|
||||||
|
*
|
||||||
|
* // Или используйте safeApiCall для автоматической обработки:
|
||||||
|
* import { safeApiCall } from './utils/errorHandler';
|
||||||
|
*
|
||||||
|
* const data = await safeApiCall(
|
||||||
|
* () => api.get('endpoint').json(),
|
||||||
|
* t,
|
||||||
|
* (response) => console.log('Success:', response)
|
||||||
|
* );
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Хранилище для региона пользователя
|
||||||
|
let userRegion: string | null = null;
|
||||||
|
|
||||||
|
// Базовый API клиент
|
||||||
|
const baseApi = ky.extend({
|
||||||
prefixUrl: import.meta.env.VITE_API_URL,
|
prefixUrl: import.meta.env.VITE_API_URL,
|
||||||
|
hooks: {
|
||||||
|
beforeRequest: [
|
||||||
|
(request) => {
|
||||||
|
// Добавляем регион в заголовки, если он уже определен
|
||||||
|
if (userRegion) {
|
||||||
|
request.headers.set("X-User-Region", userRegion);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает регион пользователя с сервера
|
||||||
|
* @returns Код региона (RU, EN, NL и т.д.)
|
||||||
|
*/
|
||||||
|
export async function detectUserRegion(): Promise<string> {
|
||||||
|
try {
|
||||||
|
const response = await baseApi.get("getCountryCode").json<{
|
||||||
|
countryCode: string;
|
||||||
|
error?: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
if (response.error) {
|
||||||
|
throw new Error(response.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.countryCode) {
|
||||||
|
// Сохраняем регион в state
|
||||||
|
userRegion = response.countryCode;
|
||||||
|
return response.countryCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("Country code not received");
|
||||||
|
} catch (error) {
|
||||||
|
// В случае ошибки используем дефолтное значение
|
||||||
|
userRegion = "EN";
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает текущий регион пользователя
|
||||||
|
* @returns Код региона или null, если еще не определен
|
||||||
|
*/
|
||||||
|
export function getUserRegion(): string | null {
|
||||||
|
return userRegion;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Устанавливает регион пользователя вручную
|
||||||
|
* @param region - Код региона
|
||||||
|
*/
|
||||||
|
export function setUserRegion(region: string): void {
|
||||||
|
userRegion = region;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Сбрасывает регион пользователя
|
||||||
|
*/
|
||||||
|
export function resetUserRegion(): void {
|
||||||
|
userRegion = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Создает объект заголовков с регионом пользователя для внешних запросов
|
||||||
|
* Используйте эту функцию для запросов, которые не используют базовый api клиент
|
||||||
|
* (например, для VITE_COORD_URL)
|
||||||
|
*
|
||||||
|
* @returns Объект с заголовками, включая X-User-Region если регион определен
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const headers = getRegionHeaders();
|
||||||
|
* await ky.get(url, { headers }).json();
|
||||||
|
*/
|
||||||
|
export function getRegionHeaders(): Record<string, string> {
|
||||||
|
const headers: Record<string, string> = {};
|
||||||
|
|
||||||
|
if (userRegion) {
|
||||||
|
headers["X-User-Region"] = userRegion;
|
||||||
|
}
|
||||||
|
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
const api = baseApi;
|
||||||
|
|
||||||
export default api;
|
export default api;
|
||||||
|
|||||||
@@ -0,0 +1,261 @@
|
|||||||
|
import { TFunction } from "i18next";
|
||||||
|
import { toast, Bounce } from "react-toastify";
|
||||||
|
import { ErrorCode, ErrorResponse } from "../types/ErrorTypes";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Типы действий при ошибке
|
||||||
|
*/
|
||||||
|
export enum ErrorAction {
|
||||||
|
TOAST = "TOAST", // Показать toast уведомление
|
||||||
|
REDIRECT = "REDIRECT", // Перенаправить на другую страницу
|
||||||
|
MODAL = "MODAL", // Показать модальное окно (для будущего использования)
|
||||||
|
CALLBACK = "CALLBACK", // Вызвать кастомный callback
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Конфигурация обработки ошибки
|
||||||
|
*/
|
||||||
|
interface ErrorHandlerConfig {
|
||||||
|
action: ErrorAction;
|
||||||
|
redirectUrl?: string;
|
||||||
|
callback?: (errorCode: ErrorCode, errorMessage: string) => void;
|
||||||
|
toastType?: "error" | "warning" | "info";
|
||||||
|
navigate?: (path: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Маппинг кодов ошибок на действия
|
||||||
|
*/
|
||||||
|
const errorActionMap: Record<ErrorCode, ErrorHandlerConfig> = {
|
||||||
|
// General errors - показываем toast
|
||||||
|
[ErrorCode.INTERNAL_ERROR]: {
|
||||||
|
action: ErrorAction.TOAST,
|
||||||
|
toastType: "error",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Active Session errors
|
||||||
|
[ErrorCode.INVALID_OBJECT_ID]: {
|
||||||
|
action: ErrorAction.TOAST,
|
||||||
|
toastType: "error",
|
||||||
|
},
|
||||||
|
[ErrorCode.SESSION_NOT_FOUND]: {
|
||||||
|
action: ErrorAction.CALLBACK,
|
||||||
|
// Не показываем toast и не делаем редирект
|
||||||
|
// Компонент сам решит, что делать с этой ошибкой
|
||||||
|
},
|
||||||
|
[ErrorCode.SESSION_FETCH_ERROR]: {
|
||||||
|
action: ErrorAction.TOAST,
|
||||||
|
toastType: "error",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Country Code errors
|
||||||
|
[ErrorCode.IP_ADDRESS_ERROR]: {
|
||||||
|
action: ErrorAction.TOAST,
|
||||||
|
toastType: "warning",
|
||||||
|
},
|
||||||
|
[ErrorCode.COUNTRY_CODE_FETCH_ERROR]: {
|
||||||
|
action: ErrorAction.TOAST,
|
||||||
|
toastType: "warning",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Email/Invite errors
|
||||||
|
[ErrorCode.EMAIL_REQUIRED]: {
|
||||||
|
action: ErrorAction.TOAST,
|
||||||
|
toastType: "error",
|
||||||
|
},
|
||||||
|
[ErrorCode.LINK_REQUIRED]: {
|
||||||
|
action: ErrorAction.TOAST,
|
||||||
|
toastType: "error",
|
||||||
|
},
|
||||||
|
[ErrorCode.EMAIL_SEND_ERROR]: {
|
||||||
|
action: ErrorAction.TOAST,
|
||||||
|
toastType: "error",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Показывает toast уведомление
|
||||||
|
*/
|
||||||
|
function showToast(
|
||||||
|
message: string,
|
||||||
|
type: "error" | "warning" | "info" = "error"
|
||||||
|
) {
|
||||||
|
const toastConfig = {
|
||||||
|
position: "top-center" as const,
|
||||||
|
autoClose: 5000,
|
||||||
|
hideProgressBar: false,
|
||||||
|
closeOnClick: true,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: true,
|
||||||
|
progress: undefined,
|
||||||
|
theme: "light" as const,
|
||||||
|
transition: Bounce,
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case "error":
|
||||||
|
toast.error(message, toastConfig);
|
||||||
|
break;
|
||||||
|
case "warning":
|
||||||
|
toast.warning(message, toastConfig);
|
||||||
|
break;
|
||||||
|
case "info":
|
||||||
|
toast.info(message, toastConfig);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Основная функция обработки ошибок
|
||||||
|
*
|
||||||
|
* @param response - Ответ от сервера с ошибкой
|
||||||
|
* @param t - Функция перевода из i18next (необязательно, если сообщение уже переведено)
|
||||||
|
* @param navigate - Функция навигации из useNavigate (для редиректов)
|
||||||
|
* @param customConfig - Кастомная конфигурация обработки (переопределяет дефолтную)
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Базовое использование
|
||||||
|
* const { t } = useTranslation();
|
||||||
|
* const navigate = useNavigate();
|
||||||
|
* const response = await api.get('endpoint').json();
|
||||||
|
* if (response.errorCode) {
|
||||||
|
* handleApiError(response, t, navigate);
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // С кастомной обработкой
|
||||||
|
* handleApiError(response, t, navigate, {
|
||||||
|
* action: ErrorAction.CALLBACK,
|
||||||
|
* callback: (code, message) => {
|
||||||
|
* console.log('Custom error handling', code, message);
|
||||||
|
* }
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function handleApiError(
|
||||||
|
response: ErrorResponse | { error: string; errorCode?: ErrorCode },
|
||||||
|
t?: TFunction,
|
||||||
|
navigate?: (path: string) => void,
|
||||||
|
customConfig?: Partial<ErrorHandlerConfig>
|
||||||
|
): void {
|
||||||
|
const errorCode = response.errorCode;
|
||||||
|
const errorMessage = response.error;
|
||||||
|
|
||||||
|
// Если нет кода ошибки, просто показываем сообщение
|
||||||
|
if (!errorCode) {
|
||||||
|
showToast(errorMessage, "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем конфигурацию для данного кода ошибки
|
||||||
|
const defaultConfig = errorActionMap[errorCode];
|
||||||
|
const config = { ...defaultConfig, ...customConfig, navigate };
|
||||||
|
|
||||||
|
// Переводим сообщение, если передана функция перевода и есть ключ
|
||||||
|
const translatedMessage = t && errorCode
|
||||||
|
? t(`errors.${errorCode}`, { defaultValue: errorMessage })
|
||||||
|
: errorMessage;
|
||||||
|
|
||||||
|
// Выполняем действие в зависимости от конфигурации
|
||||||
|
switch (config.action) {
|
||||||
|
case ErrorAction.TOAST:
|
||||||
|
showToast(translatedMessage, config.toastType || "error");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ErrorAction.REDIRECT:
|
||||||
|
if (config.redirectUrl) {
|
||||||
|
showToast(translatedMessage, "error");
|
||||||
|
const redirectUrl = config.redirectUrl;
|
||||||
|
setTimeout(() => {
|
||||||
|
// Используем React Router navigate если передан, иначе window.location
|
||||||
|
if (config.navigate) {
|
||||||
|
config.navigate(redirectUrl);
|
||||||
|
} else {
|
||||||
|
window.location.href = redirectUrl;
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ErrorAction.CALLBACK:
|
||||||
|
if (config.callback) {
|
||||||
|
config.callback(errorCode, translatedMessage);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ErrorAction.MODAL:
|
||||||
|
// Для будущей реализации модальных окон
|
||||||
|
console.warn("MODAL action not implemented yet, showing toast instead");
|
||||||
|
showToast(translatedMessage, config.toastType || "error");
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
showToast(translatedMessage, "error");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Логируем ошибку для отладки
|
||||||
|
console.error(`API Error [${errorCode}]:`, translatedMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет, является ли ответ ошибкой
|
||||||
|
*/
|
||||||
|
export function isErrorResponse(
|
||||||
|
response: unknown
|
||||||
|
): response is ErrorResponse {
|
||||||
|
return (
|
||||||
|
typeof response === "object" &&
|
||||||
|
response !== null &&
|
||||||
|
"errorCode" in response
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Обертка для безопасного выполнения API запросов с автоматической обработкой ошибок
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const navigate = useNavigate();
|
||||||
|
* const result = await safeApiCall(
|
||||||
|
* () => api.get('endpoint').json(),
|
||||||
|
* t,
|
||||||
|
* navigate,
|
||||||
|
* (data) => {
|
||||||
|
* console.log('Success:', data);
|
||||||
|
* }
|
||||||
|
* );
|
||||||
|
*/
|
||||||
|
export async function safeApiCall<T>(
|
||||||
|
apiCall: () => Promise<T>,
|
||||||
|
t?: TFunction,
|
||||||
|
navigate?: (path: string) => void,
|
||||||
|
onSuccess?: (data: T) => void,
|
||||||
|
onError?: (error: ErrorResponse) => void
|
||||||
|
): Promise<T | null> {
|
||||||
|
try {
|
||||||
|
const response = await apiCall();
|
||||||
|
|
||||||
|
// Проверяем, является ли ответ ошибкой
|
||||||
|
if (isErrorResponse(response)) {
|
||||||
|
handleApiError(response, t, navigate);
|
||||||
|
if (onError) {
|
||||||
|
onError(response);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Вызываем callback успеха, если он передан
|
||||||
|
if (onSuccess) {
|
||||||
|
onSuccess(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
// Обрабатываем сетевые ошибки
|
||||||
|
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
||||||
|
showToast(
|
||||||
|
t ? t("errors.networkError", { defaultValue: errorMessage }) : errorMessage,
|
||||||
|
"error"
|
||||||
|
);
|
||||||
|
console.error("API Call Error:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user