fix(profile): corrige persistência do avatar e melhora fluxo de salvamento

- Corrige campo de avatar na API (avatarUrl ao invés de image)
- Altera fluxo para salvar foto apenas ao clicar em "Salvar alterações"
- Adiciona preview local antes do upload definitivo
- Ajusta shader para preencher bordas arredondadas do card

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
rever-tecnologia 2025-12-15 11:25:25 -03:00
parent 4e2dd7f77e
commit 2c21daee79
2 changed files with 92 additions and 73 deletions

View file

@ -75,7 +75,7 @@ export async function POST(request: NextRequest) {
// Atualiza o usuário no banco
await prisma.authUser.update({
where: { id: session.user.id },
data: { image: avatarUrl },
data: { avatarUrl },
})
return NextResponse.json({
@ -99,7 +99,7 @@ export async function DELETE() {
// Remove a imagem do usuário (volta ao padrão)
await prisma.authUser.update({
where: { id: session.user.id },
data: { image: null },
data: { avatarUrl: null },
})
return NextResponse.json({

View file

@ -336,10 +336,15 @@ function ProfileEditCard({
const [confirmPassword, setConfirmPassword] = useState("")
const [isSubmitting, setIsSubmitting] = useState(false)
const [localAvatarUrl, setLocalAvatarUrl] = useState(avatarUrl)
const [isUploadingAvatar, setIsUploadingAvatar] = useState(false)
const [pendingAvatarFile, setPendingAvatarFile] = useState<File | null>(null)
const [pendingAvatarPreview, setPendingAvatarPreview] = useState<string | null>(null)
const [pendingRemoveAvatar, setPendingRemoveAvatar] = useState(false)
const fileInputRef = useRef<HTMLInputElement>(null)
async function handleAvatarUpload(event: React.ChangeEvent<HTMLInputElement>) {
// URL de exibição: preview pendente > URL atual (se não marcado para remoção)
const displayAvatarUrl = pendingAvatarPreview ?? (pendingRemoveAvatar ? null : localAvatarUrl)
function handleAvatarSelect(event: React.ChangeEvent<HTMLInputElement>) {
const file = event.target.files?.[0]
if (!file) return
@ -355,57 +360,28 @@ function ProfileEditCard({
return
}
setIsUploadingAvatar(true)
try {
const formData = new FormData()
formData.append("file", file)
// Cria preview local
const previewUrl = URL.createObjectURL(file)
setPendingAvatarFile(file)
setPendingAvatarPreview(previewUrl)
setPendingRemoveAvatar(false)
const res = await fetch("/api/profile/avatar", {
method: "POST",
body: formData,
})
if (!res.ok) {
const data = await res.json().catch(() => ({ error: "Erro ao fazer upload" }))
throw new Error(data.error || "Erro ao fazer upload")
}
const data = await res.json()
setLocalAvatarUrl(data.avatarUrl)
toast.success("Foto atualizada com sucesso!")
} catch (error) {
console.error("Erro ao fazer upload:", error)
toast.error(error instanceof Error ? error.message : "Erro ao fazer upload da foto")
} finally {
setIsUploadingAvatar(false)
// Limpa o input para permitir reselecionar o mesmo arquivo
if (fileInputRef.current) {
fileInputRef.current.value = ""
}
}
function handleRemoveAvatarClick() {
// Limpa preview pendente se houver
if (pendingAvatarPreview) {
URL.revokeObjectURL(pendingAvatarPreview)
}
async function handleRemoveAvatar() {
if (!localAvatarUrl) return
setIsUploadingAvatar(true)
try {
const res = await fetch("/api/profile/avatar", {
method: "DELETE",
})
if (!res.ok) {
const data = await res.json().catch(() => ({ error: "Erro ao remover foto" }))
throw new Error(data.error || "Erro ao remover foto")
}
setLocalAvatarUrl(null)
toast.success("Foto removida com sucesso!")
} catch (error) {
console.error("Erro ao remover foto:", error)
toast.error(error instanceof Error ? error.message : "Erro ao remover foto")
} finally {
setIsUploadingAvatar(false)
setPendingAvatarFile(null)
setPendingAvatarPreview(null)
// Marca para remoção apenas se já tiver avatar salvo
if (localAvatarUrl) {
setPendingRemoveAvatar(true)
}
}
@ -413,8 +389,9 @@ function ProfileEditCard({
const nameChanged = editName.trim() !== name
const emailChanged = editEmail.trim().toLowerCase() !== email.toLowerCase()
const passwordChanged = newPassword.length > 0 || confirmPassword.length > 0
return nameChanged || emailChanged || passwordChanged
}, [editName, name, editEmail, email, newPassword, confirmPassword])
const avatarChanged = pendingAvatarFile !== null || pendingRemoveAvatar
return nameChanged || emailChanged || passwordChanged || avatarChanged
}, [editName, name, editEmail, email, newPassword, confirmPassword, pendingAvatarFile, pendingRemoveAvatar])
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault()
@ -442,6 +419,46 @@ function ProfileEditCard({
setIsSubmitting(true)
try {
// Processa avatar primeiro
if (pendingAvatarFile) {
const formData = new FormData()
formData.append("file", pendingAvatarFile)
const avatarRes = await fetch("/api/profile/avatar", {
method: "POST",
body: formData,
})
if (!avatarRes.ok) {
const data = await avatarRes.json().catch(() => ({ error: "Erro ao fazer upload" }))
throw new Error(data.error || "Erro ao fazer upload da foto")
}
const avatarData = await avatarRes.json()
setLocalAvatarUrl(avatarData.avatarUrl)
// Limpa preview
if (pendingAvatarPreview) {
URL.revokeObjectURL(pendingAvatarPreview)
}
setPendingAvatarFile(null)
setPendingAvatarPreview(null)
} else if (pendingRemoveAvatar) {
const avatarRes = await fetch("/api/profile/avatar", {
method: "DELETE",
})
if (!avatarRes.ok) {
const data = await avatarRes.json().catch(() => ({ error: "Erro ao remover foto" }))
throw new Error(data.error || "Erro ao remover foto")
}
setLocalAvatarUrl(null)
setPendingRemoveAvatar(false)
}
// Processa outros dados do perfil se houver
if (Object.keys(payload).length > 0) {
const res = await fetch("/api/portal/profile", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
@ -457,12 +474,14 @@ function ProfileEditCard({
if (data?.email) {
setEditEmail(data.email)
}
}
setNewPassword("")
setConfirmPassword("")
toast.success("Dados atualizados com sucesso!")
} catch (error) {
console.error("Falha ao atualizar perfil", error)
toast.error("Não foi possível atualizar o perfil.")
toast.error(error instanceof Error ? error.message : "Não foi possível atualizar o perfil.")
} finally {
setIsSubmitting(false)
}
@ -471,14 +490,14 @@ function ProfileEditCard({
return (
<Card className="rounded-2xl border border-border/60 bg-white shadow-sm overflow-hidden">
{/* Header com shader animado */}
<div className="relative h-20 overflow-hidden">
<div className="relative h-20 overflow-hidden rounded-t-2xl">
<ShaderBackground className="absolute inset-0 h-full w-full" />
</div>
<CardHeader className="pb-4 -mt-10">
<div className="flex items-end gap-4">
<div className="relative group">
<Avatar className="size-20 border-4 border-white shadow-lg ring-2 ring-neutral-200">
<AvatarImage src={localAvatarUrl ?? undefined} alt={name} />
<AvatarImage src={displayAvatarUrl ?? undefined} alt={name} />
<AvatarFallback className="bg-neutral-200 text-xl font-semibold text-neutral-700">
{initials}
</AvatarFallback>
@ -487,11 +506,11 @@ function ProfileEditCard({
ref={fileInputRef}
type="file"
accept="image/jpeg,image/png,image/webp,image/gif"
onChange={handleAvatarUpload}
onChange={handleAvatarSelect}
className="hidden"
/>
<div className="absolute inset-0 flex items-center justify-center rounded-full bg-black/50 opacity-0 transition-opacity group-hover:opacity-100">
{isUploadingAvatar ? (
{isSubmitting ? (
<Loader2 className="size-5 text-white animate-spin" />
) : (
<div className="flex items-center gap-2">
@ -503,11 +522,11 @@ function ProfileEditCard({
>
<Camera className="size-4 text-white" />
</button>
{localAvatarUrl && (
{(displayAvatarUrl || pendingAvatarFile) && !pendingRemoveAvatar && (
<button
type="button"
className="flex items-center justify-center size-8 rounded-full bg-red-500/80 hover:bg-red-500 transition-colors"
onClick={handleRemoveAvatar}
onClick={handleRemoveAvatarClick}
title="Remover foto"
>
<Trash2 className="size-4 text-white" />