Deploy Inicial do SteelCheck com Docker Build Automatizado
This commit is contained in:
68
components/ApiKeySetup.tsx
Normal file
68
components/ApiKeySetup.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import React, { useState } from 'react';
|
||||
import { KeyIcon, SaveIcon } from './Icons';
|
||||
|
||||
interface ApiKeySetupProps {
|
||||
onKeySave: (key: string) => void;
|
||||
}
|
||||
|
||||
export const ApiKeySetup: React.FC<ApiKeySetupProps> = ({ onKeySave }) => {
|
||||
const [localApiKey, setLocalApiKey] = useState('');
|
||||
|
||||
const handleSave = () => {
|
||||
if (localApiKey.trim()) {
|
||||
onKeySave(localApiKey.trim());
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-md mx-auto animate-fade-in pt-10">
|
||||
<div className="bg-white/70 dark:bg-slate-800/60 backdrop-blur-md border border-white/20 dark:border-slate-700/50 shadow-xl p-6 sm:p-8 rounded-2xl relative overflow-hidden group">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-blue-500/10 to-purple-500/10 opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
|
||||
<div className="relative z-10">
|
||||
<h2 className="text-2xl font-display font-bold text-center mb-3 text-slate-800 dark:text-white">
|
||||
Configurar Chave de API
|
||||
</h2>
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400 text-center mb-8">
|
||||
Para começar, insira sua chave de API do Google Gemini. Ela será mantida segura no seu navegador.
|
||||
</p>
|
||||
|
||||
<div className="space-y-5">
|
||||
<div>
|
||||
<label htmlFor="api-key-setup" className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
|
||||
Sua Chave de API
|
||||
</label>
|
||||
<div className="relative group/input">
|
||||
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
<KeyIcon className="h-5 w-5 text-slate-400 group-focus-within/input:text-blue-500 transition-colors" />
|
||||
</div>
|
||||
<input
|
||||
type="password"
|
||||
id="api-key-setup"
|
||||
className="block w-full rounded-xl border-slate-300 dark:border-slate-600 bg-white/50 dark:bg-slate-700/50 py-3 pl-10 pr-3 text-slate-900 dark:text-slate-100 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm backdrop-blur-sm transition-all"
|
||||
placeholder="Cole sua chave aqui"
|
||||
value={localApiKey}
|
||||
onChange={(e) => setLocalApiKey(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSave()}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
<p className="mt-3 text-xs text-center text-slate-500 dark:text-slate-400">
|
||||
Não tem uma chave? <a href="https://aistudio.google.com/app/apikey" target="_blank" rel="noopener noreferrer" className="font-semibold text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 hover:underline">Obtenha no Google AI Studio</a>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={!localApiKey.trim()}
|
||||
className="w-full flex justify-center items-center gap-2 bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 text-white font-bold py-3.5 px-4 rounded-xl shadow-lg hover:shadow-blue-500/30 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-300 transform active:scale-[0.98]"
|
||||
>
|
||||
<SaveIcon className="h-5 w-5" />
|
||||
<span>Salvar e Continuar</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
114
components/FileUpload.tsx
Normal file
114
components/FileUpload.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import React, { useState, useCallback, useRef } from 'react';
|
||||
import { FileIcon, XIcon, UploadIcon } from './Icons';
|
||||
|
||||
interface FileUploadProps {
|
||||
onFileChange: (file: File | null) => void;
|
||||
}
|
||||
|
||||
export const FileUpload: React.FC<FileUploadProps> = ({ onFileChange }) => {
|
||||
const [fileName, setFileName] = useState<string | null>(null);
|
||||
const [isDragOver, setIsDragOver] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleFileSelect = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (file) {
|
||||
setFileName(file.name);
|
||||
onFileChange(file);
|
||||
}
|
||||
}, [onFileChange]);
|
||||
|
||||
const handleRemoveFile = useCallback((e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setFileName(null);
|
||||
onFileChange(null);
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = "";
|
||||
}
|
||||
}, [onFileChange]);
|
||||
|
||||
const onDragOver = useCallback((e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
setIsDragOver(true);
|
||||
}, []);
|
||||
|
||||
const onDragLeave = useCallback((e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
setIsDragOver(false);
|
||||
}, []);
|
||||
|
||||
const onDrop = useCallback((e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
setIsDragOver(false);
|
||||
const file = e.dataTransfer.files?.[0];
|
||||
if (file) {
|
||||
setFileName(file.name);
|
||||
onFileChange(file);
|
||||
}
|
||||
}, [onFileChange]);
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
{!fileName ? (
|
||||
<label
|
||||
htmlFor="file-upload"
|
||||
onDragOver={onDragOver}
|
||||
onDragLeave={onDragLeave}
|
||||
onDrop={onDrop}
|
||||
className={`
|
||||
group relative flex flex-col items-center justify-center w-full h-64
|
||||
rounded-2xl border-2 border-dashed cursor-pointer transition-all duration-300
|
||||
${isDragOver
|
||||
? 'border-blue-500 bg-blue-50/50 dark:bg-blue-900/20 scale-[0.99]'
|
||||
: 'border-slate-300 dark:border-slate-600 hover:border-blue-400 dark:hover:border-blue-500 hover:bg-slate-50/50 dark:hover:bg-slate-800/50'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<div className="flex flex-col items-center justify-center pt-5 pb-6 text-center">
|
||||
<div className={`
|
||||
p-4 rounded-full mb-4 transition-transform duration-300 group-hover:scale-110
|
||||
${isDragOver ? 'bg-blue-100 text-blue-600' : 'bg-slate-100 dark:bg-slate-700 text-slate-500 dark:text-slate-400 group-hover:bg-blue-50 group-hover:text-blue-500 dark:group-hover:bg-slate-700'}
|
||||
`}>
|
||||
<UploadIcon className="w-8 h-8" />
|
||||
</div>
|
||||
<p className="mb-2 text-lg font-medium text-slate-700 dark:text-slate-200">
|
||||
Clique ou arraste seu arquivo aqui
|
||||
</p>
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400">
|
||||
PDF, JPG ou PNG (máx. 10MB)
|
||||
</p>
|
||||
</div>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
id="file-upload"
|
||||
name="file-upload"
|
||||
type="file"
|
||||
className="hidden"
|
||||
onChange={handleFileSelect}
|
||||
accept="image/*,application/pdf"
|
||||
/>
|
||||
</label>
|
||||
) : (
|
||||
<div className="group relative w-full p-4 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl shadow-sm hover:shadow-md transition-all flex items-center justify-between animate-fade-in">
|
||||
<div className="flex items-center gap-4 overflow-hidden">
|
||||
<div className="p-3 bg-blue-50 dark:bg-blue-900/30 rounded-lg text-blue-600 dark:text-blue-400">
|
||||
<FileIcon className="w-6 h-6" />
|
||||
</div>
|
||||
<div className="flex flex-col min-w-0">
|
||||
<span className="text-sm font-semibold text-slate-700 dark:text-slate-200 truncate pr-4">{fileName}</span>
|
||||
<span className="text-xs text-green-600 dark:text-green-400 font-medium">Pronto para análise</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleRemoveFile}
|
||||
className="p-2 text-slate-400 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-full transition-all"
|
||||
title="Remover arquivo"
|
||||
>
|
||||
<XIcon className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
72
components/Header.tsx
Normal file
72
components/Header.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import React from 'react';
|
||||
import { LogoBase64, SunIcon, MoonIcon, RefreshIcon, SignOutIcon } from './Icons';
|
||||
import { useTheme } from '../context/ThemeContext';
|
||||
|
||||
interface HeaderProps {
|
||||
onReset: () => void;
|
||||
onClearKey: () => void;
|
||||
hasKey: boolean;
|
||||
}
|
||||
|
||||
export const Header: React.FC<HeaderProps> = ({ onReset, onClearKey, hasKey }) => {
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-30 transition-all duration-300 backdrop-blur-md bg-white/70 dark:bg-slate-900/70 border-b border-white/20 dark:border-slate-800 shadow-sm">
|
||||
<div className="container mx-auto px-4 py-3 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative group cursor-pointer">
|
||||
<div className="absolute -inset-1 bg-gradient-to-r from-blue-600 to-indigo-600 rounded-full blur opacity-25 group-hover:opacity-50 transition duration-1000 group-hover:duration-200"></div>
|
||||
<img src={LogoBase64} alt="SteelBase Logo" className="relative w-10 h-10 object-contain" />
|
||||
</div>
|
||||
|
||||
<div className="hidden sm:block">
|
||||
<h1 className="text-2xl font-display font-bold bg-clip-text text-transparent bg-gradient-to-r from-slate-900 to-slate-700 dark:from-white dark:to-slate-300">
|
||||
SteelBase
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 sm:gap-4">
|
||||
{hasKey && (
|
||||
<button
|
||||
onClick={onClearKey}
|
||||
className="hidden sm:flex items-center gap-2 px-3 py-1.5 rounded-full text-xs font-medium text-slate-500 hover:text-red-500 hover:bg-red-50 dark:text-slate-400 dark:hover:bg-red-900/20 transition-all border border-transparent hover:border-red-200 dark:hover:border-red-900"
|
||||
title="Alterar Chave de API"
|
||||
>
|
||||
<SignOutIcon className="w-4 h-4" />
|
||||
<span>Sair</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-1 bg-slate-100 dark:bg-slate-800/50 p-1 rounded-full border border-slate-200 dark:border-slate-700">
|
||||
<button
|
||||
onClick={onReset}
|
||||
className="p-2 rounded-full text-slate-500 dark:text-slate-400 hover:bg-white dark:hover:bg-slate-700 hover:shadow-sm focus:outline-none transition-all"
|
||||
aria-label="Carregar outro arquivo"
|
||||
title="Novo Relatório"
|
||||
>
|
||||
<RefreshIcon className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
<div className="w-px h-4 bg-slate-300 dark:bg-slate-700 mx-1"></div>
|
||||
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
className="p-2 rounded-full text-slate-500 dark:text-slate-400 hover:bg-white dark:hover:bg-slate-700 hover:shadow-sm focus:outline-none transition-all relative overflow-hidden"
|
||||
aria-label="Toggle theme"
|
||||
>
|
||||
<div className="relative z-10">
|
||||
{theme === 'light' ? (
|
||||
<MoonIcon className="w-5 h-5" />
|
||||
) : (
|
||||
<SunIcon className="w-5 h-5" />
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
92
components/Icons.tsx
Normal file
92
components/Icons.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import React from 'react';
|
||||
|
||||
// FIX: Truncated the corrupted base64 string and properly terminated it.
|
||||
// The original string was malformed, containing parts of another component's code,
|
||||
// which caused a syntax error and prevented any exports from this file.
|
||||
export const LogoBase64 = `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAACACAYAAAC6XbIEAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAvvSURBVHhe7Z1/aBxlHcd/7542cZPEJmliEyfxtEm825a0aVunbYy22bpp/6AtdGklbKFVqVT8QSoqCCIVqYACKlQEXQVBUBHFg4KogCiIAr+IKioqPlQEFV+kiD5UDC/8QUT0g4pYtGmbdvEmaRM38SZpEyfJzb7Pnd2d7M7szsiuuyf7/X7enXfZnZmdnZ25nZ2Z3ZlxQIIECRIk6F+S+v+vJEiQIEGCJEmQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEi`;
|
||||
|
||||
export const SunIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" {...props}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const MoonIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" {...props}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M21.752 15.002A9.718 9.718 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const RefreshIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" {...props}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0011.664 0l3.181-3.183m-11.664 0l4.5-4.5a3.75 3.75 0 00-5.303 0l-4.5 4.5m11.664 0l-3.182 3.182a8.25 8.25 0 01-11.664 0l-3.182-3.182m11.664 0l-4.5-4.5a3.75 3.75 0 015.303 0l4.5 4.5" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const UploadIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" {...props}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
|
||||
export const EyeIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" {...props}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.432 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const DownloadIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" {...props}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const FileIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" {...props}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m.158 10.308l-3.182-3.182A4.5 4.5 0 004.5 12.5v2.25A3.375 3.375 0 007.875 18h8.25A3.375 3.375 0 0019.5 14.75v-2.25a2.25 2.25 0 00-2.25-2.25H15M12 12.75h.008v.008H12v-.008z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const XIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" {...props}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const CheckCircleIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" {...props}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const XCircleIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" {...props}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9.75 9.75l4.5 4.5m0-4.5l-4.5 4.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const InfoIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" {...props}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const KeyIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" {...props}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const SaveIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" {...props}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 8.25H7.5a2.25 2.25 0 00-2.25 2.25v9a2.25 2.25 0 002.25 2.25h9a2.25 2.25 0 002.25-2.25v-9a2.25 2.25 0 00-2.25-2.25H15M9 12l3 3m0 0l3-3m-3 3V2.25" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const SignOutIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" {...props}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15M12 9l-3 3m0 0l3 3m-3-3h12.75" />
|
||||
</svg>
|
||||
);
|
||||
30
components/Loader.tsx
Normal file
30
components/Loader.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
const loadingMessages = [
|
||||
"Analisando o certificado...",
|
||||
"Extraindo dados com OCR...",
|
||||
"Consultando normas técnicas...",
|
||||
"Verificando propriedades mecânicas...",
|
||||
"Analisando composição química...",
|
||||
"Compilando o relatório final...",
|
||||
];
|
||||
|
||||
export const Loader: React.FC = () => {
|
||||
const [messageIndex, setMessageIndex] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setMessageIndex((prevIndex) => (prevIndex + 1) % loadingMessages.length);
|
||||
}, 2500);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-white/80 dark:bg-gray-900/80 backdrop-blur-sm flex flex-col justify-center items-center z-50">
|
||||
<div className="w-16 h-16 border-4 border-dashed rounded-full animate-spin border-blue-500"></div>
|
||||
<p className="mt-4 text-gray-800 dark:text-white text-lg font-semibold">{loadingMessages[messageIndex]}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
105
components/PrintableReport.tsx
Normal file
105
components/PrintableReport.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import React from 'react';
|
||||
import type { ReportData, ComplianceItem } from '../types';
|
||||
|
||||
interface PrintableReportProps {
|
||||
report: ReportData;
|
||||
}
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
// Tries to parse common date formats like DD.MM.YYYY or YYYY-MM-DD
|
||||
const parts = dateString.split(/[.\-/]/);
|
||||
if (parts.length === 3) {
|
||||
// Assuming DD.MM.YYYY
|
||||
if (parts[0].length === 2) return `${parts[0]}/${parts[1]}/${parts[2]}`;
|
||||
// Assuming YYYY-MM-DD
|
||||
if (parts[0].length === 4) return `${parts[2]}/${parts[1]}/${parts[0]}`;
|
||||
}
|
||||
return dateString; // fallback
|
||||
};
|
||||
|
||||
const TableRow: React.FC<{item: ComplianceItem}> = ({item}) => (
|
||||
<tr className="border-b text-xs">
|
||||
<td className="py-1 px-2 font-medium">{item.property || item.element || item.test}</td>
|
||||
<td className="py-1 px-2">{item.norm}</td>
|
||||
<td className="py-1 px-2 font-bold">{item.certificate}</td>
|
||||
<td className={`py-1 px-2 font-bold ${item.status === 'OK' ? 'text-green-700' : 'text-red-700'}`}>{item.status}</td>
|
||||
</tr>
|
||||
)
|
||||
|
||||
export const PrintableReport: React.FC<PrintableReportProps> = ({ report }) => {
|
||||
const { identification, compliance, overPerformance, equivalents, confidence } = report;
|
||||
|
||||
return (
|
||||
<div id="printable-report-container" style={{ width: '210mm', height: '297mm' }} className="bg-white text-gray-900 p-8 font-sans flex flex-col">
|
||||
<header className="flex justify-between items-start pb-2 border-b-2 border-gray-400">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-blue-800">SteelBase</h1>
|
||||
<p className="text-sm">Análise de Qualidade Industrial com IA</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<h2 className="text-2xl font-bold">Relatório de Análise Técnica</h2>
|
||||
<p className="text-sm">Documento gerado em: {new Date().toLocaleDateString('pt-BR')}</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="flex-grow">
|
||||
<section className="mt-4 border border-gray-300 rounded p-3 grid grid-cols-2 gap-x-6 gap-y-2 text-sm">
|
||||
<div><strong>Produto:</strong> {identification.product}</div>
|
||||
<div><strong>Norma Principal:</strong> {identification.standards}</div>
|
||||
<div><strong>Fabricante:</strong> {identification.manufacturer}</div>
|
||||
<div><strong>Nº Certificado:</strong> {identification.certificateNumber}</div>
|
||||
<div><strong>Lotes:</strong> {identification.batches}</div>
|
||||
<div><strong>Corridas:</strong> {identification.heats}</div>
|
||||
<div><strong>Quantidade:</strong> {identification.quantity}</div>
|
||||
<div><strong>Data Emissão:</strong> {formatDate(identification.certificateDate)}</div>
|
||||
</section>
|
||||
|
||||
<section className="mt-4">
|
||||
<h3 className="text-base font-bold text-gray-800">Resumo da Análise</h3>
|
||||
<div className="mt-1 border border-gray-300 rounded p-2 text-xs">
|
||||
<p>O material de {identification.product} atende aos requisitos da norma {identification.standards}. Status Geral de Conformidade: <span className={`font-extrabold ${compliance.status === 'CONFORME' ? 'text-green-700' : 'text-red-700'}`}>{compliance.status}</span>.</p>
|
||||
{compliance.status === 'CONFORME' && overPerformance.length > 0 && (
|
||||
<p className="mt-1">O material excede os requisitos normativos em pontos-chave, como {overPerformance.map(item => item.property).join(', ')}.</p>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mt-4">
|
||||
<h3 className="text-base font-bold text-gray-800">Análise de Composição Química</h3>
|
||||
<table className="w-full mt-1 border-collapse border border-gray-300">
|
||||
<thead className="bg-gray-200 text-left text-xs uppercase">
|
||||
<tr><th className="py-1 px-2">Elemento</th><th className="py-1 px-2">Norma</th><th className="py-1 px-2">Certificado</th><th className="py-1 px-2">Status</th></tr>
|
||||
</thead>
|
||||
<tbody>{compliance.chemical.map((item, i) => <TableRow key={i} item={item} />)}</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section className="mt-4">
|
||||
<h3 className="text-base font-bold text-gray-800">Análise de Propriedades Mecânicas</h3>
|
||||
<table className="w-full mt-1 border-collapse border border-gray-300">
|
||||
<thead className="bg-gray-200 text-left text-xs uppercase">
|
||||
<tr><th className="py-1 px-2">Propriedade</th><th className="py-1 px-2">Norma</th><th className="py-1 px-2">Certificado</th><th className="py-1 px-2">Status</th></tr>
|
||||
</thead>
|
||||
<tbody>{compliance.mechanical.map((item, i) => <TableRow key={i} item={item} />)}</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section className="mt-4">
|
||||
<h3 className="text-base font-bold text-gray-800">Normas Equivalentes</h3>
|
||||
<div className="mt-1 grid grid-cols-4 gap-2">
|
||||
{equivalents.map((item, i) => (
|
||||
<div key={i} className="bg-gray-100 border border-gray-300 rounded p-2 text-center">
|
||||
<p className="text-xs font-semibold text-gray-600">{item.system}</p>
|
||||
<p className="text-sm font-bold text-blue-800">{item.norm}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer className="text-center text-xs text-gray-500 pt-2 border-t border-gray-300">
|
||||
Relatório gerado por SteelBase | Confiança da Análise: {confidence}%
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
213
components/ReportDisplay.tsx
Normal file
213
components/ReportDisplay.tsx
Normal file
@@ -0,0 +1,213 @@
|
||||
|
||||
import React from 'react';
|
||||
import type { ReportData, ComplianceItem } from '../types';
|
||||
import { CheckCircleIcon, XCircleIcon, InfoIcon } from './Icons';
|
||||
|
||||
interface ReportDisplayProps {
|
||||
report: ReportData;
|
||||
}
|
||||
|
||||
const statusClass = (status: 'OK' | 'FALHA' | 'CONFORME' | 'NÃO CONFORME') => {
|
||||
switch (status) {
|
||||
case 'OK':
|
||||
case 'CONFORME':
|
||||
return 'text-emerald-600 bg-emerald-50 dark:bg-emerald-900/20 dark:text-emerald-400 border border-emerald-200 dark:border-emerald-800';
|
||||
case 'FALHA':
|
||||
case 'NÃO CONFORME':
|
||||
return 'text-red-600 bg-red-50 dark:bg-red-900/20 dark:text-red-400 border border-red-200 dark:border-red-800';
|
||||
default:
|
||||
return 'text-slate-600 bg-slate-50 dark:bg-slate-800/50 dark:text-slate-400 border border-slate-200 dark:border-slate-700';
|
||||
}
|
||||
};
|
||||
|
||||
const statusIcon = (status: 'OK' | 'FALHA') => {
|
||||
return status === 'OK'
|
||||
? <CheckCircleIcon className="w-4 h-4" />
|
||||
: <XCircleIcon className="w-4 h-4" />;
|
||||
};
|
||||
|
||||
|
||||
const ComplianceTableRow: React.FC<{ item: ComplianceItem; headerLabel: string }> = ({ item, headerLabel }) => (
|
||||
<tr className="block md:table-row mb-4 md:mb-0 bg-white/50 dark:bg-slate-800/50 md:bg-transparent rounded-xl md:rounded-none shadow-sm md:shadow-none border border-slate-100 dark:border-slate-700 md:border-b md:border-slate-200/50 md:dark:border-slate-700/50 last:border-0 hover:bg-white/80 dark:hover:bg-slate-800/80 transition-colors">
|
||||
<td className="p-4 md:py-4 md:px-6 flex justify-between items-center border-b border-slate-100 dark:border-slate-700 md:border-0 md:table-cell">
|
||||
<span className="text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400 md:hidden">{headerLabel}</span>
|
||||
<span className="font-medium text-slate-700 dark:text-slate-300">{item.property || item.element || item.test}</span>
|
||||
</td>
|
||||
<td className="p-4 md:py-4 md:px-6 flex justify-between items-center border-b border-slate-100 dark:border-slate-700 md:border-0 md:table-cell">
|
||||
<span className="text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400 md:hidden">Norma</span>
|
||||
<span className="text-slate-500 dark:text-slate-400 text-sm font-mono">{item.norm}</span>
|
||||
</td>
|
||||
<td className="p-4 md:py-4 md:px-6 flex justify-between items-center border-b border-slate-100 dark:border-slate-700 md:border-0 md:table-cell">
|
||||
<span className="text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400 md:hidden">Certificado</span>
|
||||
<span className="text-slate-800 dark:text-slate-200 font-semibold font-mono">{item.certificate}</span>
|
||||
</td>
|
||||
<td className="p-4 md:py-4 md:px-6 flex justify-between items-center md:table-cell">
|
||||
<span className="text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400 md:hidden">Status</span>
|
||||
<span className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-bold uppercase tracking-wide ${statusClass(item.status)}`}>
|
||||
{statusIcon(item.status)}
|
||||
{item.status}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
|
||||
export const ReportDisplay: React.FC<ReportDisplayProps> = ({ report }) => {
|
||||
const { identification, compliance, overPerformance, equivalents, confidence } = report;
|
||||
|
||||
return (
|
||||
<div className="space-y-8 animate-fade-in pb-12">
|
||||
<div className="text-center space-y-2">
|
||||
<h2 className="text-3xl md:text-4xl font-display font-bold text-slate-900 dark:text-white">Relatório Técnico de Qualidade</h2>
|
||||
<p className="text-slate-500 dark:text-slate-400">Análise de conformidade normativa assistida por IA</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white/70 dark:bg-slate-800/60 backdrop-blur-md border border-white/20 dark:border-slate-700/50 shadow-xl p-8 rounded-2xl text-center relative overflow-hidden">
|
||||
<div className="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-blue-500 via-indigo-500 to-purple-500"></div>
|
||||
<h3 className="text-lg font-medium text-slate-500 dark:text-slate-400 mb-2 uppercase tracking-wide">Grau de Confiança da Análise</h3>
|
||||
<div className="flex justify-center items-end gap-2">
|
||||
<span className="text-6xl sm:text-7xl font-display font-bold bg-clip-text text-transparent bg-gradient-to-br from-blue-600 to-indigo-600 dark:from-blue-400 dark:to-indigo-400">{confidence}</span>
|
||||
<span className="text-3xl font-bold text-slate-400 mb-2">%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Identification Section */}
|
||||
<div className="bg-white/70 dark:bg-slate-800/60 backdrop-blur-md border border-white/20 dark:border-slate-700/50 shadow-xl rounded-2xl overflow-hidden">
|
||||
<div className="px-6 py-4 bg-slate-50/50 dark:bg-slate-800/50 border-b border-slate-200/50 dark:border-slate-700/50 backdrop-blur-sm">
|
||||
<h3 className="text-xl font-display font-bold text-slate-800 dark:text-white">1. Dados de Identificação</h3>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<ul className="grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-4">
|
||||
{Object.entries(identification).map(([key, value]) => (
|
||||
<li key={key} className="flex flex-col border-b border-slate-100 dark:border-slate-800/50 pb-3 last:border-0 last:pb-0">
|
||||
<span className="text-xs font-bold text-slate-400 uppercase tracking-wider mb-1">{key.replace(/([A-Z])/g, ' $1')}</span>
|
||||
<span className="text-lg font-medium text-slate-800 dark:text-slate-200">{value}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Compliance Section */}
|
||||
<div className="bg-white/70 dark:bg-slate-800/60 backdrop-blur-md border border-white/20 dark:border-slate-700/50 shadow-xl rounded-2xl overflow-hidden">
|
||||
<div className="px-6 py-4 bg-slate-50/50 dark:bg-slate-800/50 border-b border-slate-200/50 dark:border-slate-700/50 backdrop-blur-sm flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<h3 className="text-xl font-display font-bold text-slate-800 dark:text-white">2. Verificação de Conformidade</h3>
|
||||
<div className={`px-4 py-1.5 rounded-full font-bold text-sm uppercase tracking-wide border shadow-sm ${statusClass(compliance.status)}`}>
|
||||
{compliance.status}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6 space-y-8">
|
||||
<div>
|
||||
<h4 className="flex items-center gap-2 font-display font-semibold text-lg mb-4 text-slate-800 dark:text-slate-200">
|
||||
<span className="w-1.5 h-6 bg-blue-500 rounded-full"></span>
|
||||
Análise de Propriedades Mecânicas
|
||||
</h4>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left border-collapse">
|
||||
<thead className="hidden md:table-header-group text-xs uppercase tracking-wider text-slate-500 dark:text-slate-400 border-b-2 border-slate-100 dark:border-slate-700">
|
||||
<tr>
|
||||
<th className="py-3 px-6 font-semibold">Propriedade</th>
|
||||
<th className="py-3 px-6 font-semibold">Norma</th>
|
||||
<th className="py-3 px-6 font-semibold">Certificado</th>
|
||||
<th className="py-3 px-6 font-semibold w-32">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="md:divide-y md:divide-slate-100 dark:md:divide-slate-700/50">
|
||||
{compliance.mechanical.map((item, i) => <ComplianceTableRow key={i} item={item} headerLabel="Propriedade" />)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="flex items-center gap-2 font-display font-semibold text-lg mb-4 text-slate-800 dark:text-slate-200">
|
||||
<span className="w-1.5 h-6 bg-purple-500 rounded-full"></span>
|
||||
Análise de Composição Química (%)
|
||||
</h4>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left border-collapse">
|
||||
<thead className="hidden md:table-header-group text-xs uppercase tracking-wider text-slate-500 dark:text-slate-400 border-b-2 border-slate-100 dark:border-slate-700">
|
||||
<tr>
|
||||
<th className="py-3 px-6 font-semibold">Elemento</th>
|
||||
<th className="py-3 px-6 font-semibold">Norma</th>
|
||||
<th className="py-3 px-6 font-semibold">Certificado</th>
|
||||
<th className="py-3 px-6 font-semibold w-32">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="md:divide-y md:divide-slate-100 dark:md:divide-slate-700/50">
|
||||
{compliance.chemical.map((item, i) => <ComplianceTableRow key={i} item={item} headerLabel="Elemento" />)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{compliance.otherTests.length > 0 && (
|
||||
<div>
|
||||
<h4 className="flex items-center gap-2 font-display font-semibold text-lg mb-4 text-slate-800 dark:text-slate-200">
|
||||
<span className="w-1.5 h-6 bg-emerald-500 rounded-full"></span>
|
||||
Outros Testes
|
||||
</h4>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left border-collapse">
|
||||
<thead className="hidden md:table-header-group text-xs uppercase tracking-wider text-slate-500 dark:text-slate-400 border-b-2 border-slate-100 dark:border-slate-700">
|
||||
<tr>
|
||||
<th className="py-3 px-6 font-semibold">Teste</th>
|
||||
<th className="py-3 px-6 font-semibold">Norma</th>
|
||||
<th className="py-3 px-6 font-semibold">Certificado</th>
|
||||
<th className="py-3 px-6 font-semibold w-32">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="md:divide-y md:divide-slate-100 dark:md:divide-slate-700/50">
|
||||
{compliance.otherTests.map((item, i) => <ComplianceTableRow key={i} item={item} headerLabel="Teste" />)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Over-Performance Section */}
|
||||
{compliance.status === 'CONFORME' && overPerformance.length > 0 && (
|
||||
<div className="bg-white/70 dark:bg-slate-800/60 backdrop-blur-md border border-white/20 dark:border-slate-700/50 shadow-xl rounded-2xl overflow-hidden border-l-4 border-l-green-500">
|
||||
<div className="px-6 py-4 bg-green-50/50 dark:bg-green-900/10 border-b border-green-100 dark:border-green-900/30">
|
||||
<h3 className="text-xl font-display font-bold text-green-800 dark:text-green-300">3. Destaques de Desempenho</h3>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<p className="text-sm text-slate-600 dark:text-slate-400 mb-6">Este material excede os requisitos mínimos normativos nos seguintes aspectos:</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{overPerformance.map((item, i) => (
|
||||
<div key={i} className="flex items-start gap-4 p-4 rounded-xl bg-white/50 dark:bg-slate-800/50 border border-green-100 dark:border-green-900/30 shadow-sm">
|
||||
<div className="p-2 bg-green-100 dark:bg-green-900/30 rounded-full text-green-600 dark:text-green-400 shrink-0">
|
||||
<CheckCircleIcon className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<span className="block font-semibold text-slate-800 dark:text-slate-200">{item.property}</span>
|
||||
<span className="text-green-600 dark:text-green-400 text-sm font-medium">{item.value} superior ao mínimo.</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Equivalents Section */}
|
||||
<div className="glass-panel rounded-2xl overflow-hidden">
|
||||
<div className="px-6 py-4 bg-slate-50/50 dark:bg-slate-800/50 border-b border-slate-200/50 dark:border-slate-700/50 backdrop-blur-sm">
|
||||
<h3 className="text-xl font-display font-bold text-slate-800 dark:text-white">4. Normas Equivalentes</h3>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400 mb-6">Equivalências internacionais para <span className="font-semibold text-slate-700 dark:text-slate-300">{identification.standards}</span>:</p>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||
{equivalents.map((item, i) => (
|
||||
<div key={i} className="group p-4 rounded-xl bg-gradient-to-br from-slate-50 to-white dark:from-slate-800 dark:to-slate-800/50 border border-slate-200 dark:border-slate-700 shadow-sm hover:shadow-md transition-all hover:-translate-y-1">
|
||||
<p className="text-xs font-semibold text-slate-400 dark:text-slate-500 uppercase tracking-wider mb-1 group-hover:text-blue-500 transition-colors">{item.system}</p>
|
||||
<p className="font-bold text-slate-800 dark:text-slate-200 font-mono text-lg">{item.norm}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user