first commit

This commit is contained in:
rodrigo committed 2025-04-10 23:07:21 -03:00
commit c9fb67e751
423 files changed
+53287

No files matched your search

+313
View File
@@ -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>
);
}