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
|
<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>
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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}`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user