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
Build and Deploy (Gitea) / build-and-deploy (push) Successful in 2m8s
Details
This commit is contained in:
parent
dd0e587551
commit
ec903c4c9c
|
|
@ -53,6 +53,7 @@ const PreMatricula: React.FC<Props> = ({ data, onConvert }) => {
|
||||||
const [editingCampo, setEditingCampo] = useState<PreMatriculaCampo | null>(null);
|
const [editingCampo, setEditingCampo] = useState<PreMatriculaCampo | null>(null);
|
||||||
const [showNewField, setShowNewField] = useState(false);
|
const [showNewField, setShowNewField] = useState(false);
|
||||||
const [newField, setNewField] = useState({ label: '', tipo: 'text', placeholder: '', obrigatorio: false, opcoes: '' });
|
const [newField, setNewField] = useState({ label: '', tipo: 'text', placeholder: '', obrigatorio: false, opcoes: '' });
|
||||||
|
const [uploadProgress, setUploadProgress] = useState<number | null>(null);
|
||||||
|
|
||||||
const [showMassModal, setShowMassModal] = useState(false);
|
const [showMassModal, setShowMassModal] = useState(false);
|
||||||
const [targetType, setTargetType] = useState('todos');
|
const [targetType, setTargetType] = useState('todos');
|
||||||
|
|
@ -505,22 +506,43 @@ const PreMatricula: React.FC<Props> = ({ data, onConvert }) => {
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-[10px] font-bold text-slate-500 uppercase mb-1">Imagem de Pré-Visualização (WhatsApp/Social)</label>
|
<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">
|
<div className="space-y-3">
|
||||||
{config?.logoUrl ? (
|
{uploadProgress !== null ? (
|
||||||
<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="border border-slate-200 rounded-xl p-5 bg-slate-50 flex flex-col items-center justify-center space-y-3 shadow-inner">
|
||||||
<div className="flex items-center gap-3">
|
<RefreshCw size={24} className="animate-spin text-indigo-600" />
|
||||||
<img src={config.logoUrl} alt="Preview" className="w-12 h-12 object-contain rounded-lg border border-slate-200 bg-white" />
|
<div className="text-center">
|
||||||
<span className="text-[10px] text-slate-400 font-mono truncate max-w-[140px]" title={config.logoUrl}>
|
<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()}
|
{config.logoUrl.split('/').pop()}
|
||||||
</span>
|
</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>
|
</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>
|
||||||
) : (
|
) : (
|
||||||
<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">
|
<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;
|
if (!file) return;
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
try {
|
|
||||||
const res = await fetch('/api/prematricula/upload', {
|
setUploadProgress(0);
|
||||||
method: 'POST',
|
const xhr = new XMLHttpRequest();
|
||||||
body: formData
|
|
||||||
});
|
xhr.upload.addEventListener('progress', (event) => {
|
||||||
if (res.ok) {
|
if (event.lengthComputable) {
|
||||||
const resData = await res.json();
|
const percent = Math.round((event.loaded / event.total) * 100);
|
||||||
setConfig(prev => prev ? { ...prev, logoUrl: resData.url } : prev);
|
setUploadProgress(percent);
|
||||||
showAlert('Sucesso', 'Imagem de visualização carregada!', 'success');
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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 {
|
} else {
|
||||||
showAlert('Erro', 'Falha ao carregar imagem.', 'error');
|
showAlert('Erro', 'Falha ao carregar imagem.', 'error');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
});
|
||||||
|
|
||||||
|
xhr.addEventListener('error', () => {
|
||||||
|
setUploadProgress(null);
|
||||||
showAlert('Erro', 'Erro de conexão ao enviar imagem.', 'error');
|
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">
|
<div className="text-center space-y-1">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue