Compare commits

...

3 Commits

Author SHA1 Message Date
João Gustavo
2c39f404d8 add-upload 2025-10-23 14:23:45 -03:00
João Gustavo
6a8a4af756 Merge branch 'feature/up-dow-avatar' into backup/fix-patient-page 2025-10-23 14:23:33 -03:00
d21ed34715 feat: upload/download de avatar e correções 2025-10-23 14:07:32 -03:00
3 changed files with 168 additions and 22 deletions

View File

@ -1,6 +1,6 @@
'use client' 'use client'
// import { useAuth } from '@/hooks/useAuth' // removido duplicado
import type { ReactNode } from 'react'
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
@ -12,6 +12,7 @@ import { Textarea } from '@/components/ui/textarea'
import { Avatar, AvatarFallback } from '@/components/ui/avatar' import { Avatar, AvatarFallback } from '@/components/ui/avatar'
import { User, LogOut, Calendar, FileText, MessageCircle, UserCog, Home, Clock, FolderOpen, ChevronLeft, ChevronRight, MapPin, Stethoscope } from 'lucide-react' import { User, LogOut, Calendar, FileText, MessageCircle, UserCog, Home, Clock, FolderOpen, ChevronLeft, ChevronRight, MapPin, Stethoscope } from 'lucide-react'
import { SimpleThemeToggle } from '@/components/simple-theme-toggle' import { SimpleThemeToggle } from '@/components/simple-theme-toggle'
import { UploadAvatar } from '@/components/ui/upload-avatar'
import Link from 'next/link' import Link from 'next/link'
import ProtectedRoute from '@/components/ProtectedRoute' import ProtectedRoute from '@/components/ProtectedRoute'
import { useAuth } from '@/hooks/useAuth' import { useAuth } from '@/hooks/useAuth'
@ -73,7 +74,7 @@ export default function PacientePage() {
// Estado para edição do perfil // Estado para edição do perfil
const [isEditingProfile, setIsEditingProfile] = useState(false) const [isEditingProfile, setIsEditingProfile] = useState(false)
const [profileData, setProfileData] = useState({ const [profileData, setProfileData] = useState<any>({
nome: '', nome: '',
email: user?.email || '', email: user?.email || '',
telefone: '', telefone: '',
@ -81,6 +82,8 @@ export default function PacientePage() {
cidade: '', cidade: '',
cep: '', cep: '',
biografia: '', biografia: '',
id: undefined,
foto_url: undefined,
}) })
const [patientId, setPatientId] = useState<string | null>(null) const [patientId, setPatientId] = useState<string | null>(null)
@ -216,7 +219,7 @@ export default function PacientePage() {
if (process.env.NODE_ENV !== 'production') console.debug('[PacientePage] paciente row', paciente) if (process.env.NODE_ENV !== 'production') console.debug('[PacientePage] paciente row', paciente)
setProfileData(prev => ({ ...prev, nome, email: emailFromRow, telefone, endereco, cidade, cep, biografia })) setProfileData((prev: any) => ({ ...prev, nome, email: emailFromRow, telefone, endereco, cidade, cep, biografia }))
} }
} catch (err) { } catch (err) {
console.warn('[PacientePage] erro ao carregar paciente', err) console.warn('[PacientePage] erro ao carregar paciente', err)
@ -231,7 +234,7 @@ export default function PacientePage() {
}, [user?.id, user?.email]) }, [user?.id, user?.email])
const handleProfileChange = (field: string, value: string) => { const handleProfileChange = (field: string, value: string) => {
setProfileData(prev => ({ ...prev, [field]: value })) setProfileData((prev: any) => ({ ...prev, [field]: value }))
} }
const handleSaveProfile = () => { const handleSaveProfile = () => {
setIsEditingProfile(false) setIsEditingProfile(false)
@ -743,19 +746,12 @@ export default function PacientePage() {
{/* Foto do Perfil */} {/* Foto do Perfil */}
<div className="border-t border-border pt-6"> <div className="border-t border-border pt-6">
<h3 className="text-lg font-semibold mb-4 text-foreground">Foto do Perfil</h3> <h3 className="text-lg font-semibold mb-4 text-foreground">Foto do Perfil</h3>
<div className="flex items-center gap-4"> <UploadAvatar
<Avatar className="h-20 w-20"> userId={profileData.id}
<AvatarFallback className="text-lg"> currentAvatarUrl={profileData.foto_url}
{profileData.nome.split(' ').map(n => n[0]).join('').toUpperCase()} onAvatarChange={(newUrl) => handleProfileChange('foto_url', newUrl)}
</AvatarFallback> userName={profileData.nome}
</Avatar> />
{isEditingProfile && (
<div className="space-y-2">
<Button variant="outline" size="sm">Alterar Foto</Button>
<p className="text-xs text-muted-foreground">Formatos aceitos: JPG, PNG (máx. 2MB)</p>
</div>
)}
</div>
</div> </div>
</div> </div>
) )

View File

@ -0,0 +1,132 @@
"use client"
import React, { useState } from 'react'
import { Button } from './button'
import { Input } from './input'
import { Avatar, AvatarFallback, AvatarImage } from './avatar'
import { Upload, Download } from 'lucide-react'
import { uploadFotoPaciente } from '@/lib/api'
interface UploadAvatarProps {
userId: string
currentAvatarUrl?: string
onAvatarChange?: (newUrl: string) => void
userName?: string
className?: string
}
export function UploadAvatar({ userId, currentAvatarUrl, onAvatarChange, userName }: UploadAvatarProps) {
const [isUploading, setIsUploading] = useState<boolean>(false)
const [error, setError] = useState<string>('')
const handleUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]
if (!file) return
try {
setIsUploading(true)
setError('')
console.debug('[UploadAvatar] Iniciando upload:', {
fileName: file.name,
fileType: file.type,
fileSize: file.size,
userId
})
const result = await uploadFotoPaciente(userId, file)
if (result.foto_url) {
console.debug('[UploadAvatar] Upload concluído:', result)
onAvatarChange?.(result.foto_url)
}
} catch (err) {
console.error('[UploadAvatar] Erro no upload:', err)
setError(err instanceof Error ? err.message : 'Erro ao fazer upload do avatar')
} finally {
setIsUploading(false)
// Limpa o input para permitir selecionar o mesmo arquivo novamente
event.target.value = ''
}
}
const handleDownload = async () => {
if (!currentAvatarUrl) return
try {
const response = await fetch(currentAvatarUrl)
const blob = await response.blob()
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `avatar-${userId}.${blob.type.split('/')[1] || 'jpg'}`
document.body.appendChild(a)
a.click()
window.URL.revokeObjectURL(url)
document.body.removeChild(a)
} catch (err) {
setError('Erro ao baixar o avatar')
}
}
const initials = userName
? userName.split(' ').map(n => n[0]).join('').toUpperCase()
: 'U'
return (
<div className="space-y-4">
<div className="flex items-center gap-4">
<Avatar className="h-20 w-20">
<AvatarImage src={currentAvatarUrl} alt={userName || 'Avatar'} />
<AvatarFallback className="text-lg">
{initials}
</AvatarFallback>
</Avatar>
<div className="space-y-2">
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => document.getElementById('avatar-upload')?.click()}
disabled={isUploading}
>
<Upload className="h-4 w-4 mr-2" />
{isUploading ? 'Enviando...' : 'Upload'}
</Button>
{currentAvatarUrl && (
<Button
variant="outline"
size="sm"
onClick={handleDownload}
>
<Download className="h-4 w-4 mr-2" />
Download
</Button>
)}
</div>
<Input
id="avatar-upload"
type="file"
className="hidden"
accept="image/jpeg,image/png,image/webp"
onChange={handleUpload}
disabled={isUploading}
/>
<p className="text-xs text-muted-foreground">
Formatos aceitos: JPG, PNG, WebP (máx. 2MB)
</p>
{error && (
<p className="text-xs text-destructive">
{error}
</p>
)}
</div>
</div>
</div>
)
}

