Files
IdP/apps/frontend/components/id/avatar-upload.tsx
2026-06-24 14:37:15 +03:00

128 lines
4.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { useRef, useState } from 'react';
import { Camera, Loader2 } from 'lucide-react';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Button } from '@/components/ui/button';
import { useToast } from '@/components/id/toast-provider';
import { useAvatarUrl } from '@/hooks/use-avatar-url';
import { apiFetch, getApiErrorMessage } from '@/lib/api';
interface PresignedUploadResponse {
uploadUrl: string;
storageKey: string;
expiresAt: string;
}
export function AvatarUpload({
userId,
displayName,
hasAvatar,
token,
onUpdated
}: {
userId: string;
displayName?: string;
hasAvatar?: boolean;
token: string | null;
onUpdated: () => Promise<void>;
}) {
const inputRef = useRef<HTMLInputElement>(null);
const { showToast } = useToast();
const [isUploading, setIsUploading] = useState(false);
const { avatarUrl, refreshAvatarUrl } = useAvatarUrl(userId, hasAvatar, token);
const initials = (displayName ?? '').slice(0, 2).toUpperCase() || 'ID';
async function handleFileChange(event: React.ChangeEvent<HTMLInputElement>) {
const file = event.target.files?.[0];
if (!file || !token) return;
if (!['image/jpeg', 'image/png', 'image/webp', 'image/gif'].includes(file.type)) {
showToast('Допустимы только JPEG, PNG, WEBP или GIF');
return;
}
setIsUploading(true);
try {
const presigned = await apiFetch<PresignedUploadResponse>(
'/media/avatars/upload-url',
{
method: 'POST',
body: JSON.stringify({ contentType: file.type })
},
token
);
const uploadResponse = await fetch(presigned.uploadUrl, {
method: 'PUT',
headers: { 'Content-Type': file.type },
body: file
});
if (!uploadResponse.ok) {
throw new Error('Не удалось загрузить файл в хранилище');
}
await apiFetch('/media/avatars/confirm', {
method: 'POST',
body: JSON.stringify({ storageKey: presigned.storageKey })
}, token);
await onUpdated();
await refreshAvatarUrl();
showToast('Аватар обновлён');
} catch (error) {
const message = getApiErrorMessage(error, 'Не удалось загрузить аватар');
if (message) showToast(message);
} finally {
setIsUploading(false);
if (inputRef.current) inputRef.current.value = '';
}
}
return (
<div className="relative inline-flex">
<Avatar className="h-24 w-24 border-4 border-white shadow-xl">
{avatarUrl ? <AvatarImage src={avatarUrl} alt={displayName ?? 'Аватар'} /> : null}
<AvatarFallback className="bg-[linear-gradient(135deg,#6f58ff,#ffd2a8)] text-2xl">{initials}</AvatarFallback>
</Avatar>
<input ref={inputRef} type="file" accept="image/jpeg,image/png,image/webp,image/gif" className="hidden" onChange={handleFileChange} />
<Button
type="button"
size="sm"
variant="secondary"
className="absolute -bottom-2 left-1/2 -translate-x-1/2 rounded-full px-3 shadow"
disabled={isUploading || !token}
onClick={() => inputRef.current?.click()}
>
{isUploading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Camera className="h-4 w-4" />}
{isUploading ? 'Загрузка...' : 'Фото'}
</Button>
</div>
);
}
export function AvatarDisplay({
userId,
displayName,
hasAvatar,
token,
className
}: {
userId: string;
displayName?: string;
hasAvatar?: boolean;
token: string | null;
className?: string;
}) {
const { avatarUrl } = useAvatarUrl(userId, hasAvatar, token);
const initials = (displayName ?? '').slice(0, 2).toUpperCase() || 'ID';
return (
<Avatar className={className}>
{avatarUrl ? <AvatarImage src={avatarUrl} alt={displayName ?? 'Аватар'} /> : null}
<AvatarFallback className="bg-[linear-gradient(135deg,#6f58ff,#ffd2a8)]">{initials}</AvatarFallback>
</Avatar>
);
}