maj metric article

This commit is contained in:
2026-06-21 17:24:31 +02:00
parent ecfe7fb433
commit af6ec84cd0

View File

@@ -14,7 +14,8 @@ import {
ArrowUp, ArrowUp,
ArrowDown, ArrowDown,
Calendar, Calendar,
Map as MapIcon Map as MapIcon,
BookOpen
} from 'lucide-react'; } from 'lucide-react';
import AnalyticsMap from '@/components/AnalyticsMap'; import AnalyticsMap from '@/components/AnalyticsMap';
import CountryHistogram from '@/components/CountryHistogram'; import CountryHistogram from '@/components/CountryHistogram';
@@ -123,10 +124,63 @@ async function getMetrics(range: string = 'week', from?: string, to?: string) {
}); });
const uniqueVisitors = uniqueVisitorsRes.length; const uniqueVisitors = uniqueVisitorsRes.length;
// Article page views (impressions)
const pageViewsByPath = await prisma.analyticsEvent.groupBy({
by: ['path'],
where: {
type: 'PAGE_VIEW',
path: { startsWith: '/actualites/' },
...currentWhere
},
_count: { id: true }
});
// Article dwell times (reading times)
const dwellTimeByPath = await prisma.analyticsEvent.groupBy({
by: ['path'],
where: {
type: 'DWELL_TIME',
path: { startsWith: '/actualites/' },
...currentWhere
},
_avg: { value: true },
_count: { id: true }
});
// Article unique visitors grouping
const uniqueVisitorsRaw = await prisma.analyticsEvent.groupBy({
by: ['path', 'ip'],
where: {
type: 'PAGE_VIEW',
path: { startsWith: '/actualites/' },
...currentWhere
}
});
// Load blog posts to associate with analytics
const blogPosts = await prisma.blogPost.findMany({
select: {
id: true,
title: true,
slug: true,
author: true,
publishedAt: true,
}
});
const totalArticleViews = await prisma.analyticsEvent.count({
where: {
type: 'PAGE_VIEW',
path: { startsWith: '/actualites/' },
...currentWhere
}
});
// 4. Fetch Previous Data (for Deltas) // 4. Fetch Previous Data (for Deltas)
let prevTotalEvents = 0; let prevTotalEvents = 0;
let prevUniqueVisitors = 0; let prevUniqueVisitors = 0;
let prevMobileCount = 0; let prevMobileCount = 0;
let prevArticleViews = 0;
if (prevWhere) { if (prevWhere) {
prevTotalEvents = await prisma.analyticsEvent.count({ where: prevWhere }); prevTotalEvents = await prisma.analyticsEvent.count({ where: prevWhere });
@@ -142,6 +196,14 @@ async function getMetrics(range: string = 'week', from?: string, to?: string) {
_count: { id: true } _count: { id: true }
}); });
prevMobileCount = prevDevices.find(d => d.device === 'Mobile')?._count.id || 0; prevMobileCount = prevDevices.find(d => d.device === 'Mobile')?._count.id || 0;
prevArticleViews = await prisma.analyticsEvent.count({
where: {
type: 'PAGE_VIEW',
path: { startsWith: '/actualites/' },
...prevWhere
}
});
} }
// 5. Recent Unique IPs // 5. Recent Unique IPs
@@ -176,9 +238,52 @@ async function getMetrics(range: string = 'week', from?: string, to?: string) {
uniqueVisitors, uniqueVisitors,
recentIPs, recentIPs,
uniqueVisitorsPerCountry, uniqueVisitorsPerCountry,
totalArticleViews,
articleStats: blogPosts.map(post => {
const paths = [`/actualites/${post.id}`];
if (post.slug) {
paths.push(`/actualites/${post.slug}`);
}
let views = 0;
pageViewsByPath.forEach(item => {
if (paths.includes(item.path)) {
views += item._count.id;
}
});
const uniqueIps = new Set<string>();
uniqueVisitorsRaw.forEach(item => {
if (paths.includes(item.path) && item.ip) {
uniqueIps.add(item.ip);
}
});
const visitors = uniqueIps.size;
let totalDwellTime = 0;
let dwellCount = 0;
dwellTimeByPath.forEach(item => {
if (paths.includes(item.path) && item._avg.value && item._count.id) {
totalDwellTime += item._avg.value * item._count.id;
dwellCount += item._count.id;
}
});
const avgDwellTime = dwellCount > 0 ? Math.round(totalDwellTime / dwellCount) : 0;
return {
id: post.id,
title: post.title,
author: post.author,
publishedAt: post.publishedAt,
views,
visitors,
avgDwellTime
};
}).sort((a, b) => b.views - a.views),
deltas: { deltas: {
events: prevTotalEvents > 0 ? ((totalEvents - prevTotalEvents) / prevTotalEvents) * 100 : null, events: prevTotalEvents > 0 ? ((totalEvents - prevTotalEvents) / prevTotalEvents) * 100 : null,
visitors: prevUniqueVisitors > 0 ? ((uniqueVisitors - prevUniqueVisitors) / prevUniqueVisitors) * 100 : null, visitors: prevUniqueVisitors > 0 ? ((uniqueVisitors - prevUniqueVisitors) / prevUniqueVisitors) * 100 : null,
articleViews: prevArticleViews > 0 ? ((totalArticleViews - prevArticleViews) / prevArticleViews) * 100 : null,
mobile: prevTotalEvents > 0 && totalEvents > 0 mobile: prevTotalEvents > 0 && totalEvents > 0
? ((currentMobileCount / totalEvents) - (prevMobileCount / prevTotalEvents)) * 100 ? ((currentMobileCount / totalEvents) - (prevMobileCount / prevTotalEvents)) * 100
: null : null
@@ -189,7 +294,7 @@ async function getMetrics(range: string = 'week', from?: string, to?: string) {
export default async function MetricsPage({ searchParams: searchParamsPromise }: { searchParams: Promise<{ range?: string, from?: string, to?: string }> }) { export default async function MetricsPage({ searchParams: searchParamsPromise }: { searchParams: Promise<{ range?: string, from?: string, to?: string }> }) {
const searchParams = await searchParamsPromise; const searchParams = await searchParamsPromise;
const range = searchParams.range || 'week'; const range = searchParams.range || 'week';
const { topPages, topCountries, devices, browsers, totalEvents, uniqueVisitors, recentIPs, deltas, uniqueVisitorsPerCountry } = await getMetrics(range, searchParams.from, searchParams.to); const { topPages, topCountries, devices, browsers, totalEvents, uniqueVisitors, recentIPs, deltas, uniqueVisitorsPerCountry, totalArticleViews, articleStats } = await getMetrics(range, searchParams.from, searchParams.to);
const rangeLabels: Record<string, string> = { const rangeLabels: Record<string, string> = {
day: 'Dernières 24h', day: 'Dernières 24h',
@@ -250,7 +355,7 @@ export default async function MetricsPage({ searchParams: searchParamsPromise }:
</div> </div>
{/* Summary Cards */} {/* Summary Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<div className="card border-t-4 border-indigo-500"> <div className="card border-t-4 border-indigo-500">
<div className="flex justify-between items-start mb-4"> <div className="flex justify-between items-start mb-4">
<div className="p-2 rounded-lg bg-indigo-500/10 text-indigo-400"> <div className="p-2 rounded-lg bg-indigo-500/10 text-indigo-400">
@@ -283,6 +388,22 @@ export default async function MetricsPage({ searchParams: searchParamsPromise }:
</p> </p>
</div> </div>
<div className="card border-t-4 border-rose-500">
<div className="flex justify-between items-start mb-4">
<div className="p-2 rounded-lg bg-rose-500/10 text-rose-400">
<BookOpen className="w-6 h-6" />
</div>
<div className="text-right">
<div className="text-2xl font-bold text-white">{totalArticleViews.toLocaleString()}</div>
<ComparisonBadge value={deltas.articleViews} />
</div>
</div>
<h3 className="text-slate-400 text-sm font-medium">Articles consultés</h3>
<p className="text-[10px] text-slate-500 uppercase mt-1 font-bold tracking-wider">
Vues totales des articles de blog
</p>
</div>
<div className="card border-t-4 border-amber-500"> <div className="card border-t-4 border-amber-500">
<div className="flex justify-between items-start mb-4"> <div className="flex justify-between items-start mb-4">
<div className="p-2 rounded-lg bg-amber-500/10 text-amber-400"> <div className="p-2 rounded-lg bg-amber-500/10 text-amber-400">
@@ -426,6 +547,53 @@ export default async function MetricsPage({ searchParams: searchParamsPromise }:
</table> </table>
</div> </div>
{/* Article Statistics Table */}
<div className="card p-0 overflow-hidden lg:col-span-2">
<div className="p-6 border-b border-slate-800 flex items-center justify-between">
<h2 className="text-xl font-bold text-white flex items-center gap-2">
<BookOpen className="w-5 h-5 text-rose-400" /> Statistiques par article
</h2>
</div>
<div className="overflow-x-auto">
<table className="w-full text-left">
<thead className="bg-slate-900/50 text-xs font-semibold text-slate-500 uppercase tracking-wider">
<tr>
<th className="px-6 py-4">Article</th>
<th className="px-6 py-4">Auteur</th>
<th className="px-6 py-4 text-right">Vues</th>
<th className="px-6 py-4 text-right">V. Uniques</th>
<th className="px-6 py-4 text-right">Temps de lecture moyen</th>
<th className="px-6 py-4 text-right">Date de publication</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800">
{articleStats.map((post) => (
<tr key={post.id} className="hover:bg-slate-800/30 transition-colors text-xs">
<td className="px-6 py-4 font-semibold text-slate-200">
{post.title}
</td>
<td className="px-6 py-4 text-slate-400">
{post.author}
</td>
<td className="px-6 py-4 text-right font-bold text-white">
{post.views.toLocaleString()}
</td>
<td className="px-6 py-4 text-right font-bold text-indigo-400">
{post.visitors.toLocaleString()}
</td>
<td className="px-6 py-4 text-right text-slate-300 font-mono">
{post.avgDwellTime > 0 ? `${post.avgDwellTime}s` : '---'}
</td>
<td className="px-6 py-4 text-right text-slate-400">
{post.publishedAt ? new Date(post.publishedAt).toLocaleDateString('fr-FR') : 'Non publié'}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* Recent IP Connections Table */} {/* Recent IP Connections Table */}
<div className="card p-0 overflow-hidden lg:col-span-2"> <div className="card p-0 overflow-hidden lg:col-span-2">
<div className="p-6 border-b border-slate-800 flex items-center justify-between"> <div className="p-6 border-b border-slate-800 flex items-center justify-between">