init: scripts diversos (crawlers, conversores, scrapers)

This commit is contained in:
2026-03-05 20:38:36 +00:00
commit 6ac6f4be2a
925 changed files with 850330 additions and 0 deletions

View File

@@ -0,0 +1,94 @@
/**
* Accounts.tsx
*
* @author Descomplicar® Crescimento Digital
* @link https://descomplicar.pt
* @copyright 2025 Descomplicar®
*/
import React, { useState } from 'react';
import type { Account } from '../types';
import AddAccountModal from './AddAccountModal';
import { GmailIcon } from './icons/GmailIcon';
import { OutlookIcon } from './icons/OutlookIcon';
import { YahooIcon } from './icons/YahooIcon';
import { ImapIcon } from './icons/ImapIcon';
interface AccountsProps {
accounts: Account[];
addAccount: (account: Omit<Account, 'id' | 'avatar'>) => void;
}
const ProviderIcon = ({ provider }: { provider: Account['provider'] }) => {
switch (provider) {
case 'gmail':
return <GmailIcon className="w-6 h-6" />;
case 'outlook':
return <OutlookIcon className="w-6 h-6" />;
case 'yahoo':
return <YahooIcon className="w-6 h-6" />;
case 'imap':
return <ImapIcon className="w-6 h-6" />;
default:
return null;
}
};
const Accounts: React.FC<AccountsProps> = ({ accounts, addAccount }) => {
const [isModalOpen, setIsModalOpen] = useState(false);
const handleAddAccount = (account: Omit<Account, 'id' | 'avatar'>) => {
addAccount(account);
setIsModalOpen(false);
}
return (
<>
<div className="space-y-6">
<div className="flex justify-between items-center">
<div>
<h2 className="text-2xl font-bold text-slate-800 dark:text-white">Contas Conectadas</h2>
<p className="text-slate-500 dark:text-slate-400">Gerencie as contas de e-mail que você deseja limpar.</p>
</div>
<button
onClick={() => setIsModalOpen(true)}
className="bg-sky-500 text-white font-semibold py-2 px-5 rounded-lg hover:bg-sky-600 transition-colors flex items-center gap-2"
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-5 h-5">
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
Adicionar Nova Conta
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{accounts.map(account => (
<div key={account.id} className="bg-white dark:bg-slate-800 rounded-xl shadow-md p-5 border border-slate-200 dark:border-slate-700 flex items-center gap-4">
<img src={account.avatar} alt={`Avatar de ${account.email}`} className="w-12 h-12 rounded-full" />
<div className="flex-1">
<p className="font-semibold text-slate-700 dark:text-slate-200 truncate">{account.email}</p>
<p className="text-sm text-slate-500 dark:text-slate-400 capitalize flex items-center gap-2">
<ProviderIcon provider={account.provider} />
{account.provider}
</p>
</div>
<button onClick={() => removeAccount(account.id)} className="p-2 rounded-full text-slate-400 hover:bg-slate-100 hover:text-red-500 dark:hover:bg-slate-700 transition-colors" aria-label="Remover conta">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-5 h-5">
<path strokeLinecap="round" strokeLinejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.134-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.067-2.09 1.02-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
</svg>
</button>
</div>
))}
</div>
</div>
<AddAccountModal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
onAddAccount={handleAddAccount}
/>
</>
);
};
export default Accounts;

View File

@@ -0,0 +1,103 @@
/**
* AddAccountModal.tsx
*
* @author Descomplicar® Crescimento Digital
* @link https://descomplicar.pt
* @copyright 2025 Descomplicar®
*/
import React, { useState } from 'react';
import type { Account } from '../types';
import { GmailIcon } from './icons/GmailIcon';
import { OutlookIcon } from './icons/OutlookIcon';
import { YahooIcon } from './icons/YahooIcon';
import { ImapIcon } from './icons/ImapIcon';
import AddImapAccountForm from './AddImapAccountForm';
interface AddAccountModalProps {
isOpen: boolean;
onClose: () => void;
onAddAccount: (account: Omit<Account, 'id' | 'avatar'>) => void;
}
const AddAccountModal: React.FC<AddAccountModalProps> = ({ isOpen, onClose, onAddAccount }) => {
const [view, setView] = useState<'providers' | 'imapForm'>('providers');
if (!isOpen) return null;
const handleProviderClick = (provider: Account['provider']) => {
const email = `novo.usuario.${Math.floor(Math.random() * 1000)}@${provider}.com`;
onAddAccount({ email, provider });
};
const handleImapConnect = (email: string) => {
onAddAccount({ email, provider: 'imap' });
}
const providers: { name: Account['provider'], icon: React.ReactNode }[] = [
{ name: 'gmail', icon: <GmailIcon className="w-6 h-6" /> },
{ name: 'outlook', icon: <OutlookIcon className="w-6 h-6" /> },
{ name: 'yahoo', icon: <YahooIcon className="w-6 h-6 text-[#6001d2]" /> },
];
return (
<div
className="fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center p-4"
aria-modal="true"
role="dialog"
onClick={onClose}
>
<div
className="bg-white dark:bg-slate-800 rounded-xl shadow-2xl p-6 w-full max-w-sm transform transition-all"
role="document"
onClick={(e) => e.stopPropagation()}
>
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-semibold text-slate-900 dark:text-white" id="modal-title">
{view === 'providers' ? 'Conectar nova conta' : 'Conectar Conta IMAP'}
</h3>
<button onClick={onClose} className="p-1 rounded-full hover:bg-slate-100 dark:hover:bg-slate-700">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-6 h-6 text-slate-500">
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18 18 6M6 6l12 12" />
</svg>
</button>
</div>
{view === 'providers' ? (
<>
<p className="text-sm text-slate-500 dark:text-slate-400 mb-6">
Selecione seu provedor de e-mail para conectar sua conta com segurança.
</p>
<div className="space-y-3">
{providers.map(provider => (
<button
key={provider.name}
onClick={() => handleProviderClick(provider.name)}
className="w-full flex items-center gap-4 p-4 rounded-lg border border-slate-200 dark:border-slate-700 hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-colors"
>
{provider.icon}
<span className="font-medium text-slate-700 dark:text-slate-200 capitalize">
Conectar com {provider.name}
</span>
</button>
))}
<button
onClick={() => setView('imapForm')}
className="w-full flex items-center gap-4 p-4 rounded-lg border border-slate-200 dark:border-slate-700 hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-colors"
>
<ImapIcon className="w-6 h-6 text-slate-500" />
<span className="font-medium text-slate-700 dark:text-slate-200">
Conectar via IMAP
</span>
</button>
</div>
</>
) : (
<AddImapAccountForm onConnect={handleImapConnect} onBack={() => setView('providers')} />
)}
</div>
</div>
);
};
export default AddAccountModal;

