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 [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">
|
||||
|
|
|
|||
Loading…
Reference in New Issue