feat: atualizar API de avatar para Supabase Storage + adicionar avatar no painel do paciente
This commit is contained in:
parent
0e27dbf1ff
commit
7ef8715f63
@ -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>
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user