feat(payers): upload de avatar via arquivo com redimensionamento client-side

- círculo de upload no final da grade de avatares abre seletor de arquivo
- imagem redimensionada para 200×200px via Canvas e salva como base64
- suporte a data URLs em next/image com prop unoptimized
- object-cover adicionado ao componente base Avatar

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Felipe Coutinho
2026-04-11 22:43:28 +00:00
parent fa41c78a39
commit 9b8ac9f71f
7 changed files with 138 additions and 18 deletions

View File

@@ -17,11 +17,6 @@ import { version } from "@/package.json";
import { FeedbackDialogBody } from "@/shared/components/navigation/navbar/feedback-dialog";
import { Badge } from "@/shared/components/ui/badge";
import { Dialog, DialogTrigger } from "@/shared/components/ui/dialog";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/shared/components/ui/tooltip";
import {
DropdownMenu,
DropdownMenuContent,
@@ -30,6 +25,11 @@ import {
DropdownMenuTrigger,
} from "@/shared/components/ui/dropdown-menu";
import { Spinner } from "@/shared/components/ui/spinner";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/shared/components/ui/tooltip";
import { authClient } from "@/shared/lib/auth/client";
import { getAvatarSrc } from "@/shared/lib/payers/utils";
import type { UpdateCheckResult } from "@/shared/lib/version/check-update";
@@ -68,6 +68,7 @@ export function NavbarUser({
const avatarSrc = pagadorAvatarUrl
? getAvatarSrc(pagadorAvatarUrl)
: user.image || getAvatarSrc(null);
const isDataUrl = avatarSrc.startsWith("data:");
async function handleLogout() {
await authClient.signOut({
@@ -91,6 +92,7 @@ export function NavbarUser({
<div className="relative size-10 overflow-hidden rounded-full">
<Image
src={avatarSrc}
unoptimized={isDataUrl}
alt={`Avatar de ${user.name}`}
fill
sizes="40px"
@@ -112,6 +114,7 @@ export function NavbarUser({
<div className="relative size-9 shrink-0 overflow-hidden rounded-full">
<Image
src={avatarSrc}
unoptimized={isDataUrl}
alt={user.name}
fill
sizes="36px"
@@ -120,7 +123,9 @@ export function NavbarUser({
</div>
<div className="flex flex-col min-w-0">
<div className="flex items-center gap-1 min-w-0">
<span className="text-sm font-medium truncate">{user.name}</span>
<span className="text-sm font-medium truncate">
{user.name}
</span>
<Tooltip>
<TooltipTrigger asChild>
<button

View File

@@ -22,6 +22,7 @@ export function NavUser({ user, pagadorAvatarUrl }: NavUserProps) {
const avatarSrc = pagadorAvatarUrl
? getAvatarSrc(pagadorAvatarUrl)
: user.image || getAvatarSrc(null);
const isDataUrl = avatarSrc.startsWith("data:");
return (
<SidebarMenu>
@@ -33,6 +34,7 @@ export function NavUser({ user, pagadorAvatarUrl }: NavUserProps) {
<div className="relative size-8 shrink-0 overflow-hidden rounded-full">
<Image
src={avatarSrc}
unoptimized={isDataUrl}
alt={user.name}
fill
sizes="32px"

View File

@@ -28,7 +28,7 @@ function AvatarImage({
return (
<AvatarPrimitive.Image
data-slot="avatar-image"
className={cn("aspect-square size-full", className)}
className={cn("aspect-square size-full object-cover", className)}
{...props}
/>
);