- wrap root layout with QueryProvider for React Query - replace market UnderConstruction placeholder with MarketClient and real data fetching - implement search, pagination, and sort via search params using getActiveListings - add necessary client components and queries - update dependencies (e.g., prisma, pkg-types)
270 lines
7.9 KiB
TypeScript
270 lines
7.9 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
import { useSession } from "@/lib/hooks/use-session";
|
|
import { TrendingUp, Eye, MousePointerClick, Heart, MessageSquare, Clock } from "lucide-react";
|
|
|
|
interface AdAnalytics {
|
|
ad_id: number;
|
|
impressions: number;
|
|
clicks: number;
|
|
ctr: number;
|
|
avg_view_duration: number;
|
|
bounce_rate: number;
|
|
inquiries: number;
|
|
favorites: number;
|
|
conversion_rate: number;
|
|
time_to_sell: number | null;
|
|
last_updated: Date;
|
|
}
|
|
|
|
interface Listing {
|
|
listing_id: number;
|
|
item_name: string;
|
|
listing_status: string;
|
|
}
|
|
|
|
export function AnalyticsDashboard() {
|
|
const { user } = useSession();
|
|
const [listings, setListings] = useState<Listing[]>([]);
|
|
const [selectedAd, setSelectedAd] = useState<number | null>(null);
|
|
const [analytics, setAnalytics] = useState<AdAnalytics | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
if (user?.userId) {
|
|
loadUserListings();
|
|
}
|
|
}, [user?.userId]);
|
|
|
|
useEffect(() => {
|
|
if (selectedAd) {
|
|
loadAnalytics(selectedAd);
|
|
}
|
|
}, [selectedAd]);
|
|
|
|
const loadUserListings = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const res = await fetch("/api/listings/my");
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
setListings(data.listings || []);
|
|
if (data.listings && data.listings.length > 0) {
|
|
setSelectedAd(data.listings[0].listing_id);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error("Erro ao carregar anúncios:", error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const loadAnalytics = async (adId: number) => {
|
|
try {
|
|
const res = await fetch(`/api/telemetry/analytics?adId=${adId}`);
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
setAnalytics(data.analytics);
|
|
}
|
|
} catch (error) {
|
|
console.error("Erro ao carregar analytics:", error);
|
|
}
|
|
};
|
|
|
|
if (!user) {
|
|
return (
|
|
<div className="text-center py-12">
|
|
<p className="text-muted-foreground">
|
|
Faça login para ver suas analytics
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="space-y-6">
|
|
<Skeleton className="h-8 w-64" />
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
{Array.from({ length: 6 }).map((_, i) => (
|
|
<Skeleton key={i} className="h-32" />
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (listings.length === 0) {
|
|
return (
|
|
<div className="text-center py-12">
|
|
<p className="text-muted-foreground">
|
|
Você ainda não tem anúncios. Crie um anúncio para ver as analytics.
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div>
|
|
<h1 className="text-3xl font-bold">Analytics dos Anúncios</h1>
|
|
<p className="text-muted-foreground mt-1">
|
|
Acompanhe o desempenho dos seus anúncios
|
|
</p>
|
|
</div>
|
|
|
|
{/* Seletor de anúncio */}
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium">Selecione um anúncio</label>
|
|
<select
|
|
className="w-full p-2 border rounded-md"
|
|
value={selectedAd || ""}
|
|
onChange={(e) => setSelectedAd(parseInt(e.target.value, 10))}
|
|
>
|
|
{listings.map((listing) => (
|
|
<option key={listing.listing_id} value={listing.listing_id}>
|
|
{listing.item_name} ({listing.listing_status})
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
{/* Métricas */}
|
|
{analytics && (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Eye className="h-5 w-5" />
|
|
Impressões
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-3xl font-bold">{analytics.impressions}</div>
|
|
<p className="text-sm text-muted-foreground mt-1">
|
|
Visualizações do anúncio
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<MousePointerClick className="h-5 w-5" />
|
|
Cliques
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-3xl font-bold">{analytics.clicks}</div>
|
|
<p className="text-sm text-muted-foreground mt-1">
|
|
CTR: {analytics.ctr.toFixed(2)}%
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Clock className="h-5 w-5" />
|
|
Duração Média
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-3xl font-bold">
|
|
{analytics.avg_view_duration.toFixed(1)}s
|
|
</div>
|
|
<p className="text-sm text-muted-foreground mt-1">
|
|
Tempo médio de visualização
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<TrendingUp className="h-5 w-5" />
|
|
Bounce Rate
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-3xl font-bold">
|
|
{analytics.bounce_rate.toFixed(1)}%
|
|
</div>
|
|
<p className="text-sm text-muted-foreground mt-1">
|
|
Usuários que saíram em menos de 3s
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<MessageSquare className="h-5 w-5" />
|
|
Inquiries
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-3xl font-bold">{analytics.inquiries}</div>
|
|
<p className="text-sm text-muted-foreground mt-1">
|
|
Mensagens recebidas
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Heart className="h-5 w-5" />
|
|
Favoritos
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-3xl font-bold">{analytics.favorites}</div>
|
|
<p className="text-sm text-muted-foreground mt-1">
|
|
Usuários que favoritaram
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{analytics.conversion_rate > 0 && (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Taxa de Conversão</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-3xl font-bold">
|
|
{analytics.conversion_rate.toFixed(2)}%
|
|
</div>
|
|
<p className="text-sm text-muted-foreground mt-1">
|
|
Taxa de vendas
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{analytics.time_to_sell && (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Tempo até Venda</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-3xl font-bold">
|
|
{Math.round(analytics.time_to_sell)}h
|
|
</div>
|
|
<p className="text-sm text-muted-foreground mt-1">
|
|
Tempo médio até ser vendido
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|