feat: atualizar API de avatar para Supabase Storage + adicionar avatar no painel do paciente

This commit is contained in:
guisilvagomes 2025-10-28 11:01:15 -03:00
parent 0e27dbf1ff
commit 7ef8715f63
3 changed files with 76 additions and 43 deletions

View File

@ -154,10 +154,10 @@ export function AvatarUpload({
<button <button
type="button" type="button"
onClick={() => setShowMenu(!showMenu)} onClick={() => setShowMenu(!showMenu)}
className="absolute bottom-0 right-0 bg-white rounded-full p-2 shadow-lg hover:bg-gray-100 transition-colors border-2 border-white" className="absolute bottom-0 right-0 bg-white rounded-full p-1.5 shadow-lg hover:bg-gray-100 transition-colors border-2 border-white"
title="Editar avatar" title="Editar avatar"
> >
<Camera className="w-4 h-4 text-gray-700" /> <Camera className="w-3 h-3 text-gray-700" />
</button> </button>
)} )}
</div> </div>

View File

@ -20,9 +20,10 @@ import { format } from "date-fns";
import { ptBR } from "date-fns/locale"; import { ptBR } from "date-fns/locale";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useAuth } from "../hooks/useAuth"; import { useAuth } from "../hooks/useAuth";
import { appointmentService, doctorService, reportService } from "../services"; import { appointmentService, doctorService, reportService, profileService } from "../services";
import type { Report } from "../services/reports/types"; import type { Report } from "../services/reports/types";
import AgendamentoConsulta from "../components/AgendamentoConsulta"; import AgendamentoConsulta from "../components/AgendamentoConsulta";
import { Avatar } from "../components/ui/Avatar";
interface Consulta { interface Consulta {
_id: string; _id: string;
@ -66,6 +67,7 @@ const AcompanhamentoPaciente: React.FC = () => {
const [especialidadeFiltro, setEspecialidadeFiltro] = useState<string>(""); const [especialidadeFiltro, setEspecialidadeFiltro] = useState<string>("");
const [laudos, setLaudos] = useState<Report[]>([]); const [laudos, setLaudos] = useState<Report[]>([]);
const [loadingLaudos, setLoadingLaudos] = useState(false); const [loadingLaudos, setLoadingLaudos] = useState(false);
const [avatarUrl, setAvatarUrl] = useState<string | undefined>(undefined);
const pacienteId = user?.id || ""; const pacienteId = user?.id || "";
const pacienteNome = user?.nome || "Paciente"; const pacienteNome = user?.nome || "Paciente";
@ -138,6 +140,22 @@ const AcompanhamentoPaciente: React.FC = () => {
fetchConsultas(); fetchConsultas();
}, [fetchConsultas]); }, [fetchConsultas]);
// Carregar avatar do perfil
useEffect(() => {
const loadAvatar = async () => {
if (!pacienteId) return;
try {
const profile = await profileService.getById(pacienteId);
if (profile?.avatar_url) {
setAvatarUrl(profile.avatar_url);
}
} catch {
console.log("Perfil não encontrado, usando avatar padrão");
}
};
loadAvatar();
}, [pacienteId]);
// Recarregar consultas quando mudar para a aba de consultas // Recarregar consultas quando mudar para a aba de consultas
const fetchLaudos = useCallback(async () => { const fetchLaudos = useCallback(async () => {
if (!pacienteId) return; if (!pacienteId) return;
@ -282,14 +300,12 @@ const AcompanhamentoPaciente: React.FC = () => {
{/* Patient Profile */} {/* Patient Profile */}
<div className="p-6 border-b border-gray-200 dark:border-slate-700"> <div className="p-6 border-b border-gray-200 dark:border-slate-700">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="h-12 w-12 rounded-full bg-gradient-to-br from-blue-700 to-blue-400 flex items-center justify-center text-white font-semibold text-lg"> <Avatar
{pacienteNome src={avatarUrl}
.split(" ") name={pacienteNome}
.map((n) => n[0]) size="md"
.join("") color="blue"
.toUpperCase() />
.slice(0, 2)}
</div>
<div> <div>
<p className="font-medium text-gray-900 dark:text-white"> <p className="font-medium text-gray-900 dark:text-white">
{pacienteNome} {pacienteNome}

View File

@ -2,7 +2,8 @@
* Serviço de Avatars (Frontend) * Serviço de Avatars (Frontend)
*/ */
import { apiClient } from "../api/client"; import axios from "axios";
import { API_CONFIG } from "../api/config";
import type { import type {
UploadAvatarInput, UploadAvatarInput,
UploadAvatarResponse, UploadAvatarResponse,
@ -11,58 +12,75 @@ import type {
} from "./types"; } from "./types";
class AvatarService { class AvatarService {
private readonly SUPABASE_URL = API_CONFIG.SUPABASE_URL;
private readonly STORAGE_URL = `${this.SUPABASE_URL}/storage/v1/object/avatars`;
/** /**
* Faz upload de avatar do usuário * Faz upload de avatar do usuário
*/ */
async upload(data: UploadAvatarInput): Promise<UploadAvatarResponse> { async upload(data: UploadAvatarInput): Promise<UploadAvatarResponse> {
try { try {
// Converte arquivo para base64 const token = localStorage.getItem(API_CONFIG.STORAGE_KEYS.ACCESS_TOKEN);
const fileData = await this.fileToBase64(data.file);
// Determina a extensão do arquivo
const ext = data.file.name.split(".").pop()?.toLowerCase() || "jpg";
const path = `${data.userId}/avatar.${ext}`;
const response = await apiClient.post<UploadAvatarResponse>( // Cria FormData para o upload
`/avatars-upload?userId=${data.userId}`, const formData = new FormData();
{ formData.append("file", data.file);
fileData,
contentType: data.file.type, // Upload usando Supabase Storage API
fileName: data.file.name, await axios.post(
}, `${this.STORAGE_URL}/${path}`,
formData,
{ {
headers: { headers: {
"Content-Type": "application/json", "Authorization": `Bearer ${token}`,
"Content-Type": "multipart/form-data",
}, },
} }
); );
return response.data; // Retorna a URL pública
const publicUrl = this.getPublicUrl({
userId: data.userId,
ext: ext as "jpg" | "png" | "webp",
});
return {
Key: publicUrl,
};
} catch (error) { } catch (error) {
console.error("Erro ao fazer upload do avatar:", error); console.error("Erro ao fazer upload do avatar:", error);
throw error; throw error;
} }
} }
/**
* Converte File para base64
*/
private fileToBase64(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
const result = reader.result as string;
// Remove o prefixo "data:image/...;base64,"
const base64 = result.split(",")[1];
resolve(base64);
};
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
/** /**
* Remove avatar do usuário * Remove avatar do usuário
*/ */
async delete(data: DeleteAvatarInput): Promise<void> { async delete(data: DeleteAvatarInput): Promise<void> {
try { try {
await apiClient.delete(`/avatars-delete?userId=${data.userId}`); const token = localStorage.getItem(API_CONFIG.STORAGE_KEYS.ACCESS_TOKEN);
// Tenta deletar todas as extensões possíveis
const extensions = ["jpg", "png", "webp"];
for (const ext of extensions) {
try {
await axios.delete(
`${this.STORAGE_URL}/${data.userId}/avatar.${ext}`,
{
headers: {
"Authorization": `Bearer ${token}`,
},
}
);
} catch {
// Ignora erros se o arquivo não existir
}
}
} catch (error) { } catch (error) {
console.error("Erro ao deletar avatar:", error); console.error("Erro ao deletar avatar:", error);
throw error; throw error;
@ -74,8 +92,7 @@ class AvatarService {
* Não precisa de autenticação pois é endpoint público * Não precisa de autenticação pois é endpoint público
*/ */
getPublicUrl(data: GetAvatarUrlInput): string { getPublicUrl(data: GetAvatarUrlInput): string {
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co"; return `${this.STORAGE_URL}/${data.userId}/avatar.${data.ext}`;
return `${SUPABASE_URL}/storage/v1/object/public/avatars/${data.userId}/avatar.${data.ext}`;
} }
} }