View File

@@ -0,0 +1,127 @@
/**
* AddImapAccountForm.tsx
*
* @author Descomplicar® Crescimento Digital
* @link https://descomplicar.pt
* @copyright 2025 Descomplicar®
*/
import React, { useState } from 'react';
import Spinner from './Spinner'; // Certifique-se que este componente exista
interface AddImapAccountFormProps {
onConnect: (email: string, user: string, host: string, port: number, secure: boolean) => void;
onBack: () => void;
}
const AddImapAccountForm: React.FC<AddImapAccountFormProps> = ({ onConnect, onBack }) => {
const [email, setEmail] = useState('');
const [user, setUser] = useState('');
const [password, setPassword] = useState('');
const [host, setHost] = useState('');
const [port, setPort] = useState(993);
const [secure, setSecure] = useState(true);
const [testStatus, setTestStatus] = useState<'idle' | 'testing' | 'success' | 'error'>('idle');
const [testMessage, setTestMessage] = useState('');
const handleTestConnection = async () => {
setTestStatus('testing');
setTestMessage('');
// A API que criamos espera um corpo de requisição (body).
// Como estamos em um ambiente de desenvolvimento sem um servidor Node rodando,
// não podemos chamar a API diretamente. A lógica abaixo é como o frontend
// se comportaria se estivesse fazendo um fetch para /api/test-imap.
// Por enquanto, vamos simular a chamada e focar na UI.
try {
const response = await fetch('/api/test-imap', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ host, port, user, password, secure })
});
if (!response.ok) {
// Tenta extrair uma mensagem de erro amigável do corpo da resposta
const errorResult = await response.json().catch(() => null);
throw new Error(errorResult?.message || `Erro no servidor: ${response.statusText}`);
}
const result = await response.json();
if (result.success) {
setTestStatus('success');
setTestMessage(result.message);
} else {
setTestStatus('error');
setTestMessage(result.message || 'Ocorreu um erro desconhecido.');
}
} catch (error) {
setTestStatus('error');
setTestMessage(error instanceof Error ? error.message : 'Falha ao se comunicar com o servidor de teste.');
}
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (testStatus === 'success') {
onConnect(email, user, host, port, secure);
} else {
alert('Por favor, teste a conexão com sucesso antes de adicionar a conta.');
}
};
return (
<form onSubmit={handleSubmit} className="space-y-4">
{/* Campos do formulário... */}
<div>
<label htmlFor="email" className="block text-sm font-medium text-slate-700 dark:text-slate-300">Endereço de E-mail (para exibição)</label>
<input type="email" id="email" value={email} onChange={(e) => setEmail(e.target.value)} required className="mt-1 block w-full input" placeholder="seunome@dominio.com" />
</div>
<div>
<label htmlFor="user" className="block text-sm font-medium text-slate-700 dark:text-slate-300">Nome de Usuário (para login)</label>
<input type="text" id="user" value={user} onChange={(e) => setUser(e.target.value)} required className="mt-1 block w-full input" placeholder="geralmente o mesmo que o e-mail" />
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-slate-700 dark:text-slate-300">Senha</label>
<input type="password" id="password" value={password} onChange={(e) => setPassword(e.target.value)} required className="mt-1 block w-full input" />
</div>
<div className="grid grid-cols-3 gap-4">
<div className="col-span-2">
<label htmlFor="host" className="block text-sm font-medium text-slate-700 dark:text-slate-300">Servidor IMAP</label>
<input type="text" id="host" value={host} onChange={(e) => setHost(e.target.value)} required className="mt-1 block w-full input" placeholder="imap.dominio.com" />
</div>
<div>
<label htmlFor="port" className="block text-sm font-medium text-slate-700 dark:text-slate-300">Porta</label>
<input type="number" id="port" value={port} onChange={(e) => setPort(parseInt(e.target.value))} required className="mt-1 block w-full input" />
</div>
</div>
<div className="flex items-center">
<input type="checkbox" id="secure" checked={secure} onChange={(e) => setSecure(e.target.checked)} className="h-4 w-4 rounded border-slate-300 text-sky-600 focus:ring-sky-500" />
<label htmlFor="secure" className="ml-2 block text-sm text-slate-900 dark:text-slate-300">Usar conexão segura (SSL/TLS)</label>
</div>
{testMessage && (
<div className={`text-sm p-3 rounded-md ${testStatus === 'success' ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}`}>
{testMessage}
</div>
)}
<div className="flex items-center justify-between gap-3 pt-2">
<button type="button" onClick={onBack} className="btn-secondary">Voltar</button>
<div className="flex items-center gap-3">
<button type="button" onClick={handleTestConnection} disabled={testStatus === 'testing'} className="btn-secondary flex items-center gap-2">
{testStatus === 'testing' && <Spinner className="w-4 h-4" />}
{testStatus === 'testing' ? 'Testando...' : 'Testar Conexão'}
</button>
<button type="submit" disabled={testStatus !== 'success'} className="btn-primary disabled:opacity-50 disabled:cursor-not-allowed">
Adicionar Conta
</button>
</div>
</div>
</form>
);
};
export default AddImapAccountForm;

