fixed get profile avatar and get user from back
This commit is contained in:
131
src/hooks/useGeolocation.js
Normal file
131
src/hooks/useGeolocation.js
Normal 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 };
|
||||
}
|
||||
@@ -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 || []);
|
||||
|
||||
@@ -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}`, {
|
||||
|
||||
Reference in New Issue
Block a user