feat: русский UI, версии в футере, даты и устойчивость загрузки API
Some checks failed
CI / build-and-test (pull_request) Has been cancelled
Some checks failed
CI / build-and-test (pull_request) Has been cancelled
- API: дата старта всегда YYYY-MM-DD; фронт: parseRaceDate без двойного T00:00:00 - GET /health с version из package.json; Vite define __FRONTEND_VERSION__ - Футер с версиями клиента/сервера (BEM), сетка app-shell на три ряда - AbortController для карточки старта; ретраи GET при 502–504 и понятные ошибки шлюза - Русские подписи навигации/страниц, lang=ru, без английских фраз в интерфейсе
This commit is contained in:
@@ -20,36 +20,68 @@ async function parseResponseBody(response: Response): Promise<unknown> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function requestJson<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
try {
|
||||
const response = await fetch(buildUrl(path), {
|
||||
...init,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(init?.headers ?? {}),
|
||||
},
|
||||
});
|
||||
const GATEWAY_RETRY_STATUSES = new Set([502, 503, 504]);
|
||||
|
||||
if (response.status === 204) {
|
||||
return undefined as T;
|
||||
}
|
||||
|
||||
const payload = await parseResponseBody(response);
|
||||
|
||||
if (!response.ok) {
|
||||
throw toApiError(response.status, payload);
|
||||
}
|
||||
|
||||
return payload as T;
|
||||
} catch (error) {
|
||||
if (error instanceof ApiError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new ApiError({
|
||||
code: "network_error",
|
||||
status: null,
|
||||
message: "Не удалось связаться с сервером.",
|
||||
});
|
||||
}
|
||||
function delay(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
}
|
||||
|
||||
export async function requestJson<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
const method = (init?.method ?? "GET").toUpperCase();
|
||||
const idempotent = method === "GET" || method === "HEAD";
|
||||
const maxAttempts = idempotent ? 3 : 1;
|
||||
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
||||
try {
|
||||
const response = await fetch(buildUrl(path), {
|
||||
...init,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(init?.headers ?? {}),
|
||||
},
|
||||
});
|
||||
|
||||
if (response.status === 204) {
|
||||
return undefined as T;
|
||||
}
|
||||
|
||||
const payload = await parseResponseBody(response);
|
||||
|
||||
if (!response.ok) {
|
||||
const retryable = idempotent && GATEWAY_RETRY_STATUSES.has(response.status) && attempt < maxAttempts;
|
||||
if (retryable) {
|
||||
await delay(80 * attempt);
|
||||
continue;
|
||||
}
|
||||
throw toApiError(response.status, payload);
|
||||
}
|
||||
|
||||
return payload as T;
|
||||
} catch (error) {
|
||||
if (error instanceof ApiError) {
|
||||
throw error;
|
||||
}
|
||||
if (error instanceof DOMException && error.name === "AbortError") {
|
||||
throw error;
|
||||
}
|
||||
const retryable = idempotent && attempt < maxAttempts;
|
||||
if (retryable) {
|
||||
await delay(80 * attempt);
|
||||
continue;
|
||||
}
|
||||
throw new ApiError({
|
||||
code: "network_error",
|
||||
status: null,
|
||||
message: "Не удалось связаться с сервером.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
throw new ApiError({
|
||||
code: "network_error",
|
||||
status: null,
|
||||
message: "Не удалось связаться с сервером.",
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user