View File

@@ -0,0 +1,8 @@
/**
* AutoCleanManager.tsx
*
* @author Descomplicar® Crescimento Digital
* @link https://descomplicar.pt
* @copyright 2025 Descomplicar®
*/

View File

@@ -0,0 +1,79 @@
/**
* ConfirmationModal.tsx
*
* @author Descomplicar® Crescimento Digital
* @link https://descomplicar.pt
* @copyright 2025 Descomplicar®
*/
import React from 'react';
interface ConfirmationModalProps {
isOpen: boolean;
onClose: () => void;
onConfirm: () => void;
categoryName: string;
emailCount: number;
}
const ConfirmationModal: React.FC<ConfirmationModalProps> = ({
isOpen,
onClose,
onConfirm,
categoryName,
emailCount,
}) => {
if (!isOpen) return null;
return (
<div
className="fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center p-4"
aria-modal="true"
role="dialog"
>
<div className="bg-white dark:bg-slate-800 rounded-xl shadow-2xl p-6 w-full max-w-md transform transition-all"
role="document"
>
<div className="text-center">
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100 dark:bg-red-900/50 mb-4">
<svg className="h-6 w-6 text-red-600 dark:text-red-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
</svg>
</div>
<h3 className="text-lg font-semibold text-slate-900 dark:text-white" id="modal-title">
Confirmar Limpeza
</h3>
<div className="mt-2">
<p className="text-sm text-slate-500 dark:text-slate-400">
Você tem certeza que deseja remover permanentemente{' '}
<span className="font-bold text-slate-700 dark:text-slate-200">{emailCount} e-mails</span> da categoria{' '}
<span className="font-bold text-slate-700 dark:text-slate-200">{categoryName}</span>?
</p>
<p className="text-xs text-slate-400 dark:text-slate-500 mt-1">
Esta ação não pode ser desfeita.
</p>
</div>
</div>
<div className="mt-6 flex justify-center gap-4">
<button
type="button"
className="w-full justify-center rounded-md border border-slate-300 dark:border-slate-600 px-4 py-2 bg-white dark:bg-slate-800 text-base font-medium text-slate-700 dark:text-slate-200 shadow-sm hover:bg-slate-50 dark:hover:bg-slate-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-sky-500 dark:focus:ring-offset-slate-900 sm:text-sm"
onClick={onClose}
>
Cancelar
</button>
<button
type="button"
className="w-full justify-center rounded-md border border-transparent px-4 py-2 bg-red-600 text-base font-medium text-white shadow-sm hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 dark:focus:ring-offset-slate-900 sm:text-sm"
onClick={onConfirm}
>
Sim, Limpar
</button>
</div>
</div>
</div>
);
};
export default ConfirmationModal;

View File

