Аватар
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
namespace Knot.Contracts.Auth.Application.Auth.DTOs;
|
namespace Knot.Contracts.Auth.Application.Auth.DTOs;
|
||||||
|
|
||||||
public class AuthResponseDto
|
public class AuthResponseDto
|
||||||
{
|
{
|
||||||
@@ -7,6 +7,7 @@ public class AuthResponseDto
|
|||||||
public Guid UserId { get; set; }
|
public Guid UserId { get; set; }
|
||||||
public string Username { get; set; } = string.Empty;
|
public string Username { get; set; } = string.Empty;
|
||||||
public string? DisplayName { get; set; }
|
public string? DisplayName { get; set; }
|
||||||
|
public string? Avatar { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ResetPasswordDto
|
public class ResetPasswordDto
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ internal sealed class GetMeQueryHandler : IQueryHandler<GetMeQuery, AuthResponse
|
|||||||
RefreshToken = string.Empty,
|
RefreshToken = string.Empty,
|
||||||
UserId = user.Id,
|
UserId = user.Id,
|
||||||
Username = user.Username,
|
Username = user.Username,
|
||||||
DisplayName = user.DisplayName
|
DisplayName = user.DisplayName,
|
||||||
|
Avatar = user.Avatar
|
||||||
};
|
};
|
||||||
|
|
||||||
return Result.Success(response);
|
return Result.Success(response);
|
||||||
|
|||||||
@@ -72,6 +72,9 @@ public sealed class User : AggregateRoot<Guid>
|
|||||||
Username = contract.Username;
|
Username = contract.Username;
|
||||||
DisplayName = contract.DisplayName;
|
DisplayName = contract.DisplayName;
|
||||||
PhoneNumber = contract.PhoneNumber;
|
PhoneNumber = contract.PhoneNumber;
|
||||||
|
Bio = contract.Bio;
|
||||||
|
Avatar = contract.Avatar;
|
||||||
|
Birthday = contract.Birthday;
|
||||||
IsBanned = contract.IsBanned;
|
IsBanned = contract.IsBanned;
|
||||||
BannedUntil = contract.BannedUntil;
|
BannedUntil = contract.BannedUntil;
|
||||||
SetOnline(contract.IsOnline, contract.LastSeen);
|
SetOnline(contract.IsOnline, contract.LastSeen);
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ internal sealed class UserRepository : IUserRepository
|
|||||||
{
|
{
|
||||||
domainUser.UpdateFromContract(user);
|
domainUser.UpdateFromContract(user);
|
||||||
_context.Users.Update(domainUser);
|
_context.Users.Update(domainUser);
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,7 @@
|
|||||||
using Knot.Shared.Kernel;
|
|
||||||
using Knot.Contracts.Profiles.Domain;
|
using Knot.Contracts.Profiles.Domain;
|
||||||
using Knot.Contracts.Profiles.Application.DTOs;
|
using Knot.Contracts.Profiles.Application.DTOs;
|
||||||
using SixLabors.ImageSharp;
|
using SixLabors.ImageSharp;
|
||||||
using SixLabors.ImageSharp.Processing;
|
using SixLabors.ImageSharp.Processing;
|
||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Knot.Modules.Profiles.Application.Profiles.Avatar;
|
namespace Knot.Modules.Profiles.Application.Profiles.Avatar;
|
||||||
|
|
||||||
@@ -15,7 +10,8 @@ public sealed record CropAvatarCommand(
|
|||||||
Stream FileStream,
|
Stream FileStream,
|
||||||
string FileName,
|
string FileName,
|
||||||
string ContentType,
|
string ContentType,
|
||||||
int X, int Y, int Width, int Height) : ICommand<UserProfileDto>;
|
int X, int Y, int Width, int Height,
|
||||||
|
int SourceWidth, int SourceHeight) : ICommand<UserProfileDto>;
|
||||||
|
|
||||||
internal sealed class CropAvatarCommandHandler : ICommandHandler<CropAvatarCommand, UserProfileDto>
|
internal sealed class CropAvatarCommandHandler : ICommandHandler<CropAvatarCommand, UserProfileDto>
|
||||||
{
|
{
|
||||||
@@ -53,11 +49,16 @@ internal sealed class CropAvatarCommandHandler : ICommandHandler<CropAvatarComma
|
|||||||
private static async Task<MemoryStream> CropAndResizeAsync(CropAvatarCommand request, CancellationToken ct)
|
private static async Task<MemoryStream> CropAndResizeAsync(CropAvatarCommand request, CancellationToken ct)
|
||||||
{
|
{
|
||||||
using var image = await Image.LoadAsync(request.FileStream, ct);
|
using var image = await Image.LoadAsync(request.FileStream, ct);
|
||||||
|
image.Mutate(x => x.AutoOrient());
|
||||||
|
|
||||||
var startX = Math.Clamp(request.X, 0, image.Width - 1);
|
// Рассчитываем коэффициент масштабирования между тем, что видел фронтенд и тем, что загрузил бэкенд
|
||||||
var startY = Math.Clamp(request.Y, 0, image.Height - 1);
|
double scaleX = (double)image.Width / request.SourceWidth;
|
||||||
var width = Math.Clamp(request.Width, 1, image.Width - startX);
|
double scaleY = (double)image.Height / request.SourceHeight;
|
||||||
var height = Math.Clamp(request.Height, 1, image.Height - startY);
|
|
||||||
|
var startX = Math.Clamp((int)(request.X * scaleX), 0, image.Width - 1);
|
||||||
|
var startY = Math.Clamp((int)(request.Y * scaleY), 0, image.Height - 1);
|
||||||
|
var width = Math.Clamp((int)(request.Width * scaleX), 1, image.Width - startX);
|
||||||
|
var height = Math.Clamp((int)(request.Height * scaleY), 1, image.Height - startY);
|
||||||
|
|
||||||
image.Mutate(ctx => ctx
|
image.Mutate(ctx => ctx
|
||||||
.Crop(new Rectangle(startX, startY, width, height))
|
.Crop(new Rectangle(startX, startY, width, height))
|
||||||
|
|||||||
@@ -18,11 +18,16 @@ internal sealed class UploadAvatarCommandHandler : ICommandHandler<UploadAvatarC
|
|||||||
{
|
{
|
||||||
private readonly IProfileRepository _repository;
|
private readonly IProfileRepository _repository;
|
||||||
private readonly IAvatarStorageService _avatarStorage;
|
private readonly IAvatarStorageService _avatarStorage;
|
||||||
|
private readonly Knot.Contracts.Auth.Domain.IUserRepository _userRepository;
|
||||||
|
|
||||||
public UploadAvatarCommandHandler(IProfileRepository repository, IAvatarStorageService avatarStorage)
|
public UploadAvatarCommandHandler(
|
||||||
|
IProfileRepository repository,
|
||||||
|
IAvatarStorageService avatarStorage,
|
||||||
|
Knot.Contracts.Auth.Domain.IUserRepository userRepository)
|
||||||
{
|
{
|
||||||
_repository = repository;
|
_repository = repository;
|
||||||
_avatarStorage = avatarStorage;
|
_avatarStorage = avatarStorage;
|
||||||
|
_userRepository = userRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Result<UserProfileDto>> Handle(UploadAvatarCommand request, CancellationToken cancellationToken)
|
public async Task<Result<UserProfileDto>> Handle(UploadAvatarCommand request, CancellationToken cancellationToken)
|
||||||
@@ -37,6 +42,14 @@ internal sealed class UploadAvatarCommandHandler : ICommandHandler<UploadAvatarC
|
|||||||
var fileId = await _avatarStorage.UploadAsync(request.FileStream, request.FileName, request.ContentType, cancellationToken);
|
var fileId = await _avatarStorage.UploadAsync(request.FileStream, request.FileName, request.ContentType, cancellationToken);
|
||||||
var avatarUrl = $"/api/files/{fileId}";
|
var avatarUrl = $"/api/files/{fileId}";
|
||||||
|
|
||||||
|
// Синхронизируем с основным модулем пользователей (Auth/Postgres)
|
||||||
|
var userContract = await _userRepository.GetByIdAsync(request.UserId, cancellationToken);
|
||||||
|
if (userContract != null)
|
||||||
|
{
|
||||||
|
userContract.Avatar = avatarUrl;
|
||||||
|
await _userRepository.UpdateAsync(userContract, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
profile.Avatar = avatarUrl;
|
profile.Avatar = avatarUrl;
|
||||||
var result = await _repository.UpdateAsync(profile, cancellationToken);
|
var result = await _repository.UpdateAsync(profile, cancellationToken);
|
||||||
if (result.IsFailure)
|
if (result.IsFailure)
|
||||||
@@ -52,11 +65,16 @@ internal sealed class DeleteAvatarCommandHandler : ICommandHandler<DeleteAvatarC
|
|||||||
{
|
{
|
||||||
private readonly IProfileRepository _repository;
|
private readonly IProfileRepository _repository;
|
||||||
private readonly IAvatarStorageService _avatarStorage;
|
private readonly IAvatarStorageService _avatarStorage;
|
||||||
|
private readonly Knot.Contracts.Auth.Domain.IUserRepository _userRepository;
|
||||||
|
|
||||||
public DeleteAvatarCommandHandler(IProfileRepository repository, IAvatarStorageService avatarStorage)
|
public DeleteAvatarCommandHandler(
|
||||||
|
IProfileRepository repository,
|
||||||
|
IAvatarStorageService avatarStorage,
|
||||||
|
Knot.Contracts.Auth.Domain.IUserRepository userRepository)
|
||||||
{
|
{
|
||||||
_repository = repository;
|
_repository = repository;
|
||||||
_avatarStorage = avatarStorage;
|
_avatarStorage = avatarStorage;
|
||||||
|
_userRepository = userRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Result<UserProfileDto>> Handle(DeleteAvatarCommand request, CancellationToken cancellationToken)
|
public async Task<Result<UserProfileDto>> Handle(DeleteAvatarCommand request, CancellationToken cancellationToken)
|
||||||
@@ -68,6 +86,14 @@ internal sealed class DeleteAvatarCommandHandler : ICommandHandler<DeleteAvatarC
|
|||||||
if (!string.IsNullOrEmpty(profile.Avatar))
|
if (!string.IsNullOrEmpty(profile.Avatar))
|
||||||
await _avatarStorage.DeleteAsync(profile.Avatar, cancellationToken);
|
await _avatarStorage.DeleteAsync(profile.Avatar, cancellationToken);
|
||||||
|
|
||||||
|
// Синхронизируем с основным модулем пользователей (Auth/Postgres)
|
||||||
|
var userContract = await _userRepository.GetByIdAsync(request.UserId, cancellationToken);
|
||||||
|
if (userContract != null)
|
||||||
|
{
|
||||||
|
userContract.Avatar = null;
|
||||||
|
await _userRepository.UpdateAsync(userContract, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
profile.Avatar = null;
|
profile.Avatar = null;
|
||||||
var result = await _repository.UpdateAsync(profile, cancellationToken);
|
var result = await _repository.UpdateAsync(profile, cancellationToken);
|
||||||
if (result.IsFailure)
|
if (result.IsFailure)
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Knot.Contracts.Profiles.Application.DTOs;
|
using Knot.Contracts.Profiles.Application.DTOs;
|
||||||
using Knot.Contracts.Profiles.Domain;
|
using Knot.Contracts.Profiles.Domain;
|
||||||
using Knot.Modules.Profiles.Domain;
|
|
||||||
using Knot.Modules.Profiles.Infrastructure.Mappings;
|
using Knot.Modules.Profiles.Infrastructure.Mappings;
|
||||||
using Knot.Shared.Kernel;
|
using Knot.Shared.Kernel;
|
||||||
using MongoDB.Bson;
|
using MongoDB.Bson;
|
||||||
@@ -76,17 +70,19 @@ internal class ProfileRepository : IProfileRepository
|
|||||||
|
|
||||||
public async Task<Result<UserProfileDto>> UpdateAsync(UserProfileDto dto, CancellationToken ct = default)
|
public async Task<Result<UserProfileDto>> UpdateAsync(UserProfileDto dto, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
var profile = await _profiles.Find(p => p.Id == dto.UserId).FirstOrDefaultAsync(ct);
|
var document = await _profiles.Find(p => p.Id == dto.UserId).FirstOrDefaultAsync(ct);
|
||||||
if (profile is null)
|
if (document is null)
|
||||||
return Result.Failure<UserProfileDto>(ProfilesErrors.ProfileNotFound);
|
return Result.Failure<UserProfileDto>(ProfilesErrors.ProfileNotFound);
|
||||||
|
|
||||||
profile.UpdateProfile(
|
document.UpdateProfile(
|
||||||
dto.DisplayName ?? profile.DisplayName,
|
dto.DisplayName ?? document.DisplayName,
|
||||||
dto.About ?? profile.Bio,
|
dto.About ?? document.Bio,
|
||||||
dto.Birthday);
|
dto.Birthday);
|
||||||
|
|
||||||
await _profiles.ReplaceOneAsync(p => p.Id == dto.UserId, profile, new ReplaceOptions { IsUpsert = true }, ct);
|
document.UpdateAvatar(dto.Avatar);
|
||||||
return Result.Success(profile.ToDto());
|
|
||||||
|
await _profiles.ReplaceOneAsync(p => p.Id == dto.UserId, document, cancellationToken: ct);
|
||||||
|
return Result.Success(document.ToDto());
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Result> UpdateStatusAsync(Guid userId, bool isBanned, bool isDeleted, CancellationToken ct = default)
|
public async Task<Result> UpdateStatusAsync(Guid userId, bool isBanned, bool isDeleted, CancellationToken ct = default)
|
||||||
|
|||||||
@@ -53,9 +53,11 @@ public static class ProfilesEndpoints
|
|||||||
int.TryParse(form["y"], out int y);
|
int.TryParse(form["y"], out int y);
|
||||||
int.TryParse(form["width"], out int width);
|
int.TryParse(form["width"], out int width);
|
||||||
int.TryParse(form["height"], out int height);
|
int.TryParse(form["height"], out int height);
|
||||||
|
int.TryParse(form["sw"], out int sw);
|
||||||
|
int.TryParse(form["sh"], out int sh);
|
||||||
|
|
||||||
using var stream = file.OpenReadStream();
|
using var stream = file.OpenReadStream();
|
||||||
var result = await sender.Send(new CropAvatarCommand(userContext.UserId, stream, file.FileName ?? "avatar.jpg", file.ContentType, x, y, width, height), ct);
|
var result = await sender.Send(new CropAvatarCommand(userContext.UserId, stream, file.FileName ?? "avatar.jpg", file.ContentType, x, y, width, height, sw, sh), ct);
|
||||||
return result.IsSuccess ? Results.Ok(result.Value) : Results.NotFound(result.Error);
|
return result.IsSuccess ? Results.Ok(result.Value) : Results.NotFound(result.Error);
|
||||||
}).DisableAntiforgery();
|
}).DisableAntiforgery();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
export const createImage = (url: string): Promise<HTMLImageElement> =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
const image = new Image();
|
||||||
|
image.addEventListener('load', () => resolve(image));
|
||||||
|
image.addEventListener('error', (error) => reject(error));
|
||||||
|
image.setAttribute('crossOrigin', 'anonymous');
|
||||||
|
image.src = url;
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function getCroppedImg(
|
||||||
|
imageSrc: string,
|
||||||
|
pixelCrop: { x: number, y: number, width: number, height: number }
|
||||||
|
): Promise<Blob | null> {
|
||||||
|
const image = await createImage(imageSrc);
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
if (!ctx) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.width = pixelCrop.width;
|
||||||
|
canvas.height = pixelCrop.height;
|
||||||
|
|
||||||
|
ctx.drawImage(
|
||||||
|
image,
|
||||||
|
pixelCrop.x,
|
||||||
|
pixelCrop.y,
|
||||||
|
pixelCrop.width,
|
||||||
|
pixelCrop.height,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
pixelCrop.width,
|
||||||
|
pixelCrop.height
|
||||||
|
);
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
canvas.toBlob((blob) => {
|
||||||
|
resolve(blob);
|
||||||
|
}, 'image/jpeg', 0.95);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -137,8 +137,7 @@ export function getMediaUrl(url: string | null | undefined): string {
|
|||||||
if (!url) return '';
|
if (!url) return '';
|
||||||
if (url.startsWith('http') || url.startsWith('blob:') || url.startsWith('data:')) return url;
|
if (url.startsWith('http') || url.startsWith('blob:') || url.startsWith('data:')) return url;
|
||||||
|
|
||||||
// Use VITE_API_URL if defined, otherwise let it be a relative path which the browser
|
// Use VITE_API_URL if defined, but avoid duplicating '/api'
|
||||||
// will resolve against the current origin (port).
|
const baseUrl = (import.meta.env.VITE_API_URL || '').replace(/\/api$/, '');
|
||||||
const baseUrl = import.meta.env.VITE_API_URL || '';
|
|
||||||
return `${baseUrl}${url.startsWith('/') ? '' : '/'}${url}`;
|
return `${baseUrl}${url.startsWith('/') ? '' : '/'}${url}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
// Конечная точка в новом бэкенде: POST /api/profiles/avatar/crop
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('avatar', file);
|
formData.append('avatar', file);
|
||||||
@@ -47,6 +47,8 @@ export class UserApi {
|
|||||||
formData.append('y', cropData.y.toString());
|
formData.append('y', cropData.y.toString());
|
||||||
formData.append('width', cropData.width.toString());
|
formData.append('width', cropData.width.toString());
|
||||||
formData.append('height', cropData.height.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', {
|
return httpClient.request<User>('/profiles/avatar/crop', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { useLang } from '../../../../core/infrastructure/i18n';
|
|||||||
|
|
||||||
interface AvatarCropModalProps {
|
interface AvatarCropModalProps {
|
||||||
image: string;
|
image: string;
|
||||||
onCrop: (cropData: Area) => void;
|
onCrop: (cropData: Area, pixelCropData: Area) => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -14,15 +14,18 @@ export default function AvatarCropModal({ image, onCrop, onClose }: AvatarCropMo
|
|||||||
const { t } = useLang();
|
const { t } = useLang();
|
||||||
const [crop, setCrop] = useState<Point>({ x: 0, y: 0 });
|
const [crop, setCrop] = useState<Point>({ x: 0, y: 0 });
|
||||||
const [zoom, setZoom] = useState(1);
|
const [zoom, setZoom] = useState(1);
|
||||||
|
const [croppedArea, setCroppedArea] = useState<Area | null>(null);
|
||||||
const [croppedAreaPixels, setCroppedAreaPixels] = 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);
|
setCroppedAreaPixels(croppedAreaPixels);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
if (croppedAreaPixels) {
|
if (croppedArea && croppedAreaPixels) {
|
||||||
onCrop(croppedAreaPixels);
|
onCrop(croppedArea, croppedAreaPixels);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { getMediaUrl, getInitials } from '../../../../core/utils/utils';
|
|||||||
import DatePicker from '../../../../core/presentation/components/ui/DatePicker';
|
import DatePicker from '../../../../core/presentation/components/ui/DatePicker';
|
||||||
import AvatarCropModal from './AvatarCropModal';
|
import AvatarCropModal from './AvatarCropModal';
|
||||||
import { Area } from 'react-easy-crop';
|
import { Area } from 'react-easy-crop';
|
||||||
|
import { getCroppedImg } from '../../../../core/utils/cropImage';
|
||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
const { user, updateUser, logout } = useAuthStore();
|
const { user, updateUser, logout } = useAuthStore();
|
||||||
@@ -27,7 +28,11 @@ export default function SettingsPage() {
|
|||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
||||||
// Avatar cropping state
|
// 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 [uploadingAvatar, setUploadingAvatar] = useState(false);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
@@ -63,26 +68,30 @@ export default function SettingsPage() {
|
|||||||
if (file) {
|
if (file) {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = () => {
|
reader.onload = () => {
|
||||||
setCropModal({ open: true, image: reader.result as string, file });
|
setCropModal({
|
||||||
|
open: true,
|
||||||
|
image: reader.result as string,
|
||||||
|
file
|
||||||
|
});
|
||||||
};
|
};
|
||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
}
|
}
|
||||||
|
e.target.value = '';
|
||||||
};
|
};
|
||||||
|
|
||||||
const onCropConfirm = async (cropData: Area) => {
|
const onCropConfirm = async (_area: Area, pixelArea: Area) => {
|
||||||
if (!cropModal.file) return;
|
if (!cropModal.file || !cropModal.image) return;
|
||||||
setCropModal(prev => ({ ...prev, open: false }));
|
setCropModal(prev => ({ ...prev, open: false }));
|
||||||
setUploadingAvatar(true);
|
setUploadingAvatar(true);
|
||||||
try {
|
try {
|
||||||
const updatedUser = await UserApi.cropAvatar(cropModal.file, {
|
const croppedBlob = await getCroppedImg(cropModal.image, pixelArea);
|
||||||
x: Math.round(cropData.x),
|
if (!croppedBlob) throw new Error('Failed to crop image');
|
||||||
y: Math.round(cropData.y),
|
|
||||||
width: Math.round(cropData.width),
|
const croppedFile = new File([croppedBlob], 'avatar.jpg', { type: 'image/jpeg' });
|
||||||
height: Math.round(cropData.height)
|
const updatedUser = await UserApi.uploadAvatar(croppedFile);
|
||||||
});
|
|
||||||
updateUser(updatedUser);
|
updateUser(updatedUser);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error('Failed to update avatar:', err);
|
||||||
} finally {
|
} finally {
|
||||||
setUploadingAvatar(false);
|
setUploadingAvatar(false);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user