View File

@ -2676,8 +2676,10 @@ export async function uploadFotoPaciente(_id: string | number, _file: File): Pro
}; };
const ext = extMap[_file.type] || 'jpg'; const ext = extMap[_file.type] || 'jpg';
const objectPath = `avatars/${userId}/avatar.${ext}`; // O bucket deve ser 'avatars' e o caminho do objeto será userId/avatar.ext
const uploadUrl = `${ENV_CONFIG.SUPABASE_URL}/storage/v1/object/avatars/${encodeURIComponent(userId)}/avatar`; const bucket = 'avatars';
const objectPath = `${userId}/avatar.${ext}`;
const uploadUrl = `${ENV_CONFIG.SUPABASE_URL}/storage/v1/object/${bucket}/${encodeURIComponent(objectPath)}`;
// Build multipart form data // Build multipart form data
const form = new FormData(); const form = new FormData();
@ -2693,6 +2695,13 @@ export async function uploadFotoPaciente(_id: string | number, _file: File): Pro
const jwt = getAuthToken(); const jwt = getAuthToken();
if (jwt) headers.Authorization = `Bearer ${jwt}`; if (jwt) headers.Authorization = `Bearer ${jwt}`;
console.debug('[uploadFotoPaciente] Iniciando upload:', {
url: uploadUrl,
fileType: _file.type,
fileSize: _file.size,
hasAuth: !!jwt
});
const res = await fetch(uploadUrl, { const res = await fetch(uploadUrl, {
method: 'POST', method: 'POST',
headers, headers,
@ -2702,10 +2711,19 @@ export async function uploadFotoPaciente(_id: string | number, _file: File): Pro
// Supabase storage returns 200/201 with object info or error // Supabase storage returns 200/201 with object info or error
if (!res.ok) { if (!res.ok) {
const raw = await res.text().catch(() => ''); const raw = await res.text().catch(() => '');
console.error('[uploadFotoPaciente] upload falhou', { status: res.status, raw }); console.error('[uploadFotoPaciente] upload falhou', {
status: res.status,
raw,
headers: Object.fromEntries(res.headers.entries()),
url: uploadUrl,
requestHeaders: headers,
objectPath
});
if (res.status === 401) throw new Error('Não autenticado'); if (res.status === 401) throw new Error('Não autenticado');
if (res.status === 403) throw new Error('Sem permissão para fazer upload'); if (res.status === 403) throw new Error('Sem permissão para fazer upload');
throw new Error('Falha no upload da imagem'); if (res.status === 404) throw new Error('Bucket de avatars não encontrado. Verifique se o bucket "avatars" existe no Supabase');
throw new Error(`Falha no upload da imagem (${res.status}): ${raw || 'Sem detalhes do erro'}`);
} }
// Try to parse JSON response // Try to parse JSON response
@ -2714,7 +2732,7 @@ export async function uploadFotoPaciente(_id: string | number, _file: File): Pro
// The API may not return a structured body; return the Key we constructed // The API may not return a structured body; return the Key we constructed
const key = (json && (json.Key || json.key)) ?? objectPath; const key = (json && (json.Key || json.key)) ?? objectPath;
const publicUrl = `${ENV_CONFIG.SUPABASE_URL}/storage/v1/object/public/${encodeURIComponent('avatars')}/${encodeURIComponent(userId)}/avatar.${ext}`; const publicUrl = `${ENV_CONFIG.SUPABASE_URL}/storage/v1/object/public/avatars/${encodeURIComponent(userId)}/avatar.${ext}`;
return { foto_url: publicUrl, Key: key }; return { foto_url: publicUrl, Key: key };
} }