@@ -0,0 +1,208 @@
/**
* Dashboard.tsx
*
* @author Descomplicar® Crescimento Digital
* @link https://descomplicar.pt
* @copyright 2025 Descomplicar®
*/
import React, { useState, useCallback } from 'react';
import { mockEmails } from '../data/mockEmails';
import { classifyEmails } from '../services/geminiService';
import { deleteTicketsFromSendersInPerfex } from '../services/perfexCrmService';
import type { CategorizedEmails, CrmSettings } from '../types';
import { EmailCategory } from '../types';
import EmailCategoryCard from './EmailCategoryCard';
import Spinner from './Spinner';
import ConfirmationModal from './ConfirmationModal';
interface DashboardProps {
crmSettings: CrmSettings;
isAutoCleanEnabled: boolean;
apiKey: string;
}
const Dashboard: React.FC<DashboardProps> = ({ crmSettings, isAutoCleanEnabled, apiKey }) => {
const [isScanning, setIsScanning] = useState(false);
const [categorizedEmails, setCategorizedEmails] = useState<CategorizedEmails | null>(null);
const [error, setError] = useState<string | null>(null);
const [isConfirmModalOpen, setIsConfirmModalOpen] = useState(false);
const [categoryToDelete, setCategoryToDelete] = useState<{name: string; count: number} | null>(null);
const [ticketDeletionStatus, setTicketDeletionStatus] = useState<{ message: string; type: 'success' | 'error' } | null>(null);
const handleScanInbox = useCallback(async () => {
setCategorizedEmails(null);
setIsScanning(true);
setError(null);
setTicketDeletionStatus(null);
try {
const result = await classifyEmails(mockEmails, apiKey);
setCategorizedEmails(result);
if (crmSettings.autoDeleteTickets && crmSettings.url && crmSettings.token) {
const spamAndNotificationEmails = [
...(result[EmailCategory.SPAM]?.emails || []),
...(result[EmailCategory.NOTIFICATIONS]?.emails || [])
];
const senderEmails = [...new Set(spamAndNotificationEmails.map(email => email.sender))];
if (senderEmails.length > 0) {
try {
const deletedCount = await deleteTicketsFromSendersInPerfex(crmSettings, senderEmails);
if (deletedCount > 0) {
setTicketDeletionStatus({ message: `${deletedCount} tickets de spam/notificações foram excluídos do CRM.`, type: 'success' });
}
} catch (err) {
setTicketDeletionStatus({ message: (err as Error).message, type: 'error' });
}
}
}
} catch (err) {
console.error(err);
setError('Falha ao analisar a caixa de entrada. A chave de API do Gemini pode estar ausente ou inválida.');
} finally {
setIsScanning(false);
}
}, [crmSettings, apiKey]);
const handleClearCategory = (category: string, emailCount: number) => {
setCategoryToDelete({ name: category, count: emailCount });
setIsConfirmModalOpen(true);
};
const confirmClearCategory = () => {
if(!categorizedEmails || !categoryToDelete) return;
const newCategories = { ...categorizedEmails };
delete newCategories[categoryToDelete.name];
setCategorizedEmails(newCategories);
setIsConfirmModalOpen(false);
setCategoryToDelete(null);
};
const getGreeting = () => {
const hour = new Date().getHours();
if (hour < 12) return 'Bom dia';
if (hour < 18) return 'Boa tarde';
return 'Boa noite';
}
const TicketDeletionToast = () => {
if (!ticketDeletionStatus) return null;
const isSuccess = ticketDeletionStatus.type === 'success';
return (
<div className={`${isSuccess ? 'bg-green-100 dark:bg-green-900/50 border-green-500 text-green-700 dark:text-green-300' : 'bg-red-100 dark:bg-red-900/50 border-red-500 text-red-700 dark:text-red-300'} border-l-4 p-4 rounded-md flex justify-between items-center`} role="alert">
<p>{ticketDeletionStatus.message}</p>
<button onClick={() => setTicketDeletionStatus(null)} className="p-1 rounded-full hover:bg-black/10">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-5 h-5"><path strokeLinecap="round" strokeLinejoin="round" d="M6 18 18 6M6 6l12 12" /></svg>
</button>
</div>
);
};
return (
<>
<div className="space-y-6">
<div>
<h2 className="text-2xl font-bold text-slate-800 dark:text-white">{getGreeting()}, Usuário!</h2>
<p className="text-slate-500 dark:text-slate-400">
{isAutoCleanEnabled
? 'A limpeza automática está ativa e a ser executada no servidor.'
: 'Vamos organizar sua caixa de entrada.'}
</p>
</div>
<TicketDeletionToast />
{isAutoCleanEnabled ? (
<div className="p-4 bg-sky-50 dark:bg-sky-900/50 rounded-xl border border-sky-200 dark:border-sky-800 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
<div className="text-sm">
<p className="font-semibold text-sky-600 dark:text-sky-300">Automação Ativa</p>
<p className="text-slate-600 dark:text-slate-300 mt-1">
Sua caixa de entrada é verificada em segundo plano a cada 2 horas.
</p>
</div>
<button
onClick={() => handleScanInbox()}
disabled={isScanning}
className="bg-sky-500 text-white font-semibold py-2 px-5 rounded-lg hover:bg-sky-600 transition-colors disabled:bg-sky-300 disabled:cursor-not-allowed flex items-center gap-2 flex-shrink-0"
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={`w-5 h-5 ${isScanning ? 'animate-spin' : ''}`}>
<path strokeLinecap="round" strokeLinejoin="round" d="M16.023 9.348h4.992v-.001a7.5 7.5 0 0 1-1.06 3.58a7.5 7.5 0 0 1-4.225 3.58a7.5 7.5 0 0 1-5.232 0a7.5 7.5 0 0 1-4.225-3.58a7.5 7.5 0 0 1-1.06-3.58m7.5 0v-4.5m0 4.5v4.5m0-4.5h4.5m-4.5 0h-4.5" />
</svg>
{isScanning ? 'Analisando...' : 'Forçar Análise Agora'}
</button>
</div>
) : (
<div className="p-6 bg-white dark:bg-slate-800 rounded-xl shadow-sm border border-slate-200 dark:border-slate-700 flex flex-col sm:flex-row items-center justify-between gap-4">
<div>
<h3 className="text-lg font-semibold text-slate-800 dark:text-white">Pronto para começar?</h3>
<p className="text-slate-500 dark:text-slate-400 mt-1">Clique para que a IA analise e organize sua caixa de entrada.</p>
</div>
<button
onClick={() => handleScanInbox()}
disabled={isScanning}
className="bg-sky-500 text-white font-semibold py-3 px-6 rounded-lg hover:bg-sky-600 transition-colors disabled:bg-sky-300 flex items-center gap-2 flex-shrink-0"
>
{isScanning ? 'Analisando...' : 'Analisar Caixa de Entrada'}
</button>
</div>
)}
{isScanning && !categorizedEmails &&(
<div className="flex flex-col items-center justify-center p-10 bg-white dark:bg-slate-800 rounded-xl shadow-sm border border-slate-200 dark:border-slate-700">
<Spinner />
<p className="mt-4 text-slate-600 dark:text-slate-300 animate-pulse">A IA está analisando seus e-mails...</p>
</div>
)}
{error && (
<div className="bg-red-100 dark:bg-red-900/50 border-l-4 border-red-500 text-red-700 dark:text-red-300 p-4 rounded-md" role="alert">
<p className="font-bold">Oops! Algo deu errado.</p>
<p>{error}</p>
</div>
)}
{categorizedEmails && Object.keys(categorizedEmails).length > 0 && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{Object.entries(categorizedEmails).map(([category, data]) => (
<EmailCategoryCard
key={category}
category={category}
summary={data.summary}
emails={data.emails}
onClear={() => handleClearCategory(category, data.emails.length)}
/>
))}
</div>
)}
{categorizedEmails && Object.keys(categorizedEmails).length === 0 && !isScanning && (
<div className="text-center p-10 bg-white dark:bg-slate-800 rounded-xl shadow-sm border border-slate-200 dark:border-slate-700">
<div className="mx-auto w-16 h-16 bg-green-100 dark:bg-green-900/50 rounded-full flex items-center justify-center mb-4">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-8 h-8 text-green-500">
<path strokeLinecap="round" strokeLinejoin="round" d="m4.5 12.75 6 6 9-13.5" />
</svg>
</div>
<h3 className="text-lg font-semibold text-slate-800 dark:text-white">Caixa de entrada limpa!</h3>
<p className="text-slate-500 dark:text-slate-400 mt-1 mb-6">Você limpou todos os e-mails acionáveis.</p>
</div>
)}
</div>
<ConfirmationModal
isOpen={isConfirmModalOpen}
onClose={() => setIsConfirmModalOpen(false)}
onConfirm={confirmClearCategory}
categoryName={categoryToDelete?.name || ''}
emailCount={categoryToDelete?.count || 0}
/>
</>
);
};
export default Dashboard;

