fix and update

This commit is contained in:
lendry
2026-06-25 16:01:41 +03:00
parent ce58e6f4c1
commit 3880c68d59
40 changed files with 1715 additions and 215 deletions

View File

@@ -3,10 +3,11 @@
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, uploadMediaObject } from '@/lib/api';
import { VerificationBadge } from '@/components/id/verification-badge';
import { cn } from '@/lib/utils';
interface PresignedUploadResponse {
uploadUrl: string;
@@ -20,13 +21,21 @@ export function AvatarUpload({
displayName,
hasAvatar,
token,
onUpdated
onUpdated,
isVerified,
verificationIcon,
className = 'h-24 w-24',
badgeSize = 'md'
}: {
userId: string;
displayName?: string;
hasAvatar?: boolean;
token: string | null;
onUpdated: () => Promise<void>;
isVerified?: boolean;
verificationIcon?: string | null;
className?: string;
badgeSize?: 'xs' | 'sm' | 'md' | 'lg';
}) {
const inputRef = useRef<HTMLInputElement>(null);
const { showToast } = useToast();
@@ -73,24 +82,45 @@ export function AvatarUpload({
}
}
function openFilePicker() {
if (isUploading || !token) return;
inputRef.current?.click();
}
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
<div className="relative inline-flex shrink-0">
<button
type="button"
size="sm"
variant="secondary"
className="absolute -bottom-2 left-1/2 -translate-x-1/2 rounded-full px-3 shadow"
className={cn('group relative rounded-full focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#3390ec] focus-visible:ring-offset-2', className)}
disabled={isUploading || !token}
onClick={() => inputRef.current?.click()}
aria-label="Изменить фото профиля"
onClick={openFilePicker}
>
{isUploading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Camera className="h-4 w-4" />}
{isUploading ? 'Загрузка...' : 'Фото'}
</Button>
<Avatar className={cn('border-4 border-white shadow-xl', className)}>
{avatarUrl ? <AvatarImage src={avatarUrl} alt={displayName ?? 'Аватар'} /> : null}
<AvatarFallback className="bg-[linear-gradient(135deg,#6f58ff,#ffd2a8)] text-2xl">{initials}</AvatarFallback>
</Avatar>
<span
className={cn(
'absolute inset-0 flex items-center justify-center rounded-full transition',
isUploading ? 'bg-black/45 opacity-100' : 'bg-black/0 opacity-0 group-hover:bg-black/40 group-hover:opacity-100 group-focus-visible:bg-black/40 group-focus-visible:opacity-100'
)}
>
{isUploading ? (
<Loader2 className="h-5 w-5 animate-spin text-white" />
) : (
<Camera className="h-5 w-5 text-white" />
)}
</span>
</button>
{isVerified ? (
<VerificationBadge
verificationIcon={verificationIcon}
size={badgeSize}
className="pointer-events-none absolute top-0 right-0 z-20"
/>
) : null}
<input ref={inputRef} type="file" accept="image/jpeg,image/png,image/webp,image/gif" className="hidden" onChange={handleFileChange} />
</div>
);
}
@@ -100,21 +130,36 @@ export function AvatarDisplay({
displayName,
hasAvatar,
token,
className
className,
isVerified,
verificationIcon,
badgeSize = 'sm'
}: {
userId: string;
displayName?: string;
hasAvatar?: boolean;
token: string | null;
className?: string;
isVerified?: boolean;
verificationIcon?: string | null;
badgeSize?: 'xs' | 'sm' | 'md' | 'lg';
}) {
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>
<div className="relative inline-flex shrink-0">
<Avatar className={className}>
{avatarUrl ? <AvatarImage src={avatarUrl} alt={displayName ?? 'Аватар'} /> : null}
<AvatarFallback className="bg-[linear-gradient(135deg,#6f58ff,#ffd2a8)]">{initials}</AvatarFallback>
</Avatar>
{isVerified ? (
<VerificationBadge
verificationIcon={verificationIcon}
size={badgeSize}
className="absolute -top-0.5 -right-0.5 z-20"
/>
) : null}
</div>
);
}