fix(ui): ThumbImg com fallback automático para thumbnails ausentes (404)
continuous-integration/webhook Deploy concluído (VPS4)
continuous-integration/webhook Deploy concluído (VPS4)
Cria ThumbImg.jsx: wrapper de <img> que intercepta onError e exibe placeholder 🦷 + label "Aguardando reenvio" em vez de imagem quebrada. Aplicado em: - DashboardPage: cards de pacientes e galeria de imagens do paciente - PatientsPage: miniatura na coluna Nome da tabela Enquanto o Plan B (reupload-files) não tiver sido concluído pelo cliente Windows, o card exibe o placeholder informativo em vez de ícone de erro. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
/**
|
||||
* Renderiza um thumbnail com fallback automático quando a imagem falha
|
||||
* (404 ou erro de rede). Enquanto aguarda reenvio pelo cliente Windows
|
||||
* exibe placeholder com ícone e label configuráveis.
|
||||
*/
|
||||
export default function ThumbImg({ src, className, style, placeholder = '🦷', label = 'Aguardando reenvio' }) {
|
||||
const [failed, setFailed] = useState(false);
|
||||
|
||||
if (!src || failed) {
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
style={{
|
||||
...style,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 4,
|
||||
background: 'var(--border, #e8edf5)',
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: '1.8rem', lineHeight: 1 }}>{placeholder}</span>
|
||||
{label && (
|
||||
<span style={{ fontSize: '0.6rem', color: 'var(--text-secondary, #888)', textAlign: 'center', padding: '0 6px', lineHeight: 1.3 }}>
|
||||
{label}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<img
|
||||
className={className}
|
||||
src={src}
|
||||
alt=""
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
style={style}
|
||||
onError={() => setFailed(true)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { useSocket } from '../contexts/SocketContext';
|
||||
import { useToast } from '../contexts/ToastContext';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import TransformModal from '../components/TransformModal';
|
||||
import ThumbImg from '../components/ThumbImg';
|
||||
import CreatePatientModal from '../components/CreatePatientModal';
|
||||
import SettingsModal from '../components/SettingsModal';
|
||||
import UserManagementModal from '../components/UserManagementModal';
|
||||
@@ -336,10 +337,11 @@ export default function DashboardPage() {
|
||||
return (
|
||||
<div key={`${fullName}-${idx}`} className="image-card patient-card" onClick={() => openPatient(p)}>
|
||||
<div className="image-preview">
|
||||
{thumb
|
||||
? <img className="preview-img" src={thumb} alt="" loading="lazy" decoding="async" style={{ background: '#e0e7ff' }} />
|
||||
: <div className="preview-img-placeholder">🦷</div>
|
||||
}
|
||||
<ThumbImg
|
||||
className="preview-img"
|
||||
src={thumb}
|
||||
style={{ background: '#e0e7ff' }}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="delete-patient-btn"
|
||||
@@ -387,7 +389,7 @@ export default function DashboardPage() {
|
||||
data-id={image.id}
|
||||
>
|
||||
<div className="image-preview">
|
||||
<img className="preview-img" src={thumbUrl} alt="" loading="lazy" decoding="async" />
|
||||
<ThumbImg className="preview-img" src={thumbUrl} label="Aguardando reenvio" />
|
||||
</div>
|
||||
<div className="image-info">
|
||||
<div className="image-meta"><span>📅 {formatDate(image.created_at)}</span></div>
|
||||
|
||||
@@ -3,6 +3,7 @@ import api from '../api/client';
|
||||
import { useToast } from '../contexts/ToastContext';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import Sidebar from '../components/Sidebar';
|
||||
import ThumbImg from '../components/ThumbImg';
|
||||
import CreatePatientModal from '../components/CreatePatientModal';
|
||||
import EditPatientModal from '../components/EditPatientModal';
|
||||
import SettingsModal from '../components/SettingsModal';
|
||||
@@ -179,10 +180,11 @@ export default function PatientsPage() {
|
||||
>
|
||||
<td style={{ padding: '12px 16px', fontWeight: 600, maxWidth: 200 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
{p.thumb_url
|
||||
? <img src={p.thumb_url} alt="" style={{ width: 36, height: 36, borderRadius: 6, objectFit: 'cover', flexShrink: 0, background: '#e0e7ff' }} loading="lazy" />
|
||||
: <div style={{ width: 36, height: 36, borderRadius: 6, background: 'var(--border)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 18, flexShrink: 0 }}>🦷</div>
|
||||
}
|
||||
<ThumbImg
|
||||
src={p.thumb_url}
|
||||
style={{ width: 36, height: 36, borderRadius: 6, objectFit: 'cover', flexShrink: 0, background: '#e0e7ff' }}
|
||||
label=""
|
||||
/>
|
||||
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }} title={p.patient_name}>
|
||||
{p.patient_name || 'Sem nome'}
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user