View File

@@ -0,0 +1,85 @@
/**
* EmailCategoryCard.tsx
*
* @author Descomplicar® Crescimento Digital
* @link https://descomplicar.pt
* @copyright 2025 Descomplicar®
*/
import React, { useState, useEffect } from 'react';
import type { Email } from '../types';
import { EmailCategory } from '../types';
interface EmailCategoryCardProps {
category: string;
summary: string;
emails: Email[];
onClear: () => void;
}
const categoryStyles: { [key in EmailCategory]?: { icon: string; bg: string; text: string; } } = {
[EmailCategory.PROMOTIONS]: { icon: '🛍️', bg: 'bg-indigo-100 dark:bg-indigo-900/50', text: 'text-indigo-600 dark:text-indigo-300' },
[EmailCategory.NOTIFICATIONS]: { icon: '🔔', bg: 'bg-amber-100 dark:bg-amber-900/50', text: 'text-amber-600 dark:text-amber-300' },
[EmailCategory.NEWSLETTERS]: { icon: '📰', bg: 'bg-cyan-100 dark:bg-cyan-900/50', text: 'text-cyan-600 dark:text-cyan-300' },
[EmailCategory.SPAM]: { icon: '🗑️', bg: 'bg-red-100 dark:bg-red-900/50', text: 'text-red-600 dark:text-red-300' },
};
const EmailCategoryCard: React.FC<EmailCategoryCardProps> = ({ category, summary, emails, onClear }) => {
const [isExpanded, setIsExpanded] = useState(false);
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
const timer = setTimeout(() => setIsVisible(true), 10);
return () => clearTimeout(timer);
}, []);
const styles = categoryStyles[category as EmailCategory] || { icon: '✉️', bg: 'bg-slate-100 dark:bg-slate-700', text: 'text-slate-600 dark:text-slate-300' };
if (emails.length === 0) return null;
return (
<div className={`bg-white dark:bg-slate-800 rounded-xl shadow-md border border-slate-200 dark:border-slate-700 overflow-hidden transition-all duration-500 hover:shadow-lg hover:-translate-y-1 ${isVisible ? 'opacity-100' : 'opacity-0'}`}>
<div className="p-5">
<div className="flex items-start justify-between">
<div>
<div className="flex items-center gap-3">
<span className={`text-2xl ${styles.bg} p-2 rounded-md`}>{styles.icon}</span>
<h3 className="text-lg font-bold text-slate-800 dark:text-white capitalize">{category.toLowerCase()}</h3>
</div>
<p className="text-slate-500 dark:text-slate-400 text-sm mt-2">{summary}</p>
</div>
<span className={`${styles.bg} ${styles.text} text-sm font-bold px-3 py-1 rounded-full`}>
{emails.length}
</span>
</div>
<div className="mt-4">
<button onClick={onClear} className="w-full bg-red-500 text-white font-semibold py-2 px-4 rounded-lg hover:bg-red-600 transition-colors flex items-center justify-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-5 h-5">
<path strokeLinecap="round" strokeLinejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.134-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.067-2.09 1.02-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
</svg>
Limpar {emails.length} e-mails
</button>
<button onClick={() => setIsExpanded(!isExpanded)} className="w-full mt-2 text-sm text-slate-500 dark:text-slate-400 hover:text-sky-500 dark:hover:text-sky-400">
{isExpanded ? 'Ocultar e-mails' : 'Rever e-mails'}
</button>
</div>
</div>
{isExpanded && (
<div className="bg-slate-50 dark:bg-slate-800/50 p-4 border-t border-slate-200 dark:border-slate-700 max-h-60 overflow-y-auto">
<ul className="space-y-3">
{emails.map(email => (
<li key={email.id} className="text-sm p-2 rounded-md bg-white dark:bg-slate-700/50">
<p className="font-semibold text-slate-700 dark:text-slate-200 truncate">{email.sender}</p>
<p className="text-slate-500 dark:text-slate-400 truncate">{email.subject}</p>
</li>
))}
</ul>
</div>
)}
</div>
);
};
export default EmailCategoryCard;

View File

