fixed get profile avatar and get user from back

This commit is contained in:
SlimusMinus
2026-09-03 00:29:19 +03:00
parent a5ecf3feb3
commit 4b0cb4e5a9
9 changed files with 262 additions and 14 deletions

131
src/hooks/useGeolocation.js Normal file
View File

@@ -0,0 +1,131 @@
import { useCallback, useEffect, useRef, useState } from "react";
export const GEO_STATUS = {
IDLE: "idle",
LOADING: "loading",
SUCCESS: "success",
DENIED: "denied",
UNSUPPORTED: "unsupported",
TIMEOUT: "timeout",
};
const DEFAULT_TIMEOUT = 10000;
/**
* Определяет код ошибки геолокации в понятный статус.
* @param {string|number} code — код из GeolocationPositionError
*/
function codeToStatus(code) {
switch (code) {
case 1: // PERMISSION_DENIED
return GEO_STATUS.DENIED;
case 2: // POSITION_UNAVAILABLE
return GEO_STATUS.UNSUPPORTED;
case 3: // TIMEOUT
return GEO_STATUS.TIMEOUT;
default:
return GEO_STATUS.UNSUPPORTED;
}
}
/**
* useGeolocation — переиспользуемый хук для запроса геолокации браузера.
*
* Возвращает:
* - coords { latitude, longitude, accuracy } | null
* - error текст ошибки (для логирования/диагностики) | null
* - status GEO_STATUS.* (loading / success / denied / unsupported / timeout)
* - requestLocation () => void — запустить запрос заново
*
* @param {Object} [options]
* @param {boolean} [options.onMount] запросить геолокацию сразу при монтировании
* @param {boolean} [options.enableHighAccuracy] точный режим (по умолчанию true)
* @param {number} [options.timeout] таймаут в мс (по умолчанию 10000)
*/
export function useGeolocation({
onMount = false,
enableHighAccuracy = true,
timeout = DEFAULT_TIMEOUT,
} = {}) {
const [coords, setCoords] = useState(null);
const [error, setError] = useState(null);
const [status, setStatus] = useState(GEO_STATUS.IDLE);
const timeoutRef = useRef(null);
const mountedRef = useRef(true);
useEffect(() => {
mountedRef.current = true;
return () => {
mountedRef.current = false;
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
};
}, []);
const requestLocation = useCallback(() => {
// Либо API полностью отсутствует, либо у геолокации нет getCurrentPosition.
if (!("geolocation" in navigator) || !navigator.geolocation) {
setCoords(null);
setError("Geolocation is not supported by this browser");
setStatus(GEO_STATUS.UNSUPPORTED);
return;
}
setStatus(GEO_STATUS.LOADING);
setError(null);
// Страховочный таймер: некоторые браузеры (Safari/WebKit) могут
// «зависать» в pending без вызова колбэков — не держим пользователя.
if (timeoutRef.current) clearTimeout(timeoutRef.current);
timeoutRef.current = setTimeout(() => {
if (!mountedRef.current) return;
setCoords(null);
setError("Geolocation request timed out");
setStatus(GEO_STATUS.TIMEOUT);
}, timeout);
const finish = (resolvedCoords) => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
if (!mountedRef.current) return;
setCoords({
latitude: resolvedCoords.latitude,
longitude: resolvedCoords.longitude,
accuracy: resolvedCoords.accuracy ?? null,
});
setError(null);
setStatus(GEO_STATUS.SUCCESS);
};
const fail = (err) => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
if (!mountedRef.current) return;
setCoords(null);
setError(err && err.message ? err.message : "Geolocation failed");
setStatus(codeToStatus(err && err.code));
};
navigator.geolocation.getCurrentPosition(
(pos) => finish(pos.coords),
fail,
{ enableHighAccuracy, timeout, maximumAge: 60000 }
);
}, [enableHighAccuracy, timeout]);
useEffect(() => {
if (onMount) requestLocation();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return { coords, error, status, requestLocation };
}

View File

@@ -26,9 +26,15 @@ export function useImageUpload(existingUrls = []) {
const fileInputRef = useRef(null);
const existingUrlList = Array.isArray(existingUrls) ? existingUrls : [];
const existingUrlsKey = existingUrlList.join("\u0000");
// Синхронизируем существующие изображения при изменениях извне (например,
// когда свежие данные профиля подтягиваются с бэка уже после монтирования).
// Не перезаписываем стейт, если пользователь успел выбрать новые файлы.
const hasUserSelected = images.some(img => !img.isExisting);
useEffect(() => {
if (existingUrlList.length > 0) {
if (existingUrlList.length > 0 && !hasUserSelected) {
setImages(existingUrlList.map((url, i) => ({
id: `existing-${i}`,
file: null,
@@ -38,7 +44,7 @@ export function useImageUpload(existingUrls = []) {
})));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
}, [existingUrlsKey, hasUserSelected]);
const handleFileChange = useCallback(async (e) => {
const files = Array.from(e.target.files || []);

View File

@@ -35,9 +35,16 @@ export function useUpdateProfile() {
formData.append("phone", profileData.phone || "");
formData.append("email", profileData.email || "");
// Если avatar — это File (новый файл), отправляем как часть FormData
if (profileData.avatar instanceof File) {
formData.append("avatar", profileData.avatar);
// Новый аватар: compressImage может вернуть Blob (а не File), поэтому
// проверяем по Blob — File является его подклассом, так что оба случая покрыты.
// Blob может не иметь .name, а Spring ждёт filename в multipart-части — задаём явно.
if (profileData.avatar instanceof Blob) {
formData.append("avatar", profileData.avatar, profileData.avatar.name || "avatar.jpg");
}
// Явное удаление аватара без замены новым файлом
if (profileData.removeAvatar) {
formData.append("removeAvatar", "true");
}
const res = await apiFetch(`${API_URL}/api/v1/users/${userId}`, {