feat(pre-matricula): add real-time upload progress bar and enlarged high-quality preview layout
Build and Deploy (Gitea) / build-and-deploy (push) Successful in 2m8s Details

This commit is contained in:
Sidney 2026-07-18 21:57:12 -03:00
parent dd0e587551
commit ec903c4c9c
1 changed files with 64 additions and 24 deletions

View File

@ -53,6 +53,7 @@ const PreMatricula: React.FC<Props> = ({ data, onConvert }) => {
const [editingCampo, setEditingCampo] = useState<PreMatriculaCampo | null>(null);
const [showNewField, setShowNewField] = useState(false);
const [newField, setNewField] = useState({ label: '', tipo: 'text', placeholder: '', obrigatorio: false, opcoes: '' });
const [uploadProgress, setUploadProgress] = useState<number | null>(null);
const [showMassModal, setShowMassModal] = useState(false);
const [targetType, setTargetType] = useState('todos');
@ -505,22 +506,43 @@ const PreMatricula: React.FC<Props> = ({ data, onConvert }) => {
<div>
<label className="block text-[10px] font-bold text-slate-500 uppercase mb-1">Imagem de Pré-Visualização (WhatsApp/Social)</label>
<div className="space-y-3">
{config?.logoUrl ? (
<div className="relative group border border-slate-200 rounded-xl overflow-hidden bg-slate-50 p-2 flex items-center justify-between shadow-sm">
<div className="flex items-center gap-3">
<img src={config.logoUrl} alt="Preview" className="w-12 h-12 object-contain rounded-lg border border-slate-200 bg-white" />
<span className="text-[10px] text-slate-400 font-mono truncate max-w-[140px]" title={config.logoUrl}>
{uploadProgress !== null ? (
<div className="border border-slate-200 rounded-xl p-5 bg-slate-50 flex flex-col items-center justify-center space-y-3 shadow-inner">
<RefreshCw size={24} className="animate-spin text-indigo-600" />
<div className="text-center">
<p className="text-xs font-bold text-slate-700">Enviando e compactando imagem...</p>
<p className="text-[10px] text-slate-400 font-bold mt-0.5">{uploadProgress}% concluído</p>
</div>
<div className="w-full bg-slate-200 rounded-full h-2 overflow-hidden shadow-inner">
<div
className="bg-indigo-600 h-full rounded-full transition-all duration-300 ease-out"
style={{ width: `${uploadProgress}%` }}
></div>
</div>
</div>
) : config?.logoUrl ? (
<div className="relative group border border-slate-200 rounded-xl overflow-hidden bg-slate-100 p-3 shadow-sm flex flex-col gap-3">
<div className="w-full h-44 bg-white border border-slate-200 rounded-lg overflow-hidden flex items-center justify-center relative">
<img src={config.logoUrl} alt="Preview" className="w-full h-full object-contain" />
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
<span className="text-white text-xs font-bold bg-black/60 px-3 py-1.5 rounded-lg backdrop-blur-sm">
Pré-visualização do Link
</span>
</div>
</div>
<div className="flex items-center justify-between">
<span className="text-[10px] text-slate-400 font-mono truncate max-w-[200px] font-bold" title={config.logoUrl}>
{config.logoUrl.split('/').pop()}
</span>
<button
type="button"
onClick={() => setConfig(prev => prev ? { ...prev, logoUrl: '' } : prev)}
className="px-3 py-2 bg-red-50 hover:bg-red-100 text-red-600 rounded-lg text-xs font-bold flex items-center gap-1.5 transition-all active:scale-95 shadow-sm"
title="Remover Imagem"
>
<Trash2 size={14} /> Remover
</button>
</div>
<button
type="button"
onClick={() => setConfig(prev => prev ? { ...prev, logoUrl: '' } : prev)}
className="p-2 bg-red-50 hover:bg-red-100 text-red-600 rounded-lg transition-colors"
title="Remover Imagem"
>
<Trash2 size={16} />
</button>
</div>
) : (
<div className="flex items-center justify-center border-2 border-dashed border-slate-200 hover:border-indigo-400 rounded-xl p-5 bg-slate-50 hover:bg-indigo-50/20 transition-all cursor-pointer relative">
@ -533,21 +555,39 @@ const PreMatricula: React.FC<Props> = ({ data, onConvert }) => {
if (!file) return;
const formData = new FormData();
formData.append('file', file);
try {
const res = await fetch('/api/prematricula/upload', {
method: 'POST',
body: formData
});
if (res.ok) {
const resData = await res.json();
setConfig(prev => prev ? { ...prev, logoUrl: resData.url } : prev);
showAlert('Sucesso', 'Imagem de visualização carregada!', 'success');
setUploadProgress(0);
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', (event) => {
if (event.lengthComputable) {
const percent = Math.round((event.loaded / event.total) * 100);
setUploadProgress(percent);
}
});
xhr.addEventListener('load', () => {
setUploadProgress(null);
if (xhr.status >= 200 && xhr.status < 300) {
try {
const resData = JSON.parse(xhr.responseText);
setConfig(prev => prev ? { ...prev, logoUrl: resData.url } : prev);
showAlert('Sucesso', 'Imagem de visualização carregada!', 'success');
} catch (err) {
showAlert('Erro', 'Falha ao processar resposta do servidor.', 'error');
}
} else {
showAlert('Erro', 'Falha ao carregar imagem.', 'error');
}
} catch (err) {
});
xhr.addEventListener('error', () => {
setUploadProgress(null);
showAlert('Erro', 'Erro de conexão ao enviar imagem.', 'error');
}
});
xhr.open('POST', '/api/prematricula/upload');
xhr.send(formData);
}}
/>
<div className="text-center space-y-1">