@@ -0,0 +1,55 @@
/**
* Header.tsx
*
* @author Descomplicar® Crescimento Digital
* @link https://descomplicar.pt
* @copyright 2025 Descomplicar®
*/
import React from 'react';
import { MailIcon } from './icons/MailIcon';
interface HeaderProps {
theme: string;
toggleTheme: () => void;
}
const Header: React.FC<HeaderProps> = ({ theme, toggleTheme }) => {
return (
<header className="bg-white/80 dark:bg-slate-800/80 backdrop-blur-sm shadow-sm p-4 flex justify-between items-center border-b border-slate-200 dark:border-slate-700 sticky top-0 z-10">
<div className="flex items-center gap-3">
<div className="bg-sky-500 p-2 rounded-lg">
<MailIcon className="w-6 h-6 text-white" />
</div>
<h1 className="text-xl font-bold text-slate-800 dark:text-white">Limpador de E-mail com IA</h1>
</div>
<div className="flex items-center gap-4">
<button
onClick={toggleTheme}
className="p-2 rounded-full hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors"
aria-label={theme === 'dark' ? 'Ativar modo claro' : 'Ativar modo escuro'}
>
{theme === 'dark' ? (
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 text-amber-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
) : (
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 text-slate-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
</svg>
)}
</button>
<button className="p-2 rounded-full hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 text-slate-500 dark:text-slate-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
</svg>
</button>
<img src="https://i.pravatar.cc/40?img=3" alt="Avatar do usuário" className="w-10 h-10 rounded-full" />
</div>
</header>
);
};
export default Header;

View File

@@ -0,0 +1,8 @@
/**
* RegisterInvoicesModal.tsx
*
* @author Descomplicar® Crescimento Digital
* @link https://descomplicar.pt
* @copyright 2025 Descomplicar®
*/

View File

@@ -0,0 +1,195 @@
/**
* Settings.tsx
*
* @author Descomplicar® Crescimento Digital
* @link https://descomplicar.pt
* @copyright 2025 Descomplicar®
*/
import React, { useState, useEffect } from 'react';
import type { CrmSettings } from '../types';
interface SettingsProps {
crmSettings: CrmSettings;
onSaveCrmSettings: (settings: CrmSettings) => void;
isAutoCleanEnabled: boolean;
setAutoCleanEnabled: (enabled: boolean) => void;
apiKey: string;
onSaveApiKey: (key: string) => void;
}
const Settings: React.FC<SettingsProps> = ({
crmSettings,
onSaveCrmSettings,
isAutoCleanEnabled,
setAutoCleanEnabled,
apiKey,
onSaveApiKey
}) => {
const [currentCrmSettings, setCurrentCrmSettings] = useState<CrmSettings>(crmSettings);
const [currentApiKey, setCurrentApiKey] = useState<string>(apiKey);
const [showSuccess, setShowSuccess] = useState(false);
useEffect(() => {
setCurrentCrmSettings(crmSettings);
}, [crmSettings]);
useEffect(() => {
setCurrentApiKey(apiKey);
}, [apiKey]);
const handleCrmChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value, type, checked } = e.target;
setCurrentCrmSettings(prev => ({
...prev,
[name]: type === 'checkbox' ? checked : value
}));
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onSaveCrmSettings(currentCrmSettings);
onSaveApiKey(currentApiKey);
setShowSuccess(true);
setTimeout(() => setShowSuccess(false), 3000);
};
return (
<div className="space-y-8 max-w-3xl mx-auto">
<div>
<h2 className="text-2xl font-bold text-slate-800 dark:text-white">Configurações</h2>
<p className="text-slate-500 dark:text-slate-400">Gerencie as integrações e preferências da aplicação.</p>
</div>
<div className="bg-white dark:bg-slate-800 rounded-xl shadow-md border border-slate-200 dark:border-slate-700">
<div className="p-6">
<h3 className="text-lg font-semibold text-slate-800 dark:text-white">Limpeza Automática (Backend)</h3>
<p className="text-slate-500 dark:text-slate-400 text-sm mt-1">
Ative para que um processo no servidor (Cloud Run) analise seus e-mails a cada 2 horas, mesmo com o navegador fechado.
</p>
</div>
<div className="bg-slate-50 dark:bg-slate-800/50 p-6 border-t border-slate-200 dark:border-slate-700 rounded-b-xl">
<div className="flex items-center justify-between">
<span className="font-medium text-slate-700 dark:text-slate-200">
{isAutoCleanEnabled ? 'Automação Ativada' : 'Automação Desativada'}
</span>
<button
onClick={() => setAutoCleanEnabled(!isAutoCleanEnabled)}
className={`${
isAutoCleanEnabled ? 'bg-sky-500' : 'bg-slate-300 dark:bg-slate-600'
} relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-sky-500 focus:ring-offset-2 dark:ring-offset-slate-800`}
role="switch"
aria-checked={isAutoCleanEnabled}
>
<span
className={`${
isAutoCleanEnabled ? 'translate-x-5' : 'translate-x-0'
} pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out`}
/>
</button>
</div>
<p className="text-xs text-slate-500 dark:text-slate-400 mt-3">
<strong>Nota:</strong> Para a automação funcionar, as chaves de API abaixo devem ser configuradas como **variáveis de ambiente** no seu serviço Cloud Run. Consulte o arquivo `README.md`.
</p>
</div>
</div>
<form onSubmit={handleSubmit}>
<div className="bg-white dark:bg-slate-800 rounded-xl shadow-md border border-slate-200 dark:border-slate-700 mb-8">
<div className="p-6 space-y-6">
<div>
<h3 className="text-lg font-semibold text-slate-800 dark:text-white">Chave da API do Google Gemini</h3>
<p className="text-slate-500 dark:text-slate-400 text-sm mt-1">Insira sua chave para habilitar a análise com IA nas verificações manuais.</p>
</div>
<div>
<label htmlFor="apiKey" className="block text-sm font-medium text-slate-700 dark:text-slate-300">
Sua Chave API
</label>
<input
type="password"
name="apiKey"
id="apiKey"
value={currentApiKey}
onChange={(e) => setCurrentApiKey(e.target.value)}
className="mt-1 block w-full px-3 py-2 bg-white dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-md shadow-sm placeholder-slate-400 focus:outline-none focus:ring-sky-500 focus:border-sky-500 sm:text-sm"
placeholder="Cole sua chave da API do Gemini aqui"
/>
</div>
</div>
</div>
<div className="bg-white dark:bg-slate-800 rounded-xl shadow-md border border-slate-200 dark:border-slate-700">
<div className="p-6 space-y-6">
<div>
<h3 className="text-lg font-semibold text-slate-800 dark:text-white">Integração com Perfex CRM</h3>
<p className="text-slate-500 dark:text-slate-400 text-sm mt-1">Conecte-se à sua instância do Perfex CRM para automatizar tarefas.</p>
</div>
<div className="space-y-4">
<div>
<label htmlFor="url" className="block text-sm font-medium text-slate-700 dark:text-slate-300">
URL da API do Perfex CRM
</label>
<input
type="url"
name="url"
id="url"
value={currentCrmSettings.url}
onChange={handleCrmChange}
className="mt-1 block w-full px-3 py-2 bg-white dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-md shadow-sm placeholder-slate-400 focus:outline-none focus:ring-sky-500 focus:border-sky-500 sm:text-sm"
placeholder="https://seu-crm.com"
/>
</div>
<div>
<label htmlFor="token" className="block text-sm font-medium text-slate-700 dark:text-slate-300">
Token da API
</label>
<input
type="password"
name="token"
id="token"
value={currentCrmSettings.token}
onChange={handleCrmChange}
className="mt-1 block w-full px-3 py-2 bg-white dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-md shadow-sm placeholder-slate-400 focus:outline-none focus:ring-sky-500 focus:border-sky-500 sm:text-sm"
placeholder="Seu token de API seguro"
/>
</div>
<div className="relative flex items-start">
<div className="flex h-5 items-center">
<input
id="autoDeleteTickets"
name="autoDeleteTickets"
type="checkbox"
checked={currentCrmSettings.autoDeleteTickets}
onChange={handleCrmChange}
className="h-4 w-4 rounded border-slate-300 text-sky-600 focus:ring-sky-500"
/>
</div>
<div className="ml-3 text-sm">
<label htmlFor="autoDeleteTickets" className="font-medium text-slate-700 dark:text-slate-200">
Excluir Tickets de Spam/Notificações
</label>
<p className="text-slate-500 dark:text-slate-400">Excluir automaticamente tickets do CRM criados por e-mails de spam ou notificações.</p>
</div>
</div>
</div>
</div>
<div className="bg-slate-50 dark:bg-slate-800/50 p-4 border-t border-slate-200 dark:border-slate-700 rounded-b-xl flex items-center justify-end gap-4">
{showSuccess && <p className="text-sm text-green-600 dark:text-green-400 animate-pulse">Configurações salvas com sucesso!</p>}
<button
type="submit"
className="bg-sky-500 text-white font-semibold py-2 px-5 rounded-lg hover:bg-sky-600 transition-colors"
>
Salvar Todas as Configurações
</button>
</div>
</div>
</form>
</div>
);
};
export default Settings;

