first commit
This commit is contained in:
commit
c9fb67e751
423 files changed
+53287
No files matched your search
@@ -0,0 +1,313 @@
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useDropzone } from 'react-dropzone';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
export function ClientProvaUpload() {
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [uploadProgress, setUploadProgress] = useState(0);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [userLocation, setUserLocation] = useState<{ latitude: number; longitude: number } | null>(null);
|
||||
const [locationPermission, setLocationPermission] = useState<string>('pending');
|
||||
const [filePreview, setFilePreview] = useState<string | null>(null);
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Tentar obter a localização do usuário quando o componente montar
|
||||
useEffect(() => {
|
||||
if (navigator.geolocation) {
|
||||
setLocationPermission('requesting');
|
||||
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
(position) => {
|
||||
console.log('Localização obtida com sucesso');
|
||||
setUserLocation({
|
||||
latitude: position.coords.latitude,
|
||||
longitude: position.coords.longitude
|
||||
});
|
||||
setLocationPermission('granted');
|
||||
},
|
||||
(error) => {
|
||||
console.log('Erro ao obter localização:', error.message);
|
||||
setLocationPermission('denied');
|
||||
},
|
||||
{
|
||||
enableHighAccuracy: true,
|
||||
timeout: 5000,
|
||||
maximumAge: 0
|
||||
}
|
||||
);
|
||||
} else {
|
||||
console.log('Geolocalização não suportada pelo navegador');
|
||||
setLocationPermission('unsupported');
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onDrop = useCallback(async (acceptedFiles: File[]) => {
|
||||
if (acceptedFiles.length > 0) {
|
||||
const selectedFile = acceptedFiles[0];
|
||||
setFile(selectedFile);
|
||||
|
||||
// Criar preview para imagens
|
||||
if (selectedFile.type.startsWith('image/')) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
setFilePreview(reader.result as string);
|
||||
};
|
||||
reader.readAsDataURL(selectedFile);
|
||||
} else {
|
||||
setFilePreview(null);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
||||
onDrop,
|
||||
accept: {
|
||||
'image/*': ['.jpg', '.jpeg', '.png', '.gif', '.webp'],
|
||||
'application/pdf': ['.pdf']
|
||||
},
|
||||
multiple: false,
|
||||
maxSize: 10 * 1024 * 1024 // 10MB
|
||||
});
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
if (!file) {
|
||||
toast.error('Por favor, selecione um arquivo primeiro');
|
||||
return;
|
||||
}
|
||||
|
||||
setUploading(true);
|
||||
setUploadProgress(0);
|
||||
setError(null);
|
||||
|
||||
const formData = new FormData();
|
||||
|
||||
// Adicionar arquivo com todas as informações preservadas
|
||||
formData.append('files', file);
|
||||
formData.append('modification_times', String(file.lastModified));
|
||||
formData.append('file_names', file.name);
|
||||
formData.append('file_sizes', String(file.size));
|
||||
formData.append('file_types', file.type);
|
||||
|
||||
// Adicionar metadados de contexto
|
||||
formData.append('user_agent', navigator.userAgent);
|
||||
formData.append('language', navigator.language);
|
||||
formData.append('viewport_width', String(window.innerWidth));
|
||||
formData.append('viewport_height', String(window.innerHeight));
|
||||
formData.append('referrer', document.referrer);
|
||||
formData.append('timezone', Intl.DateTimeFormat().resolvedOptions().timeZone);
|
||||
formData.append('timezone_offset', String(new Date().getTimezoneOffset()));
|
||||
formData.append('platform', navigator.platform);
|
||||
formData.append('cookies_enabled', String(navigator.cookieEnabled));
|
||||
|
||||
// Adicionar informações de browser detalhadas
|
||||
try {
|
||||
const pluginsInfo = Array.from(navigator.plugins).map(p => p.name).join(',');
|
||||
formData.append('browser_plugins', pluginsInfo);
|
||||
} catch (e) {
|
||||
formData.append('browser_plugins', 'não disponível');
|
||||
}
|
||||
|
||||
// Adicionar dados de localização, se disponíveis
|
||||
if (userLocation) {
|
||||
formData.append('latitude', String(userLocation.latitude));
|
||||
formData.append('longitude', String(userLocation.longitude));
|
||||
}
|
||||
formData.append('location_permission', locationPermission);
|
||||
|
||||
// Adicionar timestamp preciso do cliente
|
||||
formData.append('client_timestamp', new Date().toISOString());
|
||||
|
||||
// Criar simulação de progresso
|
||||
const progressInterval = setInterval(() => {
|
||||
setUploadProgress(prev => {
|
||||
if (prev >= 95) {
|
||||
clearInterval(progressInterval);
|
||||
return prev;
|
||||
}
|
||||
return prev + 5;
|
||||
});
|
||||
}, 300);
|
||||
|
||||
const response = await fetch('/api/provas/upload', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
clearInterval(progressInterval);
|
||||
setUploadProgress(100);
|
||||
|
||||
// Verificar se a resposta foi bem-sucedida
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
let errorMessage;
|
||||
|
||||
try {
|
||||
const errorJson = JSON.parse(errorText);
|
||||
errorMessage = errorJson.error || errorJson.details || 'Erro ao enviar arquivo';
|
||||
} catch {
|
||||
errorMessage = errorText || 'Erro ao enviar arquivo';
|
||||
}
|
||||
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
// Ler o corpo da resposta
|
||||
const result = await response.json();
|
||||
|
||||
if (result.id) {
|
||||
toast.success('Arquivo enviado com sucesso!');
|
||||
// Navegar para a página de status
|
||||
navigate(`/public/provas/status/${result.id}`);
|
||||
} else {
|
||||
throw new Error('ID do documento não encontrado na resposta');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erro detalhado no upload:', error);
|
||||
setError(error.message || 'Erro ao enviar arquivo. Por favor, tente novamente.');
|
||||
toast.error('Falha no upload: ' + error.message);
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 py-12 px-4 sm:px-6 lg:px-8 flex flex-col justify-center">
|
||||
<div className="max-w-3xl mx-auto w-full">
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900">Validação Digital de Documentos</h1>
|
||||
<p className="mt-2 text-lg text-gray-600">
|
||||
Valide e proteja a autenticidade de seus documentos e imagens com timestamping seguro
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white shadow overflow-hidden rounded-lg">
|
||||
<div className="px-4 py-5 sm:px-6 bg-blue-700 text-white">
|
||||
<h2 className="text-xl font-semibold">Upload de Documento para Validação</h2>
|
||||
<p className="mt-1 text-sm text-blue-100">
|
||||
Formatos aceitos: JPG, PNG, GIF, PDF (máx. 10MB)
|
||||
</p>
|
||||
</div>
|
||||
<div className="px-4 py-5 sm:p-6">
|
||||
<div
|
||||
{...getRootProps()}
|
||||
className={`border-2 border-dashed rounded-lg p-10 text-center cursor-pointer transition-colors ${
|
||||
isDragActive ? 'border-blue-500 bg-blue-50' : 'border-gray-300 hover:border-blue-400'
|
||||
}`}
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
|
||||
{filePreview ? (
|
||||
<div className="space-y-4">
|
||||
<div className="mx-auto max-w-sm">
|
||||
<img
|
||||
src={filePreview}
|
||||
alt="Preview"
|
||||
className="max-h-60 mx-auto object-contain rounded-lg shadow-sm"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600">
|
||||
{file?.name} ({(file?.size || 0) / 1024 < 1024
|
||||
? `${Math.round((file?.size || 0) / 1024)} KB`
|
||||
: `${Math.round((file?.size || 0) / 1024 / 1024 * 10) / 10} MB`})
|
||||
</p>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setFile(null);
|
||||
setFilePreview(null);
|
||||
}}
|
||||
className="text-sm text-red-600 hover:text-red-800"
|
||||
>
|
||||
Remover arquivo
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<svg className="mx-auto h-12 w-12 text-gray-400" stroke="currentColor" fill="none" viewBox="0 0 48 48">
|
||||
<path d="M24 8v16m0 0v16m0-16h16m-16 0H8" strokeWidth="2" strokeLinecap="round" />
|
||||
</svg>
|
||||
<p className="text-lg font-medium text-gray-900">
|
||||
{isDragActive
|
||||
? "Solte o arquivo aqui..."
|
||||
: "Arraste e solte um arquivo aqui, ou clique para selecionar"}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
Os metadados originais e informações de contexto serão preservados para melhor validação
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{locationPermission === 'granted' && (
|
||||
<div className="mt-4 text-sm text-green-600">
|
||||
✓ Localização obtida: Esta informação ajudará na validação do documento
|
||||
</div>
|
||||
)}
|
||||
|
||||
{locationPermission === 'denied' && (
|
||||
<div className="mt-4 text-sm text-yellow-600">
|
||||
⚠️ Localização não permitida: A validação ainda funcionará, mas sem dados de geolocalização
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="mt-4 text-sm text-red-600 p-3 bg-red-50 rounded-md">
|
||||
Erro: {error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{uploading && (
|
||||
<div className="mt-6">
|
||||
<div className="relative pt-1">
|
||||
<div className="flex mb-2 items-center justify-between">
|
||||
<div>
|
||||
<span className="text-xs font-semibold inline-block py-1 px-2 uppercase rounded-full text-blue-600 bg-blue-200">
|
||||
Enviando arquivo
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<span className="text-xs font-semibold inline-block text-blue-600">
|
||||
{uploadProgress}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="overflow-hidden h-2 mb-4 text-xs flex rounded bg-blue-200">
|
||||
<div
|
||||
style={{ width: `${uploadProgress}%` }}
|
||||
className="shadow-none flex flex-col text-center whitespace-nowrap text-white justify-center bg-blue-500 transition-all duration-300">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-6 flex justify-end">
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
disabled={!file || uploading}
|
||||
className={`px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white
|
||||
${!file || uploading
|
||||
? 'bg-gray-300 cursor-not-allowed'
|
||||
: 'bg-blue-700 hover:bg-blue-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500'
|
||||
}`}
|
||||
>
|
||||
{uploading ? 'Enviando...' : 'Enviar para Validação'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-4 py-4 sm:px-6 bg-gray-50 border-t border-gray-200">
|
||||
<div className="text-xs text-gray-500">
|
||||
<strong>Nota sobre privacidade:</strong> Este serviço coleta metadados como informações do dispositivo,
|
||||
localização e timestamp para garantir a validação adequada do documento. Todos os dados são criptografados
|
||||
e armazenados com segurança.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user