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

View file

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