Аватар
This commit is contained in:
@@ -39,7 +39,7 @@ export class UserApi {
|
||||
});
|
||||
}
|
||||
|
||||
static async cropAvatar(file: File, cropData: { x: number; y: number; width: number; height: number }) {
|
||||
static async cropAvatar(file: File, cropData: { x: number; y: number; width: number; height: number; sw: number; sh: number }) {
|
||||
// Конечная точка в новом бэкенде: POST /api/profiles/avatar/crop
|
||||
const formData = new FormData();
|
||||
formData.append('avatar', file);
|
||||
@@ -47,6 +47,8 @@ export class UserApi {
|
||||
formData.append('y', cropData.y.toString());
|
||||
formData.append('width', cropData.width.toString());
|
||||
formData.append('height', cropData.height.toString());
|
||||
formData.append('sw', cropData.sw.toString());
|
||||
formData.append('sh', cropData.sh.toString());
|
||||
|
||||
return httpClient.request<User>('/profiles/avatar/crop', {
|
||||
method: 'POST',
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useLang } from '../../../../core/infrastructure/i18n';
|
||||
|
||||
interface AvatarCropModalProps {
|
||||
image: string;
|
||||
onCrop: (cropData: Area) => void;
|
||||
onCrop: (cropData: Area, pixelCropData: Area) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
@@ -14,15 +14,18 @@ export default function AvatarCropModal({ image, onCrop, onClose }: AvatarCropMo
|
||||
const { t } = useLang();
|
||||
const [crop, setCrop] = useState<Point>({ x: 0, y: 0 });
|
||||
const [zoom, setZoom] = useState(1);
|
||||
const [croppedArea, setCroppedArea] = useState<Area | null>(null);
|
||||
const [croppedAreaPixels, setCroppedAreaPixels] = useState<Area | null>(null);
|
||||
|
||||
const onCropComplete = useCallback((_croppedArea: Area, croppedAreaPixels: Area) => {
|
||||
|
||||
const onCropComplete = useCallback((croppedArea: Area, croppedAreaPixels: Area) => {
|
||||
setCroppedArea(croppedArea);
|
||||
setCroppedAreaPixels(croppedAreaPixels);
|
||||
}, []);
|
||||
|
||||
const handleSave = () => {
|
||||
if (croppedAreaPixels) {
|
||||
onCrop(croppedAreaPixels);
|
||||
if (croppedArea && croppedAreaPixels) {
|
||||
onCrop(croppedArea, croppedAreaPixels);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import { getMediaUrl, getInitials } from '../../../../core/utils/utils';
|
||||
import DatePicker from '../../../../core/presentation/components/ui/DatePicker';
|
||||
import AvatarCropModal from './AvatarCropModal';
|
||||
import { Area } from 'react-easy-crop';
|
||||
import { getCroppedImg } from '../../../../core/utils/cropImage';
|
||||
|
||||
export default function SettingsPage() {
|
||||
const { user, updateUser, logout } = useAuthStore();
|
||||
@@ -27,7 +28,11 @@ export default function SettingsPage() {
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
// Avatar cropping state
|
||||
const [cropModal, setCropModal] = useState<{ open: boolean, image: string, file: File | null }>({ open: false, image: '', file: null });
|
||||
const [cropModal, setCropModal] = useState<{
|
||||
open: boolean,
|
||||
image: string,
|
||||
file: File | null
|
||||
}>({ open: false, image: '', file: null });
|
||||
const [uploadingAvatar, setUploadingAvatar] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
@@ -63,26 +68,30 @@ export default function SettingsPage() {
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
setCropModal({ open: true, image: reader.result as string, file });
|
||||
setCropModal({
|
||||
open: true,
|
||||
image: reader.result as string,
|
||||
file
|
||||
});
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
e.target.value = '';
|
||||
};
|
||||
|
||||
const onCropConfirm = async (cropData: Area) => {
|
||||
if (!cropModal.file) return;
|
||||
const onCropConfirm = async (_area: Area, pixelArea: Area) => {
|
||||
if (!cropModal.file || !cropModal.image) return;
|
||||
setCropModal(prev => ({ ...prev, open: false }));
|
||||
setUploadingAvatar(true);
|
||||
try {
|
||||
const updatedUser = await UserApi.cropAvatar(cropModal.file, {
|
||||
x: Math.round(cropData.x),
|
||||
y: Math.round(cropData.y),
|
||||
width: Math.round(cropData.width),
|
||||
height: Math.round(cropData.height)
|
||||
});
|
||||
const croppedBlob = await getCroppedImg(cropModal.image, pixelArea);
|
||||
if (!croppedBlob) throw new Error('Failed to crop image');
|
||||
|
||||
const croppedFile = new File([croppedBlob], 'avatar.jpg', { type: 'image/jpeg' });
|
||||
const updatedUser = await UserApi.uploadAvatar(croppedFile);
|
||||
updateUser(updatedUser);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
console.error('Failed to update avatar:', err);
|
||||
} finally {
|
||||
setUploadingAvatar(false);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user