View File

@@ -0,0 +1,56 @@
/**
* Sidebar.tsx
*
* @author Descomplicar® Crescimento Digital
* @link https://descomplicar.pt
* @copyright 2025 Descomplicar®
*/
import React from 'react';
import { DashboardIcon } from './icons/DashboardIcon';
import { SettingsIcon } from './icons/SettingsIcon';
import { AccountIcon } from './icons/AccountIcon';
import { HelpIcon } from './icons/HelpIcon';
interface SidebarProps {
activeView: string;
setActiveView: (view: string) => void;
}
const Sidebar: React.FC<SidebarProps> = ({ activeView, setActiveView }) => {
const navItems = [
{ id: 'dashboard', name: 'Dashboard', icon: <DashboardIcon className="w-5 h-5" /> },
{ id: 'contas', name: 'Contas', icon: <AccountIcon className="w-5 h-5" /> },
{ id: 'configuracoes', name: 'Configurações', icon: <SettingsIcon className="w-5 h-5" /> },
{ id: 'ajuda', name: 'Ajuda', icon: <HelpIcon className="w-5 h-5" /> },
];
return (
<aside className="w-64 bg-white dark:bg-slate-800/50 p-4 border-r border-slate-200 dark:border-slate-700 hidden md:block">
<nav className="flex flex-col gap-2">
{navItems.map(item => (
<a
key={item.id}
href="#"
onClick={(e) => {
e.preventDefault();
setActiveView(item.id);
}}
className={`flex items-center gap-3 px-4 py-2.5 rounded-lg text-sm font-medium transition-colors ${
activeView === item.id
? 'bg-sky-500 text-white'
: 'text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700'
}`}
>
{item.icon}
<span>{item.name}</span>
</a>
))}
</nav>
</aside>
);
};
export default Sidebar;

View File

@@ -0,0 +1,18 @@
/**
* Spinner.tsx
*
* @author Descomplicar® Crescimento Digital
* @link https://descomplicar.pt
* @copyright 2025 Descomplicar®
*/
import React from 'react';
const Spinner: React.FC = () => {
return (
<div className="w-12 h-12 border-4 border-t-4 border-slate-200 dark:border-slate-600 border-t-sky-500 rounded-full animate-spin"></div>
);
};
export default Spinner;

View File

@@ -0,0 +1,16 @@
/**
* AccountIcon.tsx
*
* @author Descomplicar® Crescimento Digital
* @link https://descomplicar.pt
* @copyright 2025 Descomplicar®
*/
import React from 'react';
export const AccountIcon: 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 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z" />
</svg>
);

View File

@@ -0,0 +1,16 @@
/**
* DashboardIcon.tsx
*
* @author Descomplicar® Crescimento Digital
* @link https://descomplicar.pt
* @copyright 2025 Descomplicar®
*/
import React from 'react';
export const DashboardIcon: 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.25 12 8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h7.5" />
</svg>
);

View File

@@ -0,0 +1,21 @@
/**
* GmailIcon.tsx
*
* @author Descomplicar® Crescimento Digital
* @link https://descomplicar.pt
* @copyright 2025 Descomplicar®
*/
import React from 'react';
export const GmailIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" {...props}>
<path d="M22 6.5H2V17.5H22V6.5Z" fill="#4285F4"/>
<path d="M2.22217 6.64185L12 14.5L21.7778 6.64185C21.6163 6.55198 21.4173 6.5 21.2001 6.5H2.80006C2.5828 6.5 2.38378 6.55198 2.22217 6.64185Z" fill="#34A853"/>
<path d="M2 17.5L9.5 11.5L12 13.5L14.5 11.5L22 17.5V6.5L12 14.5L2 6.5V17.5Z" fill="#EA4335"/>
<path d="M2 17.5H22V16.5L12 8.5L2 16.5V17.5Z" fill="#FBBC05"/>
<path d="M22 6.5V17.5L14.5 11.5L22 6.5Z" fill="#C5221F"/>
<path d="M2 6.5V17.5L9.5 11.5L2 6.5Z" fill="#1E8E3E"/>
</svg>
);

View File

@@ -0,0 +1,16 @@
/**
* HelpIcon.tsx
*
* @author Descomplicar® Crescimento Digital
* @link https://descomplicar.pt
* @copyright 2025 Descomplicar®
*/
import React from 'react';
export const HelpIcon: 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.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 5.25h.008v.008H12v-.008Z" />
</svg>
);

View File

@@ -0,0 +1,15 @@
/**
* ImapIcon.tsx
*
* @author Descomplicar® Crescimento Digital
* @link https://descomplicar.pt
* @copyright 2025 Descomplicar®
*/
import React from 'react';
export const ImapIcon: 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.75 6.75v10.5a2.25 2.25 0 0 1-2.25 2.25h-15a2.25 2.25 0 0 1-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25m19.5 0v.243a2.25 2.25 0 0 1-1.07 1.916l-7.5 4.615a2.25 2.25 0 0 1-2.36 0L3.32 8.91a2.25 2.25 0 0 1-1.07-1.916V6.75" />
</svg>
);

View File

@@ -0,0 +1,16 @@
/**
* MailIcon.tsx
*
* @author Descomplicar® Crescimento Digital
* @link https://descomplicar.pt
* @copyright 2025 Descomplicar®
*/
import React from 'react';
export const MailIcon: 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.75 9v.906a2.25 2.25 0 0 1-1.183 1.981l-6.478 3.488a2.25 2.25 0 0 1-2.18 0l-6.478-3.488A2.25 2.25 0 0 1 2.25 9.906V9m19.5 0a2.25 2.25 0 0 0-2.25-2.25H4.5A2.25 2.25 0 0 0 2.25 9m19.5 0v.906a2.25 2.25 0 0 1-1.183 1.981l-6.478 3.488a2.25 2.25 0 0 1-2.18 0l-6.478-3.488A2.25 2.25 0 0 1 2.25 9.906V9" />
</svg>
);

View File

@@ -0,0 +1,18 @@
/**
* OutlookIcon.tsx
*
* @author Descomplicar® Crescimento Digital
* @link https://descomplicar.pt
* @copyright 2025 Descomplicar®
*/
import React from 'react';
export const OutlookIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
<svg xmlns="http://www.w.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" {...props}>
<path d="M14 3H4C3.44772 3 3 3.44772 3 4V20C3 20.5523 3.44772 21 4 21H14C14.5523 21 15 20.5523 15 20V4C15 3.44772 14.5523 3 14 3Z" fill="#0072C6"/>
<path d="M20.5 7.5H15V16.5H20.5C20.7761 16.5 21 16.2761 21 16V8C21 7.72386 20.7761 7.5 20.5 7.5Z" fill="#0072C6"/>
<path d="M15 8.5V15.5L18.5 12L15 8.5Z" fill="white"/>
</svg>
);

View File

@@ -0,0 +1,16 @@
/**
* SettingsIcon.tsx
*
* @author Descomplicar® Crescimento Digital
* @link https://descomplicar.pt
* @copyright 2025 Descomplicar®
*/
import React from 'react';
export const SettingsIcon: 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="M10.5 6h9.75M10.5 6a1.5 1.5 0 1 1-3 0m3 0a1.5 1.5 0 1 0-3 0M3.75 6H7.5m3 12h9.75m-9.75 0a1.5 1.5 0 0 1-3 0m3 0a1.5 1.5 0 0 0-3 0m-3.75 0H7.5m9-6h3.75m-3.75 0a1.5 1.5 0 0 1-3 0m3 0a1.5 1.5 0 0 0-3 0m-9.75 0h9.75" />
</svg>
);

View File

@@ -0,0 +1,16 @@
/**
* YahooIcon.tsx
*
* @author Descomplicar® Crescimento Digital
* @link https://descomplicar.pt
* @copyright 2025 Descomplicar®
*/
import React from 'react';
export const YahooIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor" {...props}>
<path d="M12.001 2C6.476 2 2 6.476 2 12s4.476 10 10.001 10C17.525 22 22 17.524 22 12S17.525 2 12.001 2zm3.33 13.064h-2.112l-2.08-4.116h-.032l-.56 4.116H8.441L7.1 6.936h2.128l1.6 4.316h.048l2.256-4.316h1.968l-2.32 3.84 2.553 5.288z"/>
</svg>
);