Files
Evandro Fonseca Junior 6adc8d96e8 refactor: reorganize hooks and add safety checks
fix(api): add validation for listing and seller in bid route
feat(telemetry): add userId to MarketTelemetry
style(globe): add null check for canvas element
chore: add lint:fix script to package.json
2025-11-11 03:20:01 -03:00

276 lines
7.9 KiB
TypeScript

"use client";
import {
Clock,
Eye,
Heart,
MessageSquare,
MousePointerClick,
TrendingUp,
} from "lucide-react";
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";
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);
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);
}
};
useEffect(() => {
if (user?.userId) {
loadUserListings();
}
}, [user?.userId, loadUserListings]);
useEffect(() => {
if (selectedAd) {
loadAnalytics(selectedAd);
}
}, [selectedAd, loadAnalytics]);
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>
);
}