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';
|
||||
}
|
||||
Reference in New Issue
Block a user