feat: implement market page with QueryProvider and dynamic listings
- 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)
This commit is contained in:
1 parent
55eebde0ba
commit
ab37b477df
106 files changed
+15261
-301
No files matched your search
@@ -0,0 +1,269 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user