From 2c21daee790da91a24b773c24f79845aa04c4810 Mon Sep 17 00:00:00 2001 From: rever-tecnologia Date: Mon, 15 Dec 2025 11:25:25 -0300 Subject: [PATCH] =?UTF-8?q?fix(profile):=20corrige=20persist=C3=AAncia=20d?= =?UTF-8?q?o=20avatar=20e=20melhora=20fluxo=20de=20salvamento?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- src/app/api/profile/avatar/route.ts | 4 +- src/components/settings/settings-content.tsx | 161 +++++++++++-------- 2 files changed, 92 insertions(+), 73 deletions(-) diff --git a/src/app/api/profile/avatar/route.ts b/src/app/api/profile/avatar/route.ts index 5404823..d6e980c 100644 --- a/src/app/api/profile/avatar/route.ts +++ b/src/app/api/profile/avatar/route.ts @@ -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({ diff --git a/src/components/settings/settings-content.tsx b/src/components/settings/settings-content.tsx index f78e502..9f748a5 100644 --- a/src/components/settings/settings-content.tsx +++ b/src/components/settings/settings-content.tsx @@ -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(null) + const [pendingAvatarPreview, setPendingAvatarPreview] = useState(null) + const [pendingRemoveAvatar, setPendingRemoveAvatar] = useState(false) const fileInputRef = useRef(null) - async function handleAvatarUpload(event: React.ChangeEvent) { + // 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) { 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 = "" - } + // Limpa o input para permitir reselecionar o mesmo arquivo + if (fileInputRef.current) { + fileInputRef.current.value = "" } } - 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) + function handleRemoveAvatarClick() { + // Limpa preview pendente se houver + if (pendingAvatarPreview) { + URL.revokeObjectURL(pendingAvatarPreview) + } + 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) { event.preventDefault() @@ -442,27 +419,69 @@ function ProfileEditCard({ setIsSubmitting(true) try { - 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 + // 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) } - const data = (await res.json().catch(() => null)) as { email?: string } | null - if (data?.email) { - setEditEmail(data.email) + + // 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" }, + 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("") 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 ( {/* Header com shader animado */} -
+
- + {initials} @@ -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" />
- {isUploadingAvatar ? ( + {isSubmitting ? ( ) : (
@@ -503,11 +522,11 @@ function ProfileEditCard({ > - {localAvatarUrl && ( + {(displayAvatarUrl || pendingAvatarFile) && !pendingRemoveAvatar && (