1
0

Add navigation functionality and refactor layout structure

Introduced a sidebar and header component to improve layout organization and UI consistency. Added state management for navigating between pages (dashboard, scraper, schedules, logs) with placeholder content for future implementations.
This commit is contained in:
Márcio Eric 2025-12-23 17:35:35 -03:00
parent f069b87dbf
commit 90b427c5b7

View File

@ -1,5 +1,5 @@
import React, { useState, useEffect, useRef } from 'react';
import { Database, Globe, Download, Play, Pause, Trash2, Plus, RefreshCw, Activity, Clock, CircleX, CircleCheck } from 'lucide-react';
import { Database, Globe, Download, Play, Pause, Trash2, Plus, RefreshCw, Activity, Clock, CircleX, CircleCheck, Grid, Calendar, Terminal } from 'lucide-react';
import * as signalR from '@microsoft/signalr';
const API_BASE = 'http://localhost:5123';
@ -13,6 +13,9 @@ const WebScraper = () => {
const [logs, setLogs] = useState([]);
const [isConnected, setIsConnected] = useState(false);
// Estado de navegação simples para futuras páginas
const [activePage, setActivePage] = useState('scraper'); // 'dashboard' | 'scraper' | 'schedules' | 'logs'
const connectionRef = useRef(null);
const addLog = (message, type = 'info') => {
@ -71,10 +74,7 @@ const WebScraper = () => {
addLog('Conectado ao servidor', 'success');
connectionRef.current = connection;
// ✅ NOVO: assinar overview
await connection.invoke('SubscribeOverview');
// pega snapshot inicial uma vez
await loadSessions();
})
.catch(err => {
@ -111,7 +111,7 @@ const WebScraper = () => {
const upsertSessionFromEvent = (sid, patch) => {
setSessions(prev => {
const idx = prev.findIndex(s => s.sessionId === sid);
if (idx === -1) return prev; // ou: return [...prev, {sessionId:sid, name:'', ...patch}]
if (idx === -1) return prev;
const copy = [...prev];
copy[idx] = { ...copy[idx], ...patch };
return copy;
@ -150,7 +150,6 @@ const WebScraper = () => {
case 'ItemStarted':
addLog(`📄 Processando: ${event.url}`, 'info');
// Atualiza runtime do painel se essa for a sessão selecionada
setSessionStatus(prev => {
if (!prev || prev.runtime?.sessionId !== sid) return prev;
@ -186,7 +185,6 @@ const WebScraper = () => {
}
});
// também atualiza painel se for a sessão selecionada
if (selectedSession?.sessionId === event.sessionId) {
setSessionStatus(prev => ({
...(prev ?? {}),
@ -307,7 +305,7 @@ const WebScraper = () => {
setSelectedSessionId(session.sessionId);
setLogs([]);
addLog(`Sessão selecionada: ${session.name}`, 'info');
loadSessionStatus(session.sessionId); // snapshot inicial
loadSessionStatus(session.sessionId);
};
const formatNumber = (num) => {
@ -342,7 +340,7 @@ const WebScraper = () => {
};
const handleSessionAction = (e, session) => {
e.stopPropagation(); // Evita selecionar a sessão ao clicar no botão
e.stopPropagation();
if (session.isRunning) {
stopScrapingById(session.sessionId, session.name);
} else {
@ -350,33 +348,98 @@ const WebScraper = () => {
}
};
return (
<div className="min-h-screen bg-gradient-to-br from-slate-900 to-slate-800 text-white p-6">
<div className="max-w-7xl mx-auto">
{/* Header */}
<div className="mb-8 flex justify-between items-center">
<div>
<h1 className="text-4xl font-bold mb-2 flex items-center gap-3">
// Componente Header
const Header = () => (
<header className="h-16 shrink-0 bg-slate-900/80 backdrop-blur border-b border-slate-700 flex items-center justify-between px-6 fixed top-0 left-0 right-0 z-20">
<div className="flex items-center gap-3">
<Globe className="text-blue-400" />
Web Scraper Pro
</h1>
<p className="text-slate-400">Gerenciador de scraping com API backend</p>
<h1 className="text-lg font-bold tracking-tight text-white">Web Scraper Pro</h1>
</div>
<div className="flex items-center gap-3">
<div className={`flex items-center gap-2 px-4 py-2 rounded-lg ${isConnected ? 'bg-green-900/30 text-green-400' : 'bg-red-900/30 text-red-400'}`}>
<div className={`flex items-center gap-2 px-3.5 py-1.5 rounded border text-sm font-medium ${isConnected ? 'bg-green-900/20 border-green-800 text-green-400' : 'bg-red-900/20 border-red-800 text-red-400'}`}>
<Activity size={16} />
{isConnected ? 'Conectado' : 'Desconectado'}
</div>
<button
onClick={loadSessions}
className="flex items-center gap-2 px-4 py-2 bg-slate-700 hover:bg-slate-600 rounded-lg transition"
className="flex items-center gap-2 px-3.5 py-1.5 bg-slate-800 border border-slate-700 hover:border-blue-500/50 text-slate-300 hover:text-white rounded transition-colors text-sm"
>
<RefreshCw size={16} />
Atualizar
</button>
</div>
</header>
);
// Componente Sidebar
const Sidebar = () => (
<aside className="w-64 bg-slate-900/60 backdrop-blur border-r border-slate-700 flex flex-col shrink-0 fixed top-16 bottom-0 left-0 z-10">
<div className="p-4">
<p className="px-2 text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2">Gerenciamento</p>
<nav className="space-y-1">
<button
onClick={() => setActivePage('dashboard')}
className={`w-full text-left flex items-center gap-3 px-3 py-2 rounded-lg transition-colors ${activePage === 'dashboard' ? 'bg-blue-500/10 text-blue-400 border border-blue-500/20' : 'text-slate-400 hover:bg-slate-800 hover:text-white'}`}
>
<Grid size={18} />
<span className="text-sm font-medium">Dashboard</span>
</button>
<button
onClick={() => setActivePage('scraper')}
className={`w-full text-left flex items-center gap-3 px-3 py-2 rounded-lg transition-colors ${activePage === 'scraper' ? 'bg-blue-500/10 text-blue-400 border border-blue-500/20' : 'text-slate-400 hover:bg-slate-800 hover:text-white'}`}
>
<Database size={18} />
<span className="text-sm font-bold">Scraper</span>
</button>
<button
onClick={() => setActivePage('schedules')}
className={`w-full text-left flex items-center gap-3 px-3 py-2 rounded-lg transition-colors ${activePage === 'schedules' ? 'bg-blue-500/10 text-blue-400 border border-blue-500/20' : 'text-slate-400 hover:bg-slate-800 hover:text-white'}`}
>
<Calendar size={18} />
<span className="text-sm font-medium">Agendamentos</span>
</button>
<button
onClick={() => setActivePage('logs')}
className={`w-full text-left flex items-center gap-3 px-3 py-2 rounded-lg transition-colors ${activePage === 'logs' ? 'bg-blue-500/10 text-blue-400 border border-blue-500/20' : 'text-slate-400 hover:bg-slate-800 hover:text-white'}`}
>
<Terminal size={18} />
<span className="text-sm font-medium">Logs de Execução</span>
</button>
</nav>
</div>
</aside>
);
return (
<div className="min-h-screen bg-gradient-to-br from-slate-900 to-slate-800 text-white">
<Header />
<Sidebar />
{/* Conteúdo principal */}
<main className="pt-16 pl-0 md:pl-64">
<div className="p-6 lg:p-8">
{/* Breadcrumb e título da página ativa */}
<div className="mb-6">
<div className="flex items-center text-xs text-slate-400 mb-2">
<span className="hover:text-blue-400 transition-colors cursor-default">Extratores</span>
<span className="mx-2 text-slate-600">/</span>
<span className="text-white">{activePage === 'scraper' ? 'Scraper' : activePage.charAt(0).toUpperCase() + activePage.slice(1)}</span>
</div>
<div className="flex items-end justify-between gap-4">
<div>
<h2 className="text-2xl md:text-3xl font-bold text-white tracking-tight">
{activePage === 'scraper' ? 'Gerenciador de Sessões' : 'Em breve'}
</h2>
<p className="text-slate-400 text-sm mt-1">
{activePage === 'scraper' ? 'Gerencie e monitore suas sessões de scraping.' : 'Conteúdo a ser implementado.'}
</p>
</div>
</div>
</div>
{/* Render da página ativa */}
{activePage === 'scraper' ? (
<div className="max-w-7xl mx-auto">
<div className="grid grid-cols-3 gap-6">
{/* Lista de Sessões */}
<div className="bg-slate-800 rounded-lg p-6 shadow-xl">
@ -575,6 +638,13 @@ const WebScraper = () => {
</div>
</div>
</div>
) : (
<div className="bg-slate-800/50 border border-slate-700 rounded-xl p-8 text-slate-300">
Conteúdo da página "{activePage}" em breve.
</div>
)}
</div>
</main>
</div>
);
};