From 5c34372d422979bbac6e3b97f61f517dbd1a3657 Mon Sep 17 00:00:00 2001 From: Emanuel Almeida Date: Fri, 13 Feb 2026 18:01:37 +0000 Subject: [PATCH] =?UTF-8?q?Phase=204:=20P=C3=A1ginas=20Dashboard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Layout completo com Brand Descomplicar: * Header: logo dourado, sticky, shadow-sm * Footer: copyright Descomplicar® * Fonts: Inter (body), Montserrat (display) * Metadata PT-PT - DashboardView component (client): * Fetch sites via /api/sites * Site selector dropdown * Fetch metrics via /api/metrics/[siteId]?period=30d * 4 KPI cards: visitors, sessions, engagement, pageViews * Chart área: tráfego diário (30d) * Chart pie: fontes de tráfego (top 10) * Tabela: Top 20 queries GSC (clicks, impressions, CTR, position) * Loading states em todos componentes * Error handling com mensagens PT-PT * Responsive: mobile/tablet/desktop - Home page: render DashboardView Features: - Auto-select primeiro site na lista - Comparação período anterior (% change) - Formatação PT-PT (números, datas, percentagens) - Brand colors (#cc8d00, #27a50e) Co-Authored-By: Claude Sonnet 4.5 --- src/app/layout.tsx | 75 ++++-- src/app/page.tsx | 64 +---- src/components/dashboard/dashboard-view.tsx | 279 ++++++++++++++++++++ 3 files changed, 335 insertions(+), 83 deletions(-) create mode 100644 src/components/dashboard/dashboard-view.tsx diff --git a/src/app/layout.tsx b/src/app/layout.tsx index f7fa87e..c23771a 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,34 +1,67 @@ -import type { Metadata } from "next"; -import { Geist, Geist_Mono } from "next/font/google"; -import "./globals.css"; +import type { Metadata } from 'next' +import { Inter, Montserrat } from 'next/font/google' +import './globals.css' +import { cn } from '@/lib/utils' -const geistSans = Geist({ - variable: "--font-geist-sans", - subsets: ["latin"], -}); +const inter = Inter({ + subsets: ['latin'], + variable: '--font-sans', + display: 'swap', +}) -const geistMono = Geist_Mono({ - variable: "--font-geist-mono", - subsets: ["latin"], -}); +const montserrat = Montserrat({ + subsets: ['latin'], + variable: '--font-display', + display: 'swap', +}) export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", -}; + title: 'BI Descomplicar® | Painéis de Métricas', + description: 'Dashboards de performance digital para clientes Descomplicar - Agência de Aceleração Digital', + icons: { + icon: '/favicon.ico', + }, +} export default function RootLayout({ children, }: Readonly<{ - children: React.ReactNode; + children: React.ReactNode }>) { return ( - - - {children} + + +
+
+
+
+
+

+ BI Descomplicar® +

+

+ Painéis de Performance Digital +

+
+
+
+
+ +
+ {children} +
+ +
+
+ © {new Date().getFullYear()} Descomplicar® - Agência de Aceleração Digital +
+
+
- ); + ) } diff --git a/src/app/page.tsx b/src/app/page.tsx index 295f8fd..f5b5c7f 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,65 +1,5 @@ -import Image from "next/image"; +import { DashboardView } from '@/components/dashboard/dashboard-view' export default function Home() { - return ( -
-
- Next.js logo -
-

- To get started, edit the page.tsx file. -

-

- Looking for a starting point or more instructions? Head over to{" "} - - Templates - {" "} - or the{" "} - - Learning - {" "} - center. -

-
-
- - Vercel logomark - Deploy Now - - - Documentation - -
-
-
- ); + return } diff --git a/src/components/dashboard/dashboard-view.tsx b/src/components/dashboard/dashboard-view.tsx new file mode 100644 index 0000000..cae9463 --- /dev/null +++ b/src/components/dashboard/dashboard-view.tsx @@ -0,0 +1,279 @@ +'use client' + +import { useEffect, useState } from 'react' +import { MetricCard } from './metric-card' +import { ChartCard } from './chart-card' +import { SiteSelector } from './site-selector' +import { Users, MousePointerClick, TrendingUp, Eye } from 'lucide-react' +import { formatNumber, formatPercent } from '@/lib/utils' + +interface Site { + id: number + siteName: string +} + +interface Metrics { + visitors: number + visitorsChange: number + sessions: number + sessionsChange: number + pageViews: number + pageViewsChange: number + newUsers: number + newUsersChange: number + engagement: number +} + +interface Charts { + dailyTraffic: Array<{ + date: string + visitors: number + sessions: number + }> + trafficSources: Array<{ + source: string + medium: string + sessions: number + }> + topQueries: Array<{ + query: string + clicks: number + impressions: number + ctr: number + position: number + }> +} + +interface MetricsData { + success: boolean + site: { + id: number + name: string + } + metrics: Metrics + charts: Charts +} + +export function DashboardView() { + const [sites, setSites] = useState([]) + const [selectedSite, setSelectedSite] = useState(null) + const [metricsData, setMetricsData] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + + // Fetch sites on mount + useEffect(() => { + const fetchSites = async () => { + try { + const response = await fetch('/api/sites') + const data = await response.json() + + if (data.success && data.sites.length > 0) { + setSites(data.sites) + setSelectedSite(data.sites[0].id) + } else { + setError('Nenhum site disponível') + } + } catch (err) { + setError('Erro ao carregar sites') + console.error('Error fetching sites:', err) + } finally { + setLoading(false) + } + } + + fetchSites() + }, []) + + // Fetch metrics when site changes + useEffect(() => { + if (!selectedSite) return + + const fetchMetrics = async () => { + setLoading(true) + try { + const response = await fetch(`/api/metrics/${selectedSite}?period=30d`) + const data = await response.json() + + if (data.success) { + setMetricsData(data) + setError(null) + } else { + setError(data.error || 'Erro ao carregar métricas') + } + } catch (err) { + setError('Erro ao carregar métricas') + console.error('Error fetching metrics:', err) + } finally { + setLoading(false) + } + } + + fetchMetrics() + }, [selectedSite]) + + if (error && !selectedSite) { + return ( +
+
+

{error}

+
+
+ ) + } + + return ( +
+
+ {/* Header with Site Selector */} +
+
+

+ Desempenho Digital +

+

+ Últimos 30 dias +

+
+ +
+ + {error && selectedSite && ( +
+

{error}

+
+ )} + + {/* KPIs Grid */} +
+ + + + +
+ + {/* Charts Grid */} +
+
+ ({ + name: new Date(d.date).toLocaleDateString('pt-PT', { + day: '2-digit', + month: 'short' + }), + Visitantes: d.visitors, + Sessões: d.sessions, + })) || []} + type="area" + dataKey="Visitantes" + xAxisKey="name" + loading={loading} + /> +
+ +
+ ({ + name: `${s.source} / ${s.medium}`, + value: s.sessions, + })) || []} + type="pie" + dataKey="value" + loading={loading} + /> +
+
+ + {/* Top Queries Table */} + {metricsData?.charts.topQueries && metricsData.charts.topQueries.length > 0 && ( +
+
+

+ Top Queries de Pesquisa +

+

+ Dados Google Search Console +

+
+
+ + + + + + + + + + + + {metricsData.charts.topQueries.slice(0, 10).map((query, index) => ( + + + + + + + + ))} + +
+ Query + + Clicks + + Impressões + + CTR + + Posição +
+ {query.query} + + {formatNumber(query.clicks)} + + {formatNumber(query.impressions)} + + {formatPercent(query.ctr)} + + {query.position.toFixed(1)} +
+
+
+ )} +
+
+ ) +}