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
type="button"
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"
>
<Camera className="w-4 h-4 text-gray-700" />
<Camera className="w-3 h-3 text-gray-700" />
</button>
)}
</div>

View File

@ -20,9 +20,10 @@ import { format } from "date-fns";
import { ptBR } from "date-fns/locale";
import { useNavigate } from "react-router-dom";
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 AgendamentoConsulta from "../components/AgendamentoConsulta";
import { Avatar } from "../components/ui/Avatar";
interface Consulta {
_id: string;
@ -66,6 +67,7 @@ const AcompanhamentoPaciente: React.FC = () => {
const [especialidadeFiltro, setEspecialidadeFiltro] = useState<string>("");
const [laudos, setLaudos] = useState<Report[]>([]);
const [loadingLaudos, setLoadingLaudos] = useState(false);
const [avatarUrl, setAvatarUrl] = useState<string | undefined>(undefined);
const pacienteId = user?.id || "";
const pacienteNome = user?.nome || "Paciente";
@ -138,6 +140,22 @@ const AcompanhamentoPaciente: React.FC = () => {
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
const fetchLaudos = useCallback(async () => {
if (!pacienteId) return;
@ -282,14 +300,12 @@ const AcompanhamentoPaciente: React.FC = () => {
{/* Patient Profile */}
<div className="p-6 border-b border-gray-200 dark:border-slate-700">
<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">
{pacienteNome
.split(" ")
.map((n) => n[0])
.join("")
.toUpperCase()
.slice(0, 2)}
</div>
<Avatar
src={avatarUrl}
name={pacienteNome}
size="md"
color="blue"
/>
<div>
<p className="font-medium text-gray-900 dark:text-white">
{pacienteNome}

View File

@ -2,7 +2,8 @@
* Serviço de Avatars (Frontend)
*/
import { apiClient } from "../api/client";
import axios from "axios";
import { API_CONFIG } from "../api/config";
import type {
UploadAvatarInput,
UploadAvatarResponse,
@ -11,58 +12,75 @@ import type {
} from "./types";
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
*/
async upload(data: UploadAvatarInput): Promise<UploadAvatarResponse> {
try {
// Converte arquivo para base64
const fileData = await this.fileToBase64(data.file);
const token = localStorage.getItem(API_CONFIG.STORAGE_KEYS.ACCESS_TOKEN);
// 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>(
`/avatars-upload?userId=${data.userId}`,
{
fileData,
contentType: data.file.type,
fileName: data.file.name,
},
// Cria FormData para o upload
const formData = new FormData();
formData.append("file", data.file);
// Upload usando Supabase Storage API
await axios.post(
`${this.STORAGE_URL}/${path}`,
formData,
{
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) {
console.error("Erro ao fazer upload do avatar:", 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
*/
async delete(data: DeleteAvatarInput): Promise<void> {
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) {
console.error("Erro ao deletar avatar:", error);
throw error;
@ -74,8 +92,7 @@ class AvatarService {
* Não precisa de autenticação pois é endpoint público
*/
getPublicUrl(data: GetAvatarUrlInput): string {
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
return `${SUPABASE_URL}/storage/v1/object/public/avatars/${data.userId}/avatar.${data.ext}`;
return `${this.STORAGE_URL}/${data.userId}/avatar.${data.ext}`;
}
}