fix(ui): ThumbImg com fallback automático para thumbnails ausentes (404)
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:
VPS 4 Deploy Agent
2026-05-31 07:38:22 +02:00
parent 40ec9b94eb
commit 83ea87911b
3 changed files with 59 additions and 9 deletions
@@ -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>