PRIMEIRO ENVIO
This commit is contained in:
@@ -0,0 +1,692 @@
|
||||
// Global state
|
||||
let currentPage = 'dashboard';
|
||||
let cloneHistory = [];
|
||||
let settings = {
|
||||
outputDir: 'M:\\CLONEWEB\\cloned-sites',
|
||||
timeout: 30,
|
||||
retries: 3,
|
||||
excludedDomains: 'facebook.com\ntwitter.com\ninstagram.com\ngoogle-analytics.com\ngoogletagmanager.com'
|
||||
};
|
||||
|
||||
// Initialize app
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
initializeApp();
|
||||
});
|
||||
|
||||
function initializeApp() {
|
||||
setupNavigation();
|
||||
setupThemeToggle();
|
||||
setupForms();
|
||||
checkServiceStatus();
|
||||
loadDashboard();
|
||||
loadHistory();
|
||||
loadSettings();
|
||||
|
||||
// Auto-refresh status every 30 seconds
|
||||
setInterval(checkServiceStatus, 30000);
|
||||
}
|
||||
|
||||
// Navigation
|
||||
function setupNavigation() {
|
||||
const navItems = document.querySelectorAll('.nav-item');
|
||||
|
||||
navItems.forEach(item => {
|
||||
item.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const page = item.getAttribute('data-page');
|
||||
navigateToPage(page);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function navigateToPage(page) {
|
||||
// Update nav items
|
||||
document.querySelectorAll('.nav-item').forEach(item => {
|
||||
item.classList.remove('active');
|
||||
if (item.getAttribute('data-page') === page) {
|
||||
item.classList.add('active');
|
||||
}
|
||||
});
|
||||
|
||||
// Update pages
|
||||
document.querySelectorAll('.page').forEach(p => {
|
||||
p.classList.remove('active');
|
||||
});
|
||||
document.getElementById(page).classList.add('active');
|
||||
|
||||
// Update page title
|
||||
const titles = {
|
||||
'dashboard': 'Dashboard',
|
||||
'new-clone': 'Nova Clonagem',
|
||||
'history': 'Histórico',
|
||||
'settings': 'Configurações'
|
||||
};
|
||||
document.getElementById('pageTitle').textContent = titles[page] || page;
|
||||
|
||||
currentPage = page;
|
||||
|
||||
// Refresh data when navigating
|
||||
if (page === 'dashboard') {
|
||||
loadDashboard();
|
||||
} else if (page === 'history') {
|
||||
loadHistory();
|
||||
}
|
||||
}
|
||||
|
||||
// Theme Toggle
|
||||
function setupThemeToggle() {
|
||||
const themeToggle = document.getElementById('themeToggle');
|
||||
|
||||
// Load saved theme
|
||||
const savedTheme = localStorage.getItem('theme') || 'light';
|
||||
if (savedTheme === 'dark') {
|
||||
document.body.classList.add('dark-theme');
|
||||
themeToggle.checked = true;
|
||||
}
|
||||
|
||||
themeToggle.addEventListener('change', () => {
|
||||
if (themeToggle.checked) {
|
||||
document.body.classList.add('dark-theme');
|
||||
localStorage.setItem('theme', 'dark');
|
||||
} else {
|
||||
document.body.classList.remove('dark-theme');
|
||||
localStorage.setItem('theme', 'light');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Service Status - verifica o serviço WGET na porta 3002
|
||||
async function checkServiceStatus() {
|
||||
const statusIndicator = document.getElementById('serviceStatus');
|
||||
|
||||
try {
|
||||
// Verificar o serviço WGET na porta 3002 (não 3001)
|
||||
const response = await fetch('http://localhost:3002/health', {
|
||||
method: 'GET'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
statusIndicator.className = 'status-indicator online';
|
||||
statusIndicator.querySelector('.status-text').textContent = 'Serviço Online';
|
||||
} else {
|
||||
statusIndicator.className = 'status-indicator offline';
|
||||
statusIndicator.querySelector('.status-text').textContent = 'Serviço Offline';
|
||||
}
|
||||
} catch (error) {
|
||||
statusIndicator.className = 'status-indicator offline';
|
||||
statusIndicator.querySelector('.status-text').textContent = 'Serviço Offline';
|
||||
}
|
||||
}
|
||||
|
||||
// Dashboard
|
||||
function loadDashboard() {
|
||||
// Load from localStorage
|
||||
const history = JSON.parse(localStorage.getItem('cloneHistory') || '[]');
|
||||
cloneHistory = history;
|
||||
|
||||
// Calculate stats
|
||||
const totalSites = history.length;
|
||||
const totalSize = history.reduce((sum, item) => sum + (item.size || 0), 0);
|
||||
const lastClone = history.length > 0 ? history[0].timestamp : null;
|
||||
const successCount = history.filter(item => item.success).length;
|
||||
const successRate = totalSites > 0 ? Math.round((successCount / totalSites) * 100) : 100;
|
||||
|
||||
// Update stats
|
||||
document.getElementById('totalSites').textContent = totalSites;
|
||||
document.getElementById('totalSize').textContent = formatBytes(totalSize);
|
||||
document.getElementById('lastClone').textContent = lastClone ? formatRelativeTime(lastClone) : 'Nunca';
|
||||
document.getElementById('successRate').textContent = successRate + '%';
|
||||
|
||||
// Update recent clones list
|
||||
const recentClonesList = document.getElementById('recentClonesList');
|
||||
|
||||
if (history.length === 0) {
|
||||
recentClonesList.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-inbox"></i>
|
||||
<h3>Nenhuma clonagem ainda</h3>
|
||||
<p>Comece clonando seu primeiro site!</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
const recentClones = history.slice(0, 5);
|
||||
recentClonesList.innerHTML = recentClones.map(clone => `
|
||||
<div class="clone-item">
|
||||
<div class="clone-info">
|
||||
<div class="clone-icon">
|
||||
<i class="fas fa-${clone.success ? 'check' : 'times'}"></i>
|
||||
</div>
|
||||
<div class="clone-details">
|
||||
<h4>${clone.url}</h4>
|
||||
<p>${formatRelativeTime(clone.timestamp)} • ${formatBytes(clone.size || 0)} • ${clone.files || 0} arquivos</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clone-actions">
|
||||
${clone.success ? `
|
||||
<button class="btn btn-secondary btn-sm" onclick="openClonedSite('${clone.directory}')">
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
</button>
|
||||
<button class="btn btn-secondary btn-sm" onclick="openCloneFolder('${clone.directory}')">
|
||||
<i class="fas fa-folder-open"></i>
|
||||
</button>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function refreshDashboard() {
|
||||
loadDashboard();
|
||||
showToast('Dashboard atualizado!', 'success');
|
||||
}
|
||||
|
||||
// Clone Form
|
||||
function setupForms() {
|
||||
// Clone form
|
||||
const cloneForm = document.getElementById('cloneForm');
|
||||
cloneForm.addEventListener('submit', handleCloneSubmit);
|
||||
|
||||
// Depth slider
|
||||
const depthSlider = document.getElementById('depthLevel');
|
||||
const depthValue = document.getElementById('depthValue');
|
||||
|
||||
depthSlider.addEventListener('input', () => {
|
||||
depthValue.textContent = depthSlider.value + ' níveis';
|
||||
});
|
||||
}
|
||||
|
||||
async function handleCloneSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const url = document.getElementById('cloneUrl').value.trim();
|
||||
const depth = parseInt(document.getElementById('depthLevel').value);
|
||||
const includeImages = document.getElementById('includeImages').checked;
|
||||
const includeCSS = document.getElementById('includeCSS').checked;
|
||||
const includeJS = document.getElementById('includeJS').checked;
|
||||
const convertLinks = document.getElementById('convertLinks').checked;
|
||||
const cloneMethod = document.getElementById('cloneMethod').value;
|
||||
|
||||
if (!url) {
|
||||
showToast('Por favor, insira uma URL válida', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate URL
|
||||
try {
|
||||
new URL(url);
|
||||
} catch {
|
||||
showToast('URL inválida. Use o formato: https://exemplo.com', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
await startCloning(url, {
|
||||
depth,
|
||||
includeImages,
|
||||
includeCSS,
|
||||
includeJS,
|
||||
convertLinks,
|
||||
method: cloneMethod
|
||||
});
|
||||
}
|
||||
|
||||
async function startCloning(url, options) {
|
||||
const progressCard = document.getElementById('progressCard');
|
||||
const progressBar = document.getElementById('progressBar');
|
||||
const progressMessage = document.getElementById('progressMessage');
|
||||
const filesDownloaded = document.getElementById('filesDownloaded');
|
||||
const sizeDownloaded = document.getElementById('sizeDownloaded');
|
||||
const timeElapsed = document.getElementById('timeElapsed');
|
||||
|
||||
// Show progress card
|
||||
progressCard.style.display = 'block';
|
||||
progressBar.style.width = '0%';
|
||||
|
||||
const startTime = Date.now();
|
||||
let progress = 0;
|
||||
|
||||
// Simulate progress
|
||||
const progressInterval = setInterval(() => {
|
||||
progress = Math.min(progress + Math.random() * 10, 90);
|
||||
progressBar.style.width = progress + '%';
|
||||
|
||||
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
||||
timeElapsed.textContent = elapsed + 's';
|
||||
}, 1000);
|
||||
|
||||
const messages = options.method === 'smart' ? [
|
||||
'🚀 Iniciando Smart Clone (Puppeteer)...',
|
||||
'⏳ Renderizando página completa...',
|
||||
'🖱️ Simulando interação do usuário...',
|
||||
'📜 Carregando conteúdo sob demanda...',
|
||||
'📦 Capturando recursos da rede...',
|
||||
'🔄 Reescrevendo links dinâmicos...',
|
||||
'✨ Otimizando clone final...'
|
||||
] : [
|
||||
'🚀 Iniciando clonagem com WGET...',
|
||||
'⏳ Sem limite de tempo - pode demorar vários minutos...',
|
||||
'🔍 Analisando estrutura do site...',
|
||||
'📄 Baixando páginas HTML...',
|
||||
'🎨 Baixando CSS e estilos...',
|
||||
'⚡ Baixando JavaScript...',
|
||||
'📦 Baixando imagens e assets...',
|
||||
'🔗 Convertendo links para locais...',
|
||||
'💪 Clonagem profissional em andamento...',
|
||||
'✨ Finalizando clonagem...'
|
||||
];
|
||||
|
||||
let messageIndex = 0;
|
||||
progressMessage.textContent = messages[0];
|
||||
|
||||
const messageInterval = setInterval(() => {
|
||||
messageIndex = (messageIndex + 1) % messages.length;
|
||||
progressMessage.textContent = messages[messageIndex];
|
||||
}, 4000);
|
||||
|
||||
try {
|
||||
let response;
|
||||
|
||||
if (options.method === 'smart') {
|
||||
response = await fetch('/api/smart-clone', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ url })
|
||||
});
|
||||
} else {
|
||||
// WGET logic
|
||||
response = await fetch('/api/wget-clone', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
url,
|
||||
depth: options.depth
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
clearInterval(progressInterval);
|
||||
clearInterval(messageInterval);
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
// Complete progress
|
||||
progressBar.style.width = '100%';
|
||||
progressMessage.textContent = '✅ Clonagem concluída com sucesso!';
|
||||
|
||||
// Update stats
|
||||
// Update stats
|
||||
const files = data.data.resources || data.data.files || 0;
|
||||
const size = data.data.size || 0; // Smart clone might not return size yet
|
||||
|
||||
filesDownloaded.textContent = files;
|
||||
sizeDownloaded.textContent = formatBytes(size);
|
||||
|
||||
// Save to history
|
||||
const cloneRecord = {
|
||||
url,
|
||||
timestamp: new Date().toISOString(),
|
||||
success: true,
|
||||
files: files,
|
||||
size: size,
|
||||
directory: data.data.path || data.data.directory,
|
||||
depth: options.depth,
|
||||
method: options.method
|
||||
};
|
||||
|
||||
saveToHistory(cloneRecord);
|
||||
|
||||
// Show success message
|
||||
setTimeout(() => {
|
||||
showToast('Site clonado com sucesso!', 'success');
|
||||
progressCard.style.display = 'none';
|
||||
|
||||
// Show success dialog
|
||||
showSuccessDialog(cloneRecord);
|
||||
}, 2000);
|
||||
|
||||
} else {
|
||||
throw new Error(data.error || 'Erro desconhecido');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
clearInterval(progressInterval);
|
||||
clearInterval(messageInterval);
|
||||
|
||||
console.error('Clone error:', error);
|
||||
progressCard.style.display = 'none';
|
||||
|
||||
// Save failed attempt to history
|
||||
saveToHistory({
|
||||
url,
|
||||
timestamp: new Date().toISOString(),
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
|
||||
showToast('Erro ao clonar site: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function showSuccessDialog(cloneRecord) {
|
||||
const dialog = document.createElement('div');
|
||||
dialog.className = 'success-dialog';
|
||||
dialog.innerHTML = `
|
||||
<div class="success-dialog-content">
|
||||
<div class="success-icon">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
</div>
|
||||
<h3>Site Clonado com Sucesso!</h3>
|
||||
<p>${cloneRecord.url}</p>
|
||||
<div class="success-stats">
|
||||
<span><i class="fas fa-file"></i> ${cloneRecord.files} arquivos</span>
|
||||
<span><i class="fas fa-hdd"></i> ${formatBytes(cloneRecord.size)}</span>
|
||||
</div>
|
||||
<div class="success-actions">
|
||||
<button class="btn btn-primary" onclick="openClonedSite('${cloneRecord.directory}'); closeSuccessDialog()">
|
||||
<i class="fas fa-external-link-alt"></i> Abrir Site
|
||||
</button>
|
||||
<button class="btn btn-secondary" onclick="openCloneFolder('${cloneRecord.directory}'); closeSuccessDialog()">
|
||||
<i class="fas fa-folder-open"></i> Abrir Pasta
|
||||
</button>
|
||||
<button class="btn btn-secondary" onclick="closeSuccessDialog()">
|
||||
Fechar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(dialog);
|
||||
|
||||
// Add styles
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.success-dialog {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0,0,0,0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10000;
|
||||
animation: fadeIn 0.3s;
|
||||
}
|
||||
.success-dialog-content {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 2rem;
|
||||
max-width: 500px;
|
||||
text-align: center;
|
||||
animation: slideUp 0.3s;
|
||||
}
|
||||
.success-icon {
|
||||
font-size: 4rem;
|
||||
color: var(--success-color);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.success-dialog-content h3 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.success-dialog-content p {
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.success-stats {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
padding: 1rem;
|
||||
background: var(--bg-primary);
|
||||
border-radius: 8px;
|
||||
}
|
||||
.success-stats span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.success-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
@keyframes slideUp {
|
||||
from { transform: translateY(20px); opacity: 0; }
|
||||
to { transform: translateY(0); opacity: 1; }
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
function closeSuccessDialog() {
|
||||
const dialog = document.querySelector('.success-dialog');
|
||||
if (dialog) {
|
||||
dialog.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// History
|
||||
function loadHistory() {
|
||||
const history = JSON.parse(localStorage.getItem('cloneHistory') || '[]');
|
||||
cloneHistory = history;
|
||||
|
||||
const historyList = document.getElementById('historyList');
|
||||
|
||||
if (history.length === 0) {
|
||||
historyList.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-history"></i>
|
||||
<h3>Nenhum histórico</h3>
|
||||
<p>Seus sites clonados aparecerão aqui</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
historyList.innerHTML = history.map((item, index) => `
|
||||
<div class="history-item">
|
||||
<div class="history-info">
|
||||
<h4>${item.url}</h4>
|
||||
<div class="history-meta">
|
||||
<span><i class="fas fa-calendar"></i> ${formatDate(item.timestamp)}</span>
|
||||
<span><i class="fas fa-${item.success ? 'check-circle' : 'times-circle'}"></i> ${item.success ? 'Sucesso' : 'Falhou'}</span>
|
||||
${item.files ? `<span><i class="fas fa-file"></i> ${item.files} arquivos</span>` : ''}
|
||||
${item.size ? `<span><i class="fas fa-hdd"></i> ${formatBytes(item.size)}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="clone-actions">
|
||||
${item.success ? `
|
||||
<button class="btn btn-secondary btn-sm" onclick="openClonedSite('${item.directory}')" title="Abrir Site">
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
</button>
|
||||
<button class="btn btn-secondary btn-sm" onclick="openCloneFolder('${item.directory}')" title="Abrir Pasta">
|
||||
<i class="fas fa-folder-open"></i>
|
||||
</button>
|
||||
<button class="btn btn-primary btn-sm" onclick="recloneSite('${item.url}')" title="Clonar Novamente">
|
||||
<i class="fas fa-redo"></i>
|
||||
</button>
|
||||
` : ''}
|
||||
<button class="btn btn-danger btn-sm" onclick="deleteHistoryItem(${index})" title="Remover">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function refreshHistory() {
|
||||
loadHistory();
|
||||
showToast('Histórico atualizado!', 'success');
|
||||
}
|
||||
|
||||
function clearHistory() {
|
||||
if (confirm('Tem certeza que deseja limpar todo o histórico? Esta ação não pode ser desfeita.')) {
|
||||
localStorage.removeItem('cloneHistory');
|
||||
cloneHistory = [];
|
||||
loadHistory();
|
||||
loadDashboard();
|
||||
showToast('Histórico limpo com sucesso!', 'success');
|
||||
}
|
||||
}
|
||||
|
||||
function deleteHistoryItem(index) {
|
||||
if (confirm('Remover este item do histórico?')) {
|
||||
cloneHistory.splice(index, 1);
|
||||
localStorage.setItem('cloneHistory', JSON.stringify(cloneHistory));
|
||||
loadHistory();
|
||||
loadDashboard();
|
||||
showToast('Item removido!', 'success');
|
||||
}
|
||||
}
|
||||
|
||||
function recloneSite(url) {
|
||||
document.getElementById('cloneUrl').value = url;
|
||||
navigateToPage('new-clone');
|
||||
showToast('URL carregada! Configure as opções e clique em "Iniciar Clonagem"', 'info');
|
||||
}
|
||||
|
||||
function saveToHistory(record) {
|
||||
cloneHistory.unshift(record);
|
||||
// Keep only last 100 records
|
||||
if (cloneHistory.length > 100) {
|
||||
cloneHistory = cloneHistory.slice(0, 100);
|
||||
}
|
||||
localStorage.setItem('cloneHistory', JSON.stringify(cloneHistory));
|
||||
}
|
||||
|
||||
// Settings
|
||||
function loadSettings() {
|
||||
const savedSettings = localStorage.getItem('cloneSettings');
|
||||
if (savedSettings) {
|
||||
settings = JSON.parse(savedSettings);
|
||||
}
|
||||
|
||||
// Populate settings form (if needed)
|
||||
}
|
||||
|
||||
function saveSettings() {
|
||||
localStorage.setItem('cloneSettings', JSON.stringify(settings));
|
||||
showToast('Configurações salvas!', 'success');
|
||||
}
|
||||
|
||||
// Utility Functions
|
||||
function openClonedSite(directory) {
|
||||
// Try to open via server endpoint
|
||||
fetch('http://localhost:8080', {
|
||||
method: 'GET'
|
||||
}).then(response => {
|
||||
if (response.ok) {
|
||||
// Server is running, open the site
|
||||
const siteName = directory.split('\\').pop();
|
||||
window.open(`http://localhost:8080/${siteName}/`, '_blank');
|
||||
} else {
|
||||
showToast('Servidor HTTP não está rodando. Inicie o servidor na porta 8080.', 'error');
|
||||
}
|
||||
}).catch(error => {
|
||||
showToast('Servidor HTTP não está rodando. Inicie o servidor na porta 8080.', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
function openCloneFolder(directory) {
|
||||
showToast('Abrindo pasta: ' + directory, 'info');
|
||||
// In a real app, this would use an API endpoint to open the folder
|
||||
console.log('Open folder:', directory);
|
||||
}
|
||||
|
||||
function showToast(message, type = 'info') {
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast toast-${type}`;
|
||||
toast.innerHTML = `
|
||||
<i class="fas fa-${type === 'success' ? 'check-circle' : type === 'error' ? 'exclamation-circle' : 'info-circle'}"></i>
|
||||
<span>${message}</span>
|
||||
`;
|
||||
|
||||
document.body.appendChild(toast);
|
||||
|
||||
// Add styles if not already added
|
||||
if (!document.getElementById('toast-styles')) {
|
||||
const style = document.createElement('style');
|
||||
style.id = 'toast-styles';
|
||||
style.textContent = `
|
||||
.toast {
|
||||
position: fixed;
|
||||
bottom: 2rem;
|
||||
right: 2rem;
|
||||
background: white;
|
||||
padding: 1rem 1.5rem;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
z-index: 10000;
|
||||
animation: slideInRight 0.3s, slideOutRight 0.3s 2.7s;
|
||||
}
|
||||
.toast-success { border-left: 4px solid var(--success-color); }
|
||||
.toast-error { border-left: 4px solid var(--danger-color); }
|
||||
.toast-info { border-left: 4px solid var(--info-color); }
|
||||
.toast i { font-size: 1.2rem; }
|
||||
.toast-success i { color: var(--success-color); }
|
||||
.toast-error i { color: var(--danger-color); }
|
||||
.toast-info i { color: var(--info-color); }
|
||||
@keyframes slideInRight {
|
||||
from { transform: translateX(400px); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
@keyframes slideOutRight {
|
||||
from { transform: translateX(0); opacity: 1; }
|
||||
to { transform: translateX(400px); opacity: 0; }
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
toast.remove();
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
function formatDate(dateString) {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleString('pt-BR', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
function formatRelativeTime(dateString) {
|
||||
const date = new Date(dateString);
|
||||
const now = new Date();
|
||||
const diff = now - date;
|
||||
|
||||
const seconds = Math.floor(diff / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const days = Math.floor(hours / 24);
|
||||
|
||||
if (days > 0) return days === 1 ? 'Há 1 dia' : `Há ${days} dias`;
|
||||
if (hours > 0) return hours === 1 ? 'Há 1 hora' : `Há ${hours} horas`;
|
||||
if (minutes > 0) return minutes === 1 ? 'Há 1 minuto' : `Há ${minutes} minutos`;
|
||||
return 'Agora mesmo';
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
const urlInput = document.getElementById('urlInput');
|
||||
const startBtn = document.getElementById('startBtn');
|
||||
const heroSection = document.getElementById('hero');
|
||||
const loadingSection = document.getElementById('loading');
|
||||
const resultSection = document.getElementById('result');
|
||||
const terminalLog = document.getElementById('terminalLog');
|
||||
const progressCircle = document.getElementById('progressCircle');
|
||||
|
||||
let clonedData = null;
|
||||
|
||||
startBtn.addEventListener('click', startCloning);
|
||||
urlInput.addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') startCloning();
|
||||
});
|
||||
|
||||
document.getElementById('btnOpenSite').addEventListener('click', () => {
|
||||
if (clonedData && clonedData.data && clonedData.data.path) {
|
||||
// Construct local URL. Server serves /<folderName>/index.html
|
||||
// We know server serves static files from cloned-sites directly if configured?
|
||||
// Wait, server.js serves ../cloned-sites as static.
|
||||
// so path is just /FolderName/index.html
|
||||
const dirName = clonedData.data.path.split('\\').pop().split('/').pop();
|
||||
window.open(`/${dirName}/index.html`, '_blank');
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('btnOpenFolder').addEventListener('click', () => {
|
||||
if (clonedData && clonedData.data && clonedData.data.path) {
|
||||
fetch('/api/open-folder', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ path: clonedData.data.path })
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
async function startCloning() {
|
||||
const url = urlInput.value.trim();
|
||||
if (!url) {
|
||||
showToast('Por favor, insira uma URL válida', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Switch UI
|
||||
heroSection.classList.add('hidden');
|
||||
loadingSection.classList.remove('hidden');
|
||||
|
||||
// Simulate Terminal Logs
|
||||
log('Iniciando puppeteer...');
|
||||
let progress = 0;
|
||||
const progressInterval = setInterval(() => {
|
||||
if (progress < 90) {
|
||||
progress += Math.random() * 5;
|
||||
updateProgress(progress);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
const logMessages = [
|
||||
'Configurando viewport 1920x1080...',
|
||||
'Interceptando requisições de rede...',
|
||||
'Analisando DOM e Shadow DOM...',
|
||||
'Baixando assets estáticos...',
|
||||
'Processando estilos computados...',
|
||||
'Otimizando imagens...',
|
||||
'Verificando integridade...'
|
||||
];
|
||||
|
||||
let msgIndex = 0;
|
||||
const msgInterval = setInterval(() => {
|
||||
if (msgIndex < logMessages.length) {
|
||||
log(logMessages[msgIndex]);
|
||||
msgIndex++;
|
||||
}
|
||||
}, 2500);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/smart-clone', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ url })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
clonedData = data;
|
||||
|
||||
clearInterval(progressInterval);
|
||||
clearInterval(msgInterval);
|
||||
updateProgress(100);
|
||||
|
||||
if (data.success) {
|
||||
log('Clonagem concluída com sucesso!');
|
||||
setTimeout(() => {
|
||||
showResult(data);
|
||||
}, 1000);
|
||||
} else {
|
||||
throw new Error(data.error || 'Erro desconhecido');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
clearInterval(progressInterval);
|
||||
clearInterval(msgInterval);
|
||||
showToast('Erro: ' + error.message, 'error');
|
||||
log('❌ Erro fatal: ' + error.message);
|
||||
setTimeout(() => {
|
||||
heroSection.classList.remove('hidden');
|
||||
loadingSection.classList.add('hidden');
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
function updateProgress(value) {
|
||||
// 283 is the circumference
|
||||
const dashboard = 283;
|
||||
const offset = dashboard - (value / 100) * dashboard;
|
||||
progressCircle.style.strokeDashoffset = offset;
|
||||
}
|
||||
|
||||
function log(msg) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'log-line';
|
||||
div.innerText = `> ${msg}`;
|
||||
terminalLog.appendChild(div);
|
||||
terminalLog.scrollTop = terminalLog.scrollHeight;
|
||||
}
|
||||
|
||||
function showResult(data) {
|
||||
loadingSection.classList.add('hidden');
|
||||
resultSection.classList.remove('hidden');
|
||||
|
||||
// Handle Verification Images
|
||||
const verifyContainer = document.getElementById('verificationContainer');
|
||||
if (data.data.verification) {
|
||||
verifyContainer.style.display = 'block';
|
||||
|
||||
// Paths are relative to site dir.
|
||||
// We need to construct URL to fetch them.
|
||||
const dirName = data.data.path.split('\\').pop().split('/').pop();
|
||||
|
||||
document.getElementById('imgOriginal').src = `/${dirName}/${data.data.verification.original}`;
|
||||
document.getElementById('imgClone').src = `/${dirName}/${data.data.verification.clone}`;
|
||||
} else {
|
||||
verifyContainer.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function showToast(msg, type = 'info') {
|
||||
const toast = document.createElement('div');
|
||||
toast.style.position = 'fixed';
|
||||
toast.style.bottom = '20px';
|
||||
toast.style.right = '20px';
|
||||
toast.style.background = type === 'error' ? '#ef4444' : '#10b981';
|
||||
toast.style.color = 'white';
|
||||
toast.style.padding = '1rem 2rem';
|
||||
toast.style.borderRadius = '8px';
|
||||
toast.style.zIndex = '1000';
|
||||
toast.innerText = msg;
|
||||
document.body.appendChild(toast);
|
||||
setTimeout(() => toast.remove(), 3000);
|
||||
}
|
||||
@@ -0,0 +1,452 @@
|
||||
// Global state
|
||||
let currentResults = null;
|
||||
|
||||
// DOM Elements
|
||||
const crawlerForm = document.getElementById('crawlerForm');
|
||||
const urlInput = document.getElementById('urlInput');
|
||||
const crawlBtn = document.getElementById('crawlBtn');
|
||||
const loadingSection = document.getElementById('loadingSection');
|
||||
const resultsSection = document.getElementById('resultsSection');
|
||||
const errorSection = document.getElementById('errorSection');
|
||||
const crawlerStatus = document.getElementById('crawlerStatus');
|
||||
const exportBtn = document.getElementById('exportBtn');
|
||||
|
||||
// Initialize app
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
checkCrawlerStatus();
|
||||
setupEventListeners();
|
||||
setupTabs();
|
||||
});
|
||||
|
||||
// Check crawler service status
|
||||
async function checkCrawlerStatus() {
|
||||
try {
|
||||
const response = await fetch('/api/health');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status === 'ok') {
|
||||
updateStatusIndicator('online', 'Crawler Online');
|
||||
} else {
|
||||
updateStatusIndicator('offline', 'Crawler Offline');
|
||||
}
|
||||
} catch (error) {
|
||||
updateStatusIndicator('offline', 'Crawler Offline');
|
||||
}
|
||||
}
|
||||
|
||||
// Update status indicator
|
||||
function updateStatusIndicator(status, text) {
|
||||
crawlerStatus.className = `status-indicator ${status}`;
|
||||
crawlerStatus.querySelector('span').textContent = text;
|
||||
}
|
||||
|
||||
// Setup event listeners
|
||||
function setupEventListeners() {
|
||||
crawlerForm.addEventListener('submit', handleCrawlSubmit);
|
||||
exportBtn.addEventListener('click', exportResults);
|
||||
|
||||
// Auto-check status every 30 seconds
|
||||
setInterval(checkCrawlerStatus, 30000);
|
||||
}
|
||||
|
||||
// Setup tabs functionality
|
||||
function setupTabs() {
|
||||
const tabButtons = document.querySelectorAll('.tab-btn');
|
||||
const tabPanes = document.querySelectorAll('.tab-pane');
|
||||
|
||||
tabButtons.forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
const targetTab = button.getAttribute('data-tab');
|
||||
|
||||
// Remove active class from all buttons and panes
|
||||
tabButtons.forEach(btn => btn.classList.remove('active'));
|
||||
tabPanes.forEach(pane => pane.classList.remove('active'));
|
||||
|
||||
// Add active class to clicked button and corresponding pane
|
||||
button.classList.add('active');
|
||||
document.getElementById(targetTab).classList.add('active');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Handle crawl form submission
|
||||
async function handleCrawlSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const url = urlInput.value.trim();
|
||||
if (!url) {
|
||||
showError('Por favor, insira uma URL válida.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate URL format
|
||||
try {
|
||||
new URL(url);
|
||||
} catch {
|
||||
showError('Por favor, insira uma URL válida (ex: https://example.com).');
|
||||
return;
|
||||
}
|
||||
|
||||
await startCrawling(url);
|
||||
}
|
||||
|
||||
// Start crawling process
|
||||
async function startCrawling(url) {
|
||||
showLoading();
|
||||
updateLoadingMessage('🚀 Iniciando clonagem profissional com WGET...');
|
||||
|
||||
try {
|
||||
// SEM TIMEOUT - permite clonagens longas
|
||||
// Removido AbortController para permitir clonagens que demoram mais de 10 minutos
|
||||
|
||||
// Atualizar mensagem periodicamente
|
||||
const messages = [
|
||||
'🔍 Analisando estrutura do site...',
|
||||
'📄 Baixando páginas HTML...',
|
||||
'📦 Baixando imagens e assets...',
|
||||
'🎨 Baixando CSS e fontes...',
|
||||
'⚡ Baixando JavaScript...',
|
||||
'🔗 Convertendo links para locais...',
|
||||
'⏳ Aguarde, clonagem profissional em andamento...'
|
||||
];
|
||||
let messageIndex = 0;
|
||||
const messageInterval = setInterval(() => {
|
||||
messageIndex = (messageIndex + 1) % messages.length;
|
||||
updateLoadingMessage(messages[messageIndex]);
|
||||
}, 4000);
|
||||
|
||||
// Tentar usar wget primeiro
|
||||
let response;
|
||||
try {
|
||||
updateLoadingMessage('🔧 Usando WGET para clonagem profissional...');
|
||||
response = await fetch('/api/wget-clone', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ url })
|
||||
// SEM signal/timeout - permite clonagens longas
|
||||
});
|
||||
} catch (wgetError) {
|
||||
// Se wget falhar, usar crawler normal
|
||||
console.log('WGET não disponível, usando crawler padrão');
|
||||
updateLoadingMessage('🔄 Usando crawler alternativo...');
|
||||
response = await fetch('/api/crawl', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ url })
|
||||
// SEM signal/timeout - permite clonagens longas
|
||||
});
|
||||
}
|
||||
|
||||
// Removido clearTimeout pois não há mais timeout
|
||||
clearInterval(messageInterval);
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
updateLoadingMessage('✅ Processando resultados...');
|
||||
setTimeout(() => {
|
||||
currentResults = data.data;
|
||||
showResults(data.data);
|
||||
}, 1000);
|
||||
} else {
|
||||
throw new Error(data.error || 'Erro desconhecido no crawling');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Crawling error:', error);
|
||||
if (error.name === 'AbortError') {
|
||||
showError('⏱️ Timeout: A clonagem demorou mais de 10 minutos. Tente novamente com um site menor.');
|
||||
} else {
|
||||
showError(error.message || 'Erro ao conectar com o serviço de crawling');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
function showLoading() {
|
||||
hideAllSections();
|
||||
loadingSection.style.display = 'block';
|
||||
}
|
||||
|
||||
// Update loading message
|
||||
function updateLoadingMessage(message) {
|
||||
const loadingMessage = document.getElementById('loadingMessage');
|
||||
if (loadingMessage) {
|
||||
loadingMessage.textContent = message;
|
||||
}
|
||||
}
|
||||
|
||||
// Show results
|
||||
function showResults(data) {
|
||||
hideAllSections();
|
||||
resultsSection.style.display = 'block';
|
||||
|
||||
// Populate stats
|
||||
populateStats(data);
|
||||
|
||||
// Populate overview
|
||||
populateOverview(data);
|
||||
|
||||
// Populate links
|
||||
populateLinks(data.links || []);
|
||||
|
||||
// Populate images
|
||||
populateImages(data.images || []);
|
||||
|
||||
// Populate raw data
|
||||
populateRawData(data);
|
||||
}
|
||||
|
||||
// Populate statistics
|
||||
function populateStats(data) {
|
||||
const statsGrid = document.getElementById('statsGrid');
|
||||
|
||||
const stats = [
|
||||
{
|
||||
label: 'Status Code',
|
||||
value: data.statusCode || 'N/A',
|
||||
icon: 'fas fa-check-circle'
|
||||
},
|
||||
{
|
||||
label: 'Links Encontrados',
|
||||
value: (data.links || []).length,
|
||||
icon: 'fas fa-link'
|
||||
},
|
||||
{
|
||||
label: 'Imagens Encontradas',
|
||||
value: (data.images || []).length,
|
||||
icon: 'fas fa-image'
|
||||
},
|
||||
{
|
||||
label: 'Tamanho do Conteúdo',
|
||||
value: formatBytes(data.contentLength || 0),
|
||||
icon: 'fas fa-file-alt'
|
||||
}
|
||||
];
|
||||
|
||||
statsGrid.innerHTML = stats.map(stat => `
|
||||
<div class="stat-card">
|
||||
<i class="${stat.icon}" style="font-size: 1.5rem; color: #667eea; margin-bottom: 0.5rem;"></i>
|
||||
<span class="stat-value">${stat.value}</span>
|
||||
<div class="stat-label">${stat.label}</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// Populate overview
|
||||
function populateOverview(data) {
|
||||
const overviewContent = document.querySelector('#overview .overview-content');
|
||||
|
||||
const overviewItems = [
|
||||
{ label: 'URL', value: data.url || 'N/A' },
|
||||
{ label: 'Título', value: data.title || 'N/A' },
|
||||
{ label: 'Status Code', value: data.statusCode || 'N/A' },
|
||||
{ label: 'Timestamp', value: formatDate(data.timestamp) },
|
||||
{ label: 'Tamanho do Conteúdo', value: formatBytes(data.contentLength || 0) },
|
||||
{ label: 'Total de Links', value: (data.links || []).length },
|
||||
{ label: 'Total de Imagens', value: (data.images || []).length }
|
||||
];
|
||||
|
||||
// Adicionar informações do clone se disponível
|
||||
if (data.cloneInfo) {
|
||||
overviewItems.push(
|
||||
{ label: 'Clone Status', value: data.cloneInfo.success ? '✅ Sucesso' : '❌ Falhou' },
|
||||
{ label: 'Assets Baixados', value: `${data.cloneInfo.assetsDownloaded}/${data.cloneInfo.totalAssets}` },
|
||||
{ label: 'Diretório Local', value: data.cloneInfo.directory }
|
||||
);
|
||||
}
|
||||
|
||||
overviewContent.innerHTML = overviewItems.map(item => `
|
||||
<div class="overview-item">
|
||||
<span class="label">${item.label}:</span>
|
||||
<span class="value">${item.value}</span>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
// Adicionar botão para abrir pasta do clone
|
||||
if (data.cloneInfo && data.cloneInfo.success) {
|
||||
const cloneButton = document.createElement('div');
|
||||
cloneButton.className = 'clone-actions';
|
||||
cloneButton.innerHTML = `
|
||||
<div style="margin-top: 1rem; padding: 1rem; background: #e8f5e8; border-radius: 8px; border-left: 4px solid #28a745;">
|
||||
<h4 style="color: #155724; margin-bottom: 0.5rem;">🎉 Site Clonado com Sucesso!</h4>
|
||||
<p style="color: #155724; margin-bottom: 1rem;">O site foi clonado e salvo localmente com todos os assets.</p>
|
||||
<p style="color: #155724; font-size: 0.9rem; margin-bottom: 1rem;"><strong>Localização:</strong> ${data.cloneInfo.directory}</p>
|
||||
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
|
||||
<button class="btn btn-success btn-sm" onclick="openCloneFolder('${data.cloneInfo.directory}')">
|
||||
📁 Abrir Pasta
|
||||
</button>
|
||||
<button class="btn btn-primary btn-sm" onclick="openClonedSite('${data.cloneInfo.directory}')">
|
||||
🌐 Abrir Site Local
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
overviewContent.appendChild(cloneButton);
|
||||
}
|
||||
}
|
||||
|
||||
// Populate links
|
||||
function populateLinks(links) {
|
||||
const linksList = document.querySelector('#links .links-list');
|
||||
|
||||
if (links.length === 0) {
|
||||
linksList.innerHTML = '<p style="text-align: center; color: #666; padding: 2rem;">Nenhum link encontrado.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
linksList.innerHTML = links.map(link => `
|
||||
<div class="link-item">
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
<a href="${link}" target="_blank" rel="noopener noreferrer">${link}</a>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// Populate images
|
||||
function populateImages(images) {
|
||||
const imagesGrid = document.querySelector('#images .images-grid');
|
||||
|
||||
if (images.length === 0) {
|
||||
imagesGrid.innerHTML = '<p style="text-align: center; color: #666; padding: 2rem; grid-column: 1 / -1;">Nenhuma imagem encontrada.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
imagesGrid.innerHTML = images.map(imageUrl => `
|
||||
<div class="image-item">
|
||||
<img src="${imageUrl}" alt="Website Image" onerror="this.style.display='none'; this.nextElementSibling.style.display='block';">
|
||||
<div style="display: none; padding: 2rem; color: #666;">
|
||||
<i class="fas fa-image" style="font-size: 2rem; margin-bottom: 1rem;"></i>
|
||||
<p>Imagem não pôde ser carregada</p>
|
||||
</div>
|
||||
<div class="image-url">${imageUrl}</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// Populate raw data
|
||||
function populateRawData(data) {
|
||||
const rawDataElement = document.getElementById('rawData');
|
||||
rawDataElement.textContent = JSON.stringify(data, null, 2);
|
||||
}
|
||||
|
||||
// Show error
|
||||
function showError(message) {
|
||||
hideAllSections();
|
||||
errorSection.style.display = 'block';
|
||||
document.getElementById('errorMessage').textContent = message;
|
||||
}
|
||||
|
||||
// Hide all sections
|
||||
function hideAllSections() {
|
||||
loadingSection.style.display = 'none';
|
||||
resultsSection.style.display = 'none';
|
||||
errorSection.style.display = 'none';
|
||||
}
|
||||
|
||||
// Reset form
|
||||
function resetForm() {
|
||||
hideAllSections();
|
||||
urlInput.value = '';
|
||||
urlInput.focus();
|
||||
currentResults = null;
|
||||
}
|
||||
|
||||
// Export results
|
||||
function exportResults() {
|
||||
if (!currentResults) {
|
||||
alert('Nenhum resultado para exportar.');
|
||||
return;
|
||||
}
|
||||
|
||||
const dataStr = JSON.stringify(currentResults, null, 2);
|
||||
const dataBlob = new Blob([dataStr], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(dataBlob);
|
||||
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `crawl-results-${new Date().toISOString().split('T')[0]}.json`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
function formatBytes(bytes) {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
function formatDate(dateString) {
|
||||
if (!dateString) return 'N/A';
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleString('pt-BR');
|
||||
}
|
||||
|
||||
// Add some sample URLs for quick testing
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const urlInput = document.getElementById('urlInput');
|
||||
|
||||
// Add placeholder with sample URLs
|
||||
const sampleUrls = [
|
||||
'https://example.com',
|
||||
'https://github.com',
|
||||
'https://stackoverflow.com',
|
||||
'https://www.uol.com.br'
|
||||
];
|
||||
|
||||
let currentSample = 0;
|
||||
urlInput.placeholder = sampleUrls[currentSample];
|
||||
|
||||
// Cycle through sample URLs in placeholder
|
||||
setInterval(() => {
|
||||
currentSample = (currentSample + 1) % sampleUrls.length;
|
||||
if (!urlInput.value) {
|
||||
urlInput.placeholder = sampleUrls[currentSample];
|
||||
}
|
||||
}, 3000);
|
||||
});
|
||||
|
||||
// Funções para gerenciar sites clonados
|
||||
function openCloneFolder(directory) {
|
||||
// Tentar abrir a pasta no explorador do Windows
|
||||
fetch('/api/open-folder', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ path: directory })
|
||||
}).then(response => {
|
||||
if (!response.ok) {
|
||||
alert('Não foi possível abrir a pasta automaticamente.\nLocalização: ' + directory);
|
||||
}
|
||||
}).catch(error => {
|
||||
alert('Não foi possível abrir a pasta automaticamente.\nLocalização: ' + directory);
|
||||
});
|
||||
}
|
||||
|
||||
function openClonedSite(directory) {
|
||||
// Tentar abrir o index.html do site clonado
|
||||
fetch('/api/open-site', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ path: directory })
|
||||
}).then(response => {
|
||||
if (!response.ok) {
|
||||
alert('Não foi possível abrir o site automaticamente.\nAbra manualmente: ' + directory + '\\index.html');
|
||||
}
|
||||
}).catch(error => {
|
||||
alert('Não foi possível abrir o site automaticamente.\nAbra manualmente: ' + directory + '\\index.html');
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/* Fallback icons using Unicode symbols */
|
||||
.fas.fa-spider::before { content: "🕷️"; }
|
||||
.fas.fa-circle::before { content: "●"; }
|
||||
.fas.fa-globe::before { content: "🌐"; }
|
||||
.fas.fa-link::before { content: "🔗"; }
|
||||
.fas.fa-chart-line::before { content: "📊"; }
|
||||
.fas.fa-download::before { content: "⬇️"; }
|
||||
.fas.fa-check-circle::before { content: "✅"; }
|
||||
.fas.fa-image::before { content: "🖼️"; }
|
||||
.fas.fa-file-alt::before { content: "📄"; }
|
||||
.fas.fa-external-link-alt::before { content: "↗️"; }
|
||||
.fas.fa-exclamation-triangle::before { content: "⚠️"; }
|
||||
.fas.fa-redo::before { content: "🔄"; }
|
||||
|
||||
/* Remove font-family for fallback */
|
||||
.fas {
|
||||
font-family: inherit !important;
|
||||
font-style: normal;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<circle cx="16" cy="16" r="14" fill="#667eea"/>
|
||||
<text x="16" y="22" text-anchor="middle" fill="white" font-size="16" font-family="Arial">🕷️</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 223 B |
@@ -0,0 +1,297 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-BR">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>CloneWeb Pro - Clonagem Profissional de Sites</title>
|
||||
<link rel="stylesheet" href="styles-new.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<!-- Sidebar -->
|
||||
<aside class="sidebar">
|
||||
<div class="logo">
|
||||
<i class="fas fa-clone"></i>
|
||||
<h1>CloneWeb Pro</h1>
|
||||
</div>
|
||||
|
||||
<nav class="nav-menu">
|
||||
<a href="#" class="nav-item active" data-page="dashboard">
|
||||
<i class="fas fa-home"></i>
|
||||
<span>Dashboard</span>
|
||||
</a>
|
||||
<a href="#" class="nav-item" data-page="new-clone">
|
||||
<i class="fas fa-plus-circle"></i>
|
||||
<span>Nova Clonagem</span>
|
||||
</a>
|
||||
<a href="#" class="nav-item" data-page="history">
|
||||
<i class="fas fa-history"></i>
|
||||
<span>Histórico</span>
|
||||
</a>
|
||||
<a href="#" class="nav-item" data-page="settings">
|
||||
<i class="fas fa-cog"></i>
|
||||
<span>Configurações</span>
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
<div class="theme-toggle">
|
||||
<i class="fas fa-moon"></i>
|
||||
<span>Modo Escuro</span>
|
||||
<label class="switch">
|
||||
<input type="checkbox" id="themeToggle">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="main-content">
|
||||
<!-- Header -->
|
||||
<header class="header">
|
||||
<div class="header-left">
|
||||
<h2 id="pageTitle">Dashboard</h2>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="status-indicator" id="serviceStatus">
|
||||
<span class="status-dot"></span>
|
||||
<span class="status-text">Verificando...</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Dashboard Page -->
|
||||
<div class="page active" id="dashboard">
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
|
||||
<i class="fas fa-globe"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<h3 id="totalSites">0</h3>
|
||||
<p>Sites Clonados</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);">
|
||||
<i class="fas fa-database"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<h3 id="totalSize">0 MB</h3>
|
||||
<p>Espaço Utilizado</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon" style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);">
|
||||
<i class="fas fa-clock"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<h3 id="lastClone">Nunca</h3>
|
||||
<p>Última Clonagem</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon" style="background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<h3 id="successRate">100%</h3>
|
||||
<p>Taxa de Sucesso</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="recent-clones">
|
||||
<div class="section-header">
|
||||
<h3>Sites Clonados Recentemente</h3>
|
||||
<button class="btn btn-secondary btn-sm" onclick="refreshDashboard()">
|
||||
<i class="fas fa-sync-alt"></i> Atualizar
|
||||
</button>
|
||||
</div>
|
||||
<div id="recentClonesList" class="clones-list">
|
||||
<!-- Será preenchido dinamicamente -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- New Clone Page -->
|
||||
<div class="page" id="new-clone">
|
||||
<div class="clone-form-container">
|
||||
<div class="form-card">
|
||||
<h3>Nova Clonagem de Site</h3>
|
||||
<p class="form-description">Insira a URL do site que deseja clonar e configure as opções avançadas.</p>
|
||||
|
||||
<form id="cloneForm">
|
||||
<div class="form-group">
|
||||
<label for="cloneUrl">
|
||||
<i class="fas fa-link"></i> URL do Site
|
||||
</label>
|
||||
<input
|
||||
type="url"
|
||||
id="cloneUrl"
|
||||
class="form-control"
|
||||
placeholder="https://exemplo.com.br"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<i class="fas fa-sliders-h"></i> Configurações Avançadas
|
||||
</label>
|
||||
<div class="advanced-options">
|
||||
<div class="option-row">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="includeImages" checked>
|
||||
<span>Incluir Imagens</span>
|
||||
</label>
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="includeCSS" checked>
|
||||
<span>Incluir CSS</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="option-row">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="includeJS" checked>
|
||||
<span>Incluir JavaScript</span>
|
||||
</label>
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="convertLinks" checked>
|
||||
<span>Converter Links</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="depthLevel">
|
||||
<i class="fas fa-layer-group"></i> Profundidade de Clonagem
|
||||
</label>
|
||||
<div class="slider-container">
|
||||
<input type="range" id="depthLevel" min="1" max="5" value="3" class="slider-input">
|
||||
<span class="slider-value" id="depthValue">3 níveis</span>
|
||||
</div>
|
||||
<small>Controla quantas páginas vinculadas serão clonadas</small>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary btn-lg">
|
||||
<i class="fas fa-download"></i> Iniciar Clonagem
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Progress Card -->
|
||||
<div class="progress-card" id="progressCard" style="display: none;">
|
||||
<h3>Clonagem em Andamento</h3>
|
||||
<div class="progress-info">
|
||||
<div class="progress-status">
|
||||
<i class="fas fa-spinner fa-spin"></i>
|
||||
<span id="progressMessage">Iniciando clonagem...</span>
|
||||
</div>
|
||||
<div class="progress-bar-container">
|
||||
<div class="progress-bar" id="progressBar"></div>
|
||||
</div>
|
||||
<div class="progress-stats">
|
||||
<span><i class="fas fa-file"></i> <span id="filesDownloaded">0</span> arquivos</span>
|
||||
<span><i class="fas fa-hdd"></i> <span id="sizeDownloaded">0 MB</span></span>
|
||||
<span><i class="fas fa-clock"></i> <span id="timeElapsed">0s</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- History Page -->
|
||||
<div class="page" id="history">
|
||||
<div class="section-header">
|
||||
<h3>Histórico de Clonagens</h3>
|
||||
<div class="header-actions">
|
||||
<button class="btn btn-secondary btn-sm" onclick="refreshHistory()">
|
||||
<i class="fas fa-sync-alt"></i> Atualizar
|
||||
</button>
|
||||
<button class="btn btn-danger btn-sm" onclick="clearHistory()">
|
||||
<i class="fas fa-trash"></i> Limpar Histórico
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="history-list" id="historyList">
|
||||
<!-- Será preenchido dinamicamente -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings Page -->
|
||||
<div class="page" id="settings">
|
||||
<div class="settings-container">
|
||||
<div class="settings-section">
|
||||
<h3><i class="fas fa-cog"></i> Configurações Gerais</h3>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<h4>Diretório de Saída</h4>
|
||||
<p>Local onde os sites clonados serão salvos</p>
|
||||
</div>
|
||||
<div class="setting-control">
|
||||
<input type="text" class="form-control" value="M:\CLONEWEB\cloned-sites" readonly>
|
||||
<button class="btn btn-secondary btn-sm">
|
||||
<i class="fas fa-folder-open"></i> Abrir
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<h4>Timeout de Requisição</h4>
|
||||
<p>Tempo máximo de espera por arquivo (segundos)</p>
|
||||
</div>
|
||||
<div class="setting-control">
|
||||
<input type="number" class="form-control" value="30" min="10" max="120">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<h4>Tentativas de Download</h4>
|
||||
<p>Número de tentativas para arquivos que falharem</p>
|
||||
</div>
|
||||
<div class="setting-control">
|
||||
<input type="number" class="form-control" value="3" min="1" max="10">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<h3><i class="fas fa-ban"></i> Exclusões</h3>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<h4>Domínios Excluídos</h4>
|
||||
<p>Domínios que não serão clonados (um por linha)</p>
|
||||
</div>
|
||||
<div class="setting-control">
|
||||
<textarea class="form-control" rows="5" placeholder="facebook.com twitter.com instagram.com"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-actions">
|
||||
<button class="btn btn-primary">
|
||||
<i class="fas fa-save"></i> Salvar Configurações
|
||||
</button>
|
||||
<button class="btn btn-secondary">
|
||||
<i class="fas fa-undo"></i> Restaurar Padrões
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script src="app-new.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,166 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-BR">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' http://localhost:3001; font-src 'self';">
|
||||
<title>CloneWeb - Website Crawler Platform</title>
|
||||
<link rel="icon" type="image/svg+xml" href="favicon.svg">
|
||||
<link rel="stylesheet" href="fallback-icons.css">
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<!-- Header -->
|
||||
<header class="header">
|
||||
<div class="container">
|
||||
<div class="header-content">
|
||||
<div class="logo">
|
||||
<i class="fas fa-spider"></i>
|
||||
<h1>CloneWeb</h1>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<div class="status-indicator" id="crawlerStatus">
|
||||
<i class="fas fa-circle"></i>
|
||||
<span>Verificando...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="main">
|
||||
<div class="container">
|
||||
<!-- Hero Section -->
|
||||
<section class="hero">
|
||||
<h2>Plataforma Inteligente de Crawling</h2>
|
||||
<p>Extraia dados de websites com precisão usando nossa tecnologia avançada de crawling</p>
|
||||
</section>
|
||||
|
||||
<!-- Crawler Form -->
|
||||
<section class="crawler-section">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3><i class="fas fa-globe"></i> Iniciar Crawling</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="crawlerForm">
|
||||
<div class="form-group">
|
||||
<label for="urlInput">URL do Website</label>
|
||||
<div class="input-group">
|
||||
<i class="fas fa-link"></i>
|
||||
<input
|
||||
type="url"
|
||||
id="urlInput"
|
||||
placeholder="https://example.com"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary" id="crawlBtn">
|
||||
<i class="fas fa-spider"></i>
|
||||
Iniciar Crawling
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Loading State -->
|
||||
<section class="loading-section" id="loadingSection" style="display: none;">
|
||||
<div class="card">
|
||||
<div class="card-body text-center">
|
||||
<div class="spinner"></div>
|
||||
<h3>Crawling em Progresso...</h3>
|
||||
<p id="loadingMessage">Analisando o website...</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Results Section -->
|
||||
<section class="results-section" id="resultsSection" style="display: none;">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3><i class="fas fa-chart-line"></i> Resultados do Crawling</h3>
|
||||
<button class="btn btn-secondary btn-sm" id="exportBtn">
|
||||
<i class="fas fa-download"></i>
|
||||
Exportar JSON
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<!-- Summary Stats -->
|
||||
<div class="stats-grid" id="statsGrid">
|
||||
<!-- Stats will be populated by JavaScript -->
|
||||
</div>
|
||||
|
||||
<!-- Detailed Results -->
|
||||
<div class="results-tabs">
|
||||
<div class="tab-buttons">
|
||||
<button class="tab-btn active" data-tab="overview">Visão Geral</button>
|
||||
<button class="tab-btn" data-tab="links">Links</button>
|
||||
<button class="tab-btn" data-tab="images">Imagens</button>
|
||||
<button class="tab-btn" data-tab="raw">Dados Brutos</button>
|
||||
</div>
|
||||
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active" id="overview">
|
||||
<div class="overview-content">
|
||||
<!-- Overview content will be populated -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane" id="links">
|
||||
<div class="links-list">
|
||||
<!-- Links will be populated -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane" id="images">
|
||||
<div class="images-grid">
|
||||
<!-- Images will be populated -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane" id="raw">
|
||||
<pre class="json-output" id="rawData">
|
||||
<!-- Raw JSON will be populated -->
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Error Section -->
|
||||
<section class="error-section" id="errorSection" style="display: none;">
|
||||
<div class="card error-card">
|
||||
<div class="card-body text-center">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
<h3>Erro no Crawling</h3>
|
||||
<p id="errorMessage">Ocorreu um erro durante o processo de crawling.</p>
|
||||
<button class="btn btn-primary" onclick="resetForm()">
|
||||
<i class="fas fa-redo"></i>
|
||||
Tentar Novamente
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<p>© 2025 CloneWeb Platform. Desenvolvido com ❤️ para crawling inteligente.</p>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,123 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-BR">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>CloneWeb Premium</title>
|
||||
<link rel="stylesheet" href="styles-premium.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;700&display=swap" rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="background-effects">
|
||||
<div class="glow-orb orb-1"></div>
|
||||
<div class="glow-orb orb-2"></div>
|
||||
<div class="grid-background"></div>
|
||||
</div>
|
||||
|
||||
<main class="container">
|
||||
<!-- Hero / Input Section -->
|
||||
<section class="hero-section" id="hero">
|
||||
<div class="logo-area">
|
||||
<i class="fas fa-bolt logo-icon"></i>
|
||||
<h1>CloneWeb <span class="badge">PRO</span></h1>
|
||||
<p>Clonagem de alta fidelidade com Inteligência Artificial</p>
|
||||
</div>
|
||||
|
||||
<div class="clone-input-wrapper">
|
||||
<div class="input-group">
|
||||
<i class="fas fa-link input-icon"></i>
|
||||
<input type="url" id="urlInput" placeholder="Cole o link do site aqui..." autocomplete="off">
|
||||
<button id="startBtn" class="action-btn">
|
||||
<span>CLONAR</span>
|
||||
<i class="fas fa-arrow-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Remove toggle if we want to force smart mode or hide complex options -->
|
||||
<div class="options-toggle hidden">
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="smartMode" checked>
|
||||
<span class="slider"></span>
|
||||
<span class="label-text">Smart Mode (IA)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="features-pill">
|
||||
<span><i class="fas fa-magic"></i> Smart Clone</span>
|
||||
<span><i class="fas fa-image"></i> Verificação Visual</span>
|
||||
<span><i class="fas fa-code"></i> HTML5/CSS3</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Loading State -->
|
||||
<section class="loading-section hidden" id="loading">
|
||||
<div class="loader-circle">
|
||||
<svg viewBox="0 0 100 100">
|
||||
<circle cx="50" cy="50" r="45" class="bg"></circle>
|
||||
<circle cx="50" cy="50" r="45" class="fg" id="progressCircle"></circle>
|
||||
</svg>
|
||||
<div class="loader-icon">
|
||||
<i class="fas fa-dna fa-spin animated-icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
<h2 id="loadingText">Iniciando Motor IA...</h2>
|
||||
<p id="loadingSubtext">Preparando ambiente de clonagem</p>
|
||||
|
||||
<div class="terminal-log" id="terminalLog">
|
||||
<div class="log-line">> Sistema inicializado</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Result / Verification State -->
|
||||
<section class="result-section hidden" id="result">
|
||||
<div class="success-header">
|
||||
<div class="checkmark-circle">
|
||||
<i class="fas fa-check"></i>
|
||||
</div>
|
||||
<h2>Clonagem Concluída!</h2>
|
||||
</div>
|
||||
|
||||
<div class="verification-container hidden" id="verificationContainer">
|
||||
<h3 class="verify-title">Verificação de Qualidade</h3>
|
||||
<div class="comparison-card">
|
||||
<div class="image-wrapper">
|
||||
<span class="img-label">Site Original</span>
|
||||
<img id="imgOriginal" src="" alt="Original">
|
||||
</div>
|
||||
<div class="vs-badge">VS</div>
|
||||
<div class="image-wrapper">
|
||||
<span class="img-label lg-accent">Clone Local</span>
|
||||
<img id="imgClone" src="" alt="Clone">
|
||||
</div>
|
||||
</div>
|
||||
<p class="verification-note">As imagens acima mostram a comparação visual automática.</p>
|
||||
</div>
|
||||
|
||||
<div class="actions-grid">
|
||||
<button class="btn-secondary" id="btnOpenFolder">
|
||||
<i class="fas fa-folder-open"></i> Abrir Pasta
|
||||
</button>
|
||||
<button class="btn-primary glow" id="btnOpenSite">
|
||||
<i class="fas fa-external-link-alt"></i> Abrir Site
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="retry-area">
|
||||
<button class="btn-text" onclick="window.location.reload()">
|
||||
<i class="fas fa-undo"></i> Clonar outro site
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<div id="toast-container"></div>
|
||||
|
||||
<script src="app-premium.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,740 @@
|
||||
/* Reset e Variáveis */
|
||||
:root {
|
||||
--primary-color: #667eea;
|
||||
--secondary-color: #764ba2;
|
||||
--success-color: #43e97b;
|
||||
--danger-color: #f5576c;
|
||||
--warning-color: #ffa726;
|
||||
--info-color: #4facfe;
|
||||
|
||||
--bg-primary: #f8f9fa;
|
||||
--bg-secondary: #ffffff;
|
||||
--bg-sidebar: #1a1d29;
|
||||
|
||||
--text-primary: #2d3748;
|
||||
--text-secondary: #718096;
|
||||
--text-light: #a0aec0;
|
||||
|
||||
--border-color: #e2e8f0;
|
||||
--shadow-sm: 0 2px 4px rgba(0,0,0,0.05);
|
||||
--shadow-md: 0 4px 6px rgba(0,0,0,0.07);
|
||||
--shadow-lg: 0 10px 15px rgba(0,0,0,0.1);
|
||||
|
||||
--sidebar-width: 260px;
|
||||
--header-height: 70px;
|
||||
|
||||
--transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
line-height: 1.6;
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
.sidebar {
|
||||
width: var(--sidebar-width);
|
||||
background: var(--bg-sidebar);
|
||||
color: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: fixed;
|
||||
height: 100vh;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.logo {
|
||||
padding: 2rem 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
.logo i {
|
||||
font-size: 2rem;
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.logo h1 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.nav-menu {
|
||||
flex: 1;
|
||||
padding: 1.5rem 0;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1rem 1.5rem;
|
||||
color: rgba(255,255,255,0.7);
|
||||
text-decoration: none;
|
||||
transition: var(--transition);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
background: rgba(255,255,255,0.05);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
background: rgba(102, 126, 234, 0.1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-item.active::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 4px;
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
||||
}
|
||||
|
||||
.nav-item i {
|
||||
font-size: 1.2rem;
|
||||
width: 24px;
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
padding: 1.5rem;
|
||||
border-top: 1px solid rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
.theme-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
color: rgba(255,255,255,0.7);
|
||||
}
|
||||
|
||||
.switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 50px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(255,255,255,0.2);
|
||||
transition: var(--transition);
|
||||
border-radius: 24px;
|
||||
}
|
||||
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
left: 3px;
|
||||
bottom: 3px;
|
||||
background-color: white;
|
||||
transition: var(--transition);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
input:checked + .slider {
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
||||
}
|
||||
|
||||
input:checked + .slider:before {
|
||||
transform: translateX(26px);
|
||||
}
|
||||
|
||||
/* Main Content */
|
||||
.main-content {
|
||||
margin-left: var(--sidebar-width);
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.header {
|
||||
height: var(--header-height);
|
||||
background: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding: 0 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.header h2 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--bg-primary);
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--text-light);
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.status-indicator.online .status-dot {
|
||||
background: var(--success-color);
|
||||
}
|
||||
|
||||
.status-indicator.offline .status-dot {
|
||||
background: var(--danger-color);
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
/* Pages */
|
||||
.page {
|
||||
display: none;
|
||||
padding: 2rem;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.page.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Stats Grid */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
box-shadow: var(--shadow-md);
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.5rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.stat-content h3 {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.stat-content p {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Section Header */
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.section-header h3 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* Recent Clones */
|
||||
.recent-clones {
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.clones-list {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.clone-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1rem;
|
||||
background: var(--bg-primary);
|
||||
border-radius: 8px;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.clone-item:hover {
|
||||
background: #e8eaf0;
|
||||
}
|
||||
|
||||
.clone-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.clone-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.clone-details h4 {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.clone-details p {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.clone-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* Form */
|
||||
.clone-form-container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.form-card {
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
box-shadow: var(--shadow-md);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.form-card h3 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.form-description {
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.form-group label i {
|
||||
margin-right: 0.5rem;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
transition: var(--transition);
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
background: white;
|
||||
}
|
||||
|
||||
.advanced-options {
|
||||
background: var(--bg-primary);
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.option-row {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.option-row:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checkbox-label input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.slider-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.slider-input {
|
||||
flex: 1;
|
||||
height: 6px;
|
||||
border-radius: 3px;
|
||||
background: var(--border-color);
|
||||
outline: none;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
.slider-input::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.slider-value {
|
||||
min-width: 80px;
|
||||
text-align: right;
|
||||
font-weight: 600;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.form-group small {
|
||||
display: block;
|
||||
margin-top: 0.5rem;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
padding: 0.75rem 1.5rem;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 16px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #e2e8f0;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: var(--danger-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #e04560;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
padding: 1rem 2rem;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
/* Progress Card */
|
||||
.progress-card {
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.progress-card h3 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.progress-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--primary-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.progress-bar-container {
|
||||
height: 8px;
|
||||
background: var(--bg-primary);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--primary-color), var(--secondary-color));
|
||||
width: 0%;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-stats {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.progress-stats span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.progress-stats i {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* History */
|
||||
.history-list {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.history-item {
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
box-shadow: var(--shadow-md);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.history-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.history-info h4 {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.history-meta {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.history-meta span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* Settings */
|
||||
.settings-container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.settings-section {
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
box-shadow: var(--shadow-md);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.settings-section h3 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
padding: 1.5rem 0;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.setting-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.setting-info h4 {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.setting-info p {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.setting-control {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.settings-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.sidebar {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
.main-content {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.option-row {
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Empty State */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 3rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.empty-state i {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 1rem;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.empty-state h3 {
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
@@ -0,0 +1,502 @@
|
||||
:root {
|
||||
--bg-dark: #020617;
|
||||
--bg-card: rgba(15, 23, 42, 0.8);
|
||||
--accent-primary: #06b6d4;
|
||||
--accent-secondary: #3b82f6;
|
||||
--accent-glow: rgba(6, 182, 212, 0.4);
|
||||
--text-main: #f8fafc;
|
||||
--text-dim: #94a3b8;
|
||||
--border-glass: rgba(255, 255, 255, 0.1);
|
||||
--glass-blur: blur(12px);
|
||||
--success: #10b981;
|
||||
--error: #ef4444;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: 'Outfit', sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
background: radial-gradient(circle at 50% 50%, #0f172a 0%, #020617 100%);
|
||||
color: var(--text-main);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow-x: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Background Effects */
|
||||
.background-effects {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: -1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.glow-orb {
|
||||
position: absolute;
|
||||
width: 600px;
|
||||
height: 600px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle, var(--accent-glow) 0%, transparent 70%);
|
||||
filter: blur(80px);
|
||||
opacity: 0.5;
|
||||
animation: orbFloat 20s infinite alternate;
|
||||
}
|
||||
|
||||
.orb-1 {
|
||||
top: -10%;
|
||||
right: -5%;
|
||||
}
|
||||
|
||||
.orb-2 {
|
||||
bottom: -10%;
|
||||
left: -5%;
|
||||
animation-delay: -5s;
|
||||
}
|
||||
|
||||
.grid-background {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image:
|
||||
linear-gradient(rgba(255, 255, 255, 0.02) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255, 255, 255, 0.02) 1px, transparent 1px);
|
||||
-webkit-mask-image: radial-gradient(circle at center, black, transparent 80%);
|
||||
mask-image: radial-gradient(circle at center, black, transparent 80%);
|
||||
}
|
||||
|
||||
@keyframes orbFloat {
|
||||
0% {
|
||||
transform: translate(0, 0) scale(1);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translate(-50px, 50px) scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Hero Section */
|
||||
.hero-section {
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
.logo-area {
|
||||
margin-bottom: 4rem;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
font-size: 4rem;
|
||||
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
filter: drop-shadow(0 0 30px var(--accent-glow));
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 4rem;
|
||||
font-weight: 800;
|
||||
margin-bottom: 0.5rem;
|
||||
letter-spacing: -2px;
|
||||
background: linear-gradient(to bottom, #fff, #94a3b8);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.badge {
|
||||
font-size: 0.9rem;
|
||||
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
|
||||
padding: 0.3rem 0.8rem;
|
||||
border-radius: 99px;
|
||||
vertical-align: middle;
|
||||
margin-left: 1rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
-webkit-text-fill-color: white;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 0 20px var(--accent-glow);
|
||||
}
|
||||
|
||||
.logo-area p {
|
||||
color: var(--text-dim);
|
||||
font-size: 1.4rem;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
/* Input Group */
|
||||
.clone-input-wrapper {
|
||||
background: var(--bg-card);
|
||||
padding: 1.5rem;
|
||||
border-radius: 24px;
|
||||
border: 1px solid var(--border-glass);
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
|
||||
-webkit-backdrop-filter: var(--glass-blur);
|
||||
backdrop-filter: var(--glass-blur);
|
||||
margin-bottom: 3rem;
|
||||
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
}
|
||||
|
||||
.clone-input-wrapper:hover {
|
||||
transform: translateY(-5px) scale(1.02);
|
||||
border-color: rgba(6, 182, 212, 0.4);
|
||||
box-shadow: 0 30px 60px -12px rgba(0, 0, 0, 0.6), 0 0 20px rgba(6, 182, 212, 0.1);
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 12px;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid transparent;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.input-group:focus-within {
|
||||
border-color: var(--accent-color);
|
||||
box-shadow: 0 0 0 2px rgba(6, 182, 212, 0.2);
|
||||
}
|
||||
|
||||
.input-icon {
|
||||
color: var(--text-secondary);
|
||||
font-size: 1.2rem;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
input[type="url"] {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-primary);
|
||||
font-size: 1.1rem;
|
||||
padding: 1rem 0;
|
||||
outline: none;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 1.2rem 2.5rem;
|
||||
border-radius: 16px;
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.8rem;
|
||||
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
box-shadow: 0 10px 20px -5px var(--accent-glow);
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
transform: translateX(5px) scale(1.05);
|
||||
box-shadow: 0 15px 30px -5px var(--accent-glow);
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
|
||||
.input-icon {
|
||||
color: var(--accent-primary);
|
||||
font-size: 1.4rem;
|
||||
padding: 0 1.5rem;
|
||||
filter: drop-shadow(0 0 10px var(--accent-glow));
|
||||
}
|
||||
|
||||
input[type="url"] {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 1.2rem;
|
||||
padding: 1.2rem 0;
|
||||
outline: none;
|
||||
min-width: 0;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
input[type="url"]::placeholder {
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.features-pill {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 3rem;
|
||||
color: var(--text-dim);
|
||||
font-size: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.features-pill span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.features-pill span:hover {
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.features-pill i {
|
||||
color: var(--accent-primary);
|
||||
filter: drop-shadow(0 0 5px var(--accent-glow));
|
||||
}
|
||||
|
||||
/* Loading Section */
|
||||
.loading-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
animation: fadeIn 0.5s ease;
|
||||
}
|
||||
|
||||
.loader-circle {
|
||||
position: relative;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
svg {
|
||||
transform: rotate(-90deg);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
circle {
|
||||
fill: none;
|
||||
stroke-width: 6;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
|
||||
.bg {
|
||||
stroke: var(--card-bg);
|
||||
}
|
||||
|
||||
.fg {
|
||||
stroke: var(--accent-color);
|
||||
stroke-dasharray: 283;
|
||||
stroke-dashoffset: 283;
|
||||
transition: stroke-dashoffset 0.5s ease;
|
||||
}
|
||||
|
||||
.loader-icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 3rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.terminal-log {
|
||||
margin-top: 2rem;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
font-family: 'Consolas', monospace;
|
||||
text-align: left;
|
||||
height: 150px;
|
||||
overflow-y: auto;
|
||||
font-size: 0.9rem;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.log-line {
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
/* Result Section */
|
||||
.result-section {
|
||||
animation: slideUp 0.5s ease;
|
||||
}
|
||||
|
||||
.success-header {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.checkmark-circle {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background: rgba(16, 185, 129, 0.2);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto 1.5rem;
|
||||
color: #10b981;
|
||||
font-size: 3rem;
|
||||
box-shadow: 0 0 30px rgba(16, 185, 129, 0.3);
|
||||
}
|
||||
|
||||
.verification-container {
|
||||
background: var(--card-bg);
|
||||
padding: 1.5rem;
|
||||
border-radius: 16px;
|
||||
border: 1px solid var(--border-color);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.comparison-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.image-wrapper {
|
||||
position: relative;
|
||||
width: 250px;
|
||||
height: 150px;
|
||||
/* fixed aspect ratio roughly */
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: #000;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.image-wrapper img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
/* or contain */
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.img-label {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
left: 5px;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
color: white;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.lg-accent {
|
||||
color: var(--accent-color);
|
||||
border: 1px solid var(--accent-color);
|
||||
}
|
||||
|
||||
.vs-badge {
|
||||
background: var(--bg-darker);
|
||||
padding: 0.5rem;
|
||||
border-radius: 50%;
|
||||
font-weight: bold;
|
||||
color: var(--text-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.actions-grid {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.btn-primary,
|
||||
.btn-secondary {
|
||||
padding: 1rem 2rem;
|
||||
border-radius: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
font-size: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--accent-color), var(--secondary-color));
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 2rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.btn-text:hover {
|
||||
color: var(--text-primary);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
transform: translateY(20px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.comparison-card {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.image-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.actions-grid {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,477 @@
|
||||
/* Reset and Base Styles */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.header {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.logo i {
|
||||
font-size: 2rem;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.logo h1 {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: #f8f9fa;
|
||||
border-radius: 20px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.status-indicator.online i {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.status-indicator.offline i {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.status-indicator.checking i {
|
||||
color: #ffc107;
|
||||
animation: pulse 1.5s infinite;
|
||||
}
|
||||
|
||||
/* Main Content */
|
||||
.main {
|
||||
padding: 2rem 0;
|
||||
min-height: calc(100vh - 140px);
|
||||
}
|
||||
|
||||
/* Hero Section */
|
||||
.hero {
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.hero h2 {
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 1rem;
|
||||
text-shadow: 0 2px 4px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.hero p {
|
||||
font-size: 1.2rem;
|
||||
opacity: 0.9;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Cards */
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
|
||||
margin-bottom: 2rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: 1.5rem;
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-header h3 {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
/* Forms */
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.input-group i {
|
||||
position: absolute;
|
||||
left: 1rem;
|
||||
color: #999;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.input-group input {
|
||||
width: 100%;
|
||||
padding: 1rem 1rem 1rem 3rem;
|
||||
border: 2px solid #e9ecef;
|
||||
border-radius: 12px;
|
||||
font-size: 1rem;
|
||||
transition: all 0.3s ease;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.input-group input:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
background: white;
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #5a6268;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(40, 167, 69, 0.3);
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
text-align: center;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
/* Loading */
|
||||
.spinner {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #667eea;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 1rem;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
/* Stats Grid */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||
padding: 1.5rem;
|
||||
border-radius: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-card .stat-value {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: #667eea;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.stat-card .stat-label {
|
||||
font-size: 0.9rem;
|
||||
color: #666;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
/* Tabs */
|
||||
.results-tabs {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.tab-buttons {
|
||||
display: flex;
|
||||
border-bottom: 2px solid #e9ecef;
|
||||
margin-bottom: 1.5rem;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
padding: 0.75rem 1.5rem;
|
||||
border: none;
|
||||
background: none;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
border-bottom: 3px solid transparent;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
color: #667eea;
|
||||
border-bottom-color: #667eea;
|
||||
}
|
||||
|
||||
.tab-btn:hover {
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.tab-pane {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-pane.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Overview Content */
|
||||
.overview-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.overview-item .label {
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.overview-item .value {
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Links List */
|
||||
.link-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.link-item i {
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.link-item a {
|
||||
color: #333;
|
||||
text-decoration: none;
|
||||
flex: 1;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.link-item a:hover {
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
/* Images Grid */
|
||||
.images-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.image-item {
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.image-item img {
|
||||
max-width: 100%;
|
||||
height: 120px;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.image-item .image-url {
|
||||
font-size: 0.8rem;
|
||||
color: #666;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* JSON Output */
|
||||
.json-output {
|
||||
background: #2d3748;
|
||||
color: #e2e8f0;
|
||||
padding: 1.5rem;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Error Card */
|
||||
.error-card {
|
||||
border-left: 4px solid #dc3545;
|
||||
}
|
||||
|
||||
.error-card .card-body {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.error-card i {
|
||||
font-size: 3rem;
|
||||
color: #dc3545;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.error-card h3 {
|
||||
color: #dc3545;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.footer {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.2);
|
||||
padding: 1rem 0;
|
||||
text-align: center;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* Utilities */
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.hero h2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.hero p {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.tab-buttons {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user