metaads-pro/src/app/dashboard/page.tsx
Marcio Bevervanso 234314d1c6 Publish MetaAds Pro: AI-driven Meta Ads campaign management dashboard
Full app with AI campaign creation/copy/creative generation, dashboard,
campaign automation engine, Meta OAuth login, and a redesigned landing page.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 19:30:33 -03:00

220 lines
7.7 KiB
TypeScript

'use client';
import React, { useState, useEffect, useCallback } from 'react';
import { MetricsCard } from '@/components/dashboard/MetricsCard';
import { CampaignTable } from '@/components/dashboard/CampaignTable';
import { DateFilter } from '@/components/dashboard/DateFilter';
import { Spinner } from '@/components/ui/Spinner';
import { RefreshCw } from 'lucide-react';
import type { CampaignWithInsights, AccountOverview, DatePreset } from '@/lib/meta/types';
export default function DashboardPage() {
const [datePreset, setDatePreset] = useState<DatePreset>('last_30d');
const [campaigns, setCampaigns] = useState<CampaignWithInsights[]>([]);
const [overview, setOverview] = useState<AccountOverview | null>(null);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [error, setError] = useState('');
const fetchData = useCallback(async (showLoading = true) => {
if (showLoading) setLoading(true);
else setRefreshing(true);
setError('');
try {
const [campaignsRes, overviewRes] = await Promise.all([
fetch(`/api/campaigns?date_preset=${datePreset}`),
fetch(`/api/campaigns/insights?date_preset=${datePreset}`),
]);
if (!campaignsRes.ok || !overviewRes.ok) {
throw new Error('Erro ao carregar dados');
}
const campaignsData = await campaignsRes.json();
const overviewData = await overviewRes.json();
if (campaignsData.success) setCampaigns(campaignsData.data);
if (overviewData.success) setOverview(overviewData.data);
} catch (err) {
setError(err instanceof Error ? err.message : 'Erro ao carregar dados');
} finally {
setLoading(false);
setRefreshing(false);
}
}, [datePreset]);
useEffect(() => {
fetchData();
}, [fetchData]);
// Auto-refresh every 60s
useEffect(() => {
const interval = setInterval(() => fetchData(false), 60000);
return () => clearInterval(interval);
}, [fetchData]);
const handleStatusChange = async (campaignId: string, newStatus: 'ACTIVE' | 'PAUSED') => {
const res = await fetch(`/api/campaigns/${campaignId}/status`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: newStatus }),
});
if (!res.ok) throw new Error('Erro ao atualizar status');
// Update local state
setCampaigns(prev =>
prev.map(c =>
c.campaign.id === campaignId
? { ...c, campaign: { ...c.campaign, status: newStatus } }
: c
)
);
};
const handleBudgetChange = async (campaignId: string, newBudget: number) => {
const res = await fetch(`/api/campaigns/${campaignId}/budget`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ daily_budget: newBudget }),
});
if (!res.ok) throw new Error('Erro ao atualizar orçamento');
// Update local state
setCampaigns(prev =>
prev.map(c =>
c.campaign.id === campaignId
? { ...c, campaign: { ...c.campaign, daily_budget: String(newBudget) } }
: c
)
);
};
const formatCurrency = (val: any) => `R$ ${Number(val || 0).toFixed(2)}`;
const formatNumber = (val: any) => Number(val || 0).toLocaleString('pt-BR');
return (
<div className="flex flex-col gap-6">
{/* Header */}
<div className="flex items-center justify-between animate-fade-in-up">
<div>
<h1 className="text-2xl font-bold text-white">Dashboard</h1>
<p className="text-sm mt-1" style={{ color: 'var(--muted-fg)' }}>
Visão geral das suas campanhas de Facebook Ads
</p>
</div>
<div className="flex items-center gap-4">
<DateFilter selected={datePreset} onChange={setDatePreset} />
<button
onClick={() => fetchData(false)}
disabled={refreshing}
className="btn-ghost flex items-center gap-2"
>
<RefreshCw size={14} className={refreshing ? 'animate-spin-slow' : ''} />
Atualizar
</button>
</div>
</div>
{/* Error */}
{error && (
<div className="px-5 py-4 rounded-xl animate-fade-in" style={{ background: 'rgba(239,68,68,0.08)', border: '1px solid rgba(239,68,68,0.15)' }}>
<p className="text-sm" style={{ color: '#f87171' }}>{error}</p>
<p className="text-xs mt-1" style={{ color: 'var(--muted-fg)' }}>
Verifique se as credenciais do Meta estão configuradas no .env.local
</p>
</div>
)}
{/* Metrics Cards */}
{loading ? (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="shimmer h-[120px]" />
))}
</div>
) : overview ? (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
<MetricsCard
title="Investido"
value={formatCurrency(overview.total_spend)}
subtitle={`${overview.campaigns_active} campanhas ativas`}
icon="dollar"
color="purple"
delay={1}
/>
<MetricsCard
title="Leads"
value={formatNumber(overview.total_leads)}
subtitle={overview.total_leads > 0 ? `CPL: ${formatCurrency(overview.total_spend / overview.total_leads)}` : undefined}
icon="users"
color="green"
delay={2}
/>
<MetricsCard
title="ROAS"
value={overview.total_roas > 0 ? `${overview.total_roas.toFixed(2)}x` : '—'}
subtitle="Retorno sobre investimento"
icon="trending"
color="blue"
delay={3}
/>
<MetricsCard
title="CTR"
value={`${overview.avg_ctr.toFixed(2)}%`}
subtitle={`${formatNumber(overview.total_clicks)} cliques`}
icon="target"
color="amber"
delay={4}
/>
<MetricsCard
title="CPC"
value={formatCurrency(overview.avg_cpc)}
subtitle="Custo por clique"
icon="click"
color="pink"
delay={5}
/>
<MetricsCard
title="CPM"
value={formatCurrency(overview.avg_cpm)}
subtitle={`${formatNumber(overview.total_impressions)} impressões`}
icon="chart"
color="cyan"
delay={6}
/>
</div>
) : null}
{/* Campaigns Table */}
<CampaignTable
campaigns={campaigns}
loading={loading}
onStatusChange={handleStatusChange}
onBudgetChange={handleBudgetChange}
/>
{/* Footer */}
{!loading && (
<div className="flex items-center justify-between animate-fade-in">
<p className="text-xs" style={{ color: 'var(--muted-fg)' }}>
{campaigns.length} campanha{campaigns.length !== 1 ? 's' : ''} encontrada{campaigns.length !== 1 ? 's' : ''}
</p>
<p className="text-xs" style={{ color: 'var(--muted-fg)' }}>
Auto-refresh: 60s Dados via Meta Marketing API v20.0
</p>
</div>
)}
{/* Loading overlay on refresh */}
{refreshing && (
<div className="fixed bottom-6 right-6 flex items-center gap-2 px-4 py-2.5 rounded-xl animate-fade-in" style={{ background: 'rgba(99,102,241,0.15)', border: '1px solid rgba(99,102,241,0.25)' }}>
<Spinner size="sm" className="text-[var(--accent)]" />
<span className="text-xs font-medium" style={{ color: 'var(--accent-soft-fg)' }}>Atualizando...</span>
</div>
)}
</div>
);
}