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:
parent
f069b87dbf
commit
90b427c5b7
112
src/App.js
112
src/App.js
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user