apiFetch оборачивает сбой сети в ApiError(0, NETWORK) и не-JSON тело 2xx в ApiError(BAD_RESPONSE); AbortError пробрасывается как есть, пустое тело даёт undefined. В production-ветке входа показываются ошибка загрузки профиля и кнопка «Повторить», форма dev-входа по-прежнему вырезается из бандла литералом process.env.NODE_ENV. Мелочи: «Сессия истекла» при 401, trim и whitespace-правило для email, центрирование по 100svh, union-типы прав, срез хвостовых слэшей в API_BASE. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
66 lines
2.0 KiB
TypeScript
66 lines
2.0 KiB
TypeScript
import { useCallback, useEffect, useState } from 'react';
|
||
import { UNAUTHORIZED_EVENT, api } from '../api/client';
|
||
import type { Me } from '../api/types';
|
||
import { getToken } from './token';
|
||
|
||
type MeState = { me: Me | null; loading: boolean; error: string | null; reload: () => void };
|
||
|
||
/**
|
||
* Профиль текущего пользователя (`GET /me`). Запрос уходит только при наличии токена; ответ после размонтирования
|
||
* или после повторного `reload` (устаревший) отбрасывается. На событие `armka:unauthorized` профиль сбрасывается.
|
||
*/
|
||
export function useMe(): MeState {
|
||
const [me, setMe] = useState<Me | null>(null);
|
||
const [loading, setLoading] = useState<boolean>(() => getToken() !== null);
|
||
const [error, setError] = useState<string | null>(null);
|
||
const [reloadCount, setReloadCount] = useState(0);
|
||
|
||
useEffect(() => {
|
||
// reloadCount в зависимостях: его смена перезапускает загрузку
|
||
void reloadCount;
|
||
if (!getToken()) {
|
||
setMe(null);
|
||
setLoading(false);
|
||
return;
|
||
}
|
||
let cancelled = false;
|
||
setLoading(true);
|
||
setError(null);
|
||
api
|
||
.get<Me>('/me')
|
||
.then((data) => {
|
||
if (!cancelled) {
|
||
setMe(data);
|
||
}
|
||
})
|
||
.catch((e: unknown) => {
|
||
if (!cancelled) {
|
||
setMe(null);
|
||
setError(e instanceof Error ? e.message : 'Не удалось загрузить профиль');
|
||
}
|
||
})
|
||
.finally(() => {
|
||
if (!cancelled) {
|
||
setLoading(false);
|
||
}
|
||
});
|
||
return () => {
|
||
cancelled = true;
|
||
};
|
||
}, [reloadCount]);
|
||
|
||
useEffect(() => {
|
||
const onUnauthorized = () => {
|
||
setMe(null);
|
||
setLoading(false);
|
||
setError('Сессия истекла');
|
||
};
|
||
window.addEventListener(UNAUTHORIZED_EVENT, onUnauthorized);
|
||
return () => window.removeEventListener(UNAUTHORIZED_EVENT, onUnauthorized);
|
||
}, []);
|
||
|
||
const reload = useCallback(() => setReloadCount((n) => n + 1), []);
|
||
|
||
return { me, loading, error, reload };
|
||
}
|