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:
parent
4e2dd7f77e
commit
2c21daee79
2 changed files with 92 additions and 73 deletions
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue