1. Système d'Analytics (Nouveauté Majeure)
Modèle de Données : Ajout de la table AnalyticsEvent dans prisma/schema.prisma pour suivre les vues de pages, les clics et le temps de rétention (DWELL_TIME). API Backend : Création de la route app/api/analytics/route.ts pour enregistrer les événements en base de données. Tracking Client : Création du composant AnalyticsTracker.tsx (intégré dans le layout principal) qui capture automatiquement : Le changement de page (Page Views). Les clics sur les boutons et liens importants. Le temps passé sur chaque page. 2. Intégration Base de Données (PostgreSQL/Prisma) Fiches Entreprises : Mise à jour de app/directory/[id]/page.tsx pour récupérer les données réelles depuis PostgreSQL via Prisma au lieu d'utiliser les données de test (mockData). Navigation Dashboard : Correction du bouton "Voir ma fiche" dans le tableau de bord pour qu'il redirige correctement vers le profil public de l'entrepreneur. 3. Administration & Metrics Nouvelle Application Admin : Initialisation d'un dossier admin/ (application Next.js séparée) destinée à la gestion de la plateforme et à la visualisation des statistiques (Metrics & Analytics). 4. Authentification & Inscription Nouvelle Page : Création de app/register/page.tsx pour permettre l'inscription des nouveaux utilisateurs. Gestion de Session : Amélioration de UserProvider.tsx pour une meilleure gestion de l'état utilisateur à travers l'application. 5. Maintenance & Types Mise à jour des types globaux dans types.ts et rafraîchissement du package-lock.json suite à l'ajout de dépendances.
This commit is contained in:
205
admin/src/app/metrics/page.tsx
Normal file
205
admin/src/app/metrics/page.tsx
Normal file
@@ -0,0 +1,205 @@
|
||||
import { prisma } from '@/lib/prisma';
|
||||
import {
|
||||
BarChart3,
|
||||
MousePointer2,
|
||||
Clock,
|
||||
Eye,
|
||||
ArrowUpRight,
|
||||
TrendingUp
|
||||
} from 'lucide-react';
|
||||
|
||||
async function getMetrics() {
|
||||
// 1. Top Pages
|
||||
const topPages = await prisma.analyticsEvent.groupBy({
|
||||
by: ['path'],
|
||||
where: { type: 'PAGE_VIEW' },
|
||||
_count: {
|
||||
id: true
|
||||
},
|
||||
orderBy: {
|
||||
_count: {
|
||||
id: 'desc'
|
||||
}
|
||||
},
|
||||
take: 10
|
||||
});
|
||||
|
||||
// 2. Top Clicks
|
||||
const topClicks = await prisma.analyticsEvent.groupBy({
|
||||
by: ['label'],
|
||||
where: { type: 'CLICK' },
|
||||
_count: {
|
||||
id: true
|
||||
},
|
||||
orderBy: {
|
||||
_count: {
|
||||
id: 'desc'
|
||||
}
|
||||
},
|
||||
take: 10
|
||||
});
|
||||
|
||||
// 3. Average Dwell Time
|
||||
const dwellTime = await prisma.analyticsEvent.groupBy({
|
||||
by: ['path'],
|
||||
where: { type: 'DWELL_TIME' },
|
||||
_avg: {
|
||||
value: true
|
||||
},
|
||||
_count: {
|
||||
id: true
|
||||
},
|
||||
orderBy: {
|
||||
_avg: {
|
||||
value: 'desc'
|
||||
}
|
||||
},
|
||||
take: 10
|
||||
});
|
||||
|
||||
// 4. Global Stats
|
||||
const totalEvents = await prisma.analyticsEvent.count();
|
||||
const uniquePaths = (await prisma.analyticsEvent.groupBy({ by: ['path'] })).length;
|
||||
|
||||
return { topPages, topClicks, dwellTime, totalEvents, uniquePaths };
|
||||
}
|
||||
|
||||
export default async function MetricsPage() {
|
||||
const { topPages, topClicks, dwellTime, totalEvents, uniquePaths } = await getMetrics();
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="flex justify-between items-end">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-white mb-2">Metrics & Analytics</h1>
|
||||
<p className="text-slate-400">Comportement des utilisateurs et engagement en temps réel.</p>
|
||||
</div>
|
||||
<div className="bg-slate-800/50 border border-slate-700 px-4 py-2 rounded-lg text-xs font-mono text-slate-400">
|
||||
Capture : <span className="text-emerald-400">{totalEvents.toLocaleString()}</span> événements total
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Summary Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div className="card">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div className="p-2 rounded-lg bg-indigo-500/10 text-indigo-400">
|
||||
<Eye className="w-6 h-6" />
|
||||
</div>
|
||||
<span className="text-2xl font-bold text-white">{uniquePaths}</span>
|
||||
</div>
|
||||
<h3 className="text-slate-400 font-medium">Pages uniques explorées</h3>
|
||||
</div>
|
||||
<div className="card">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div className="p-2 rounded-lg bg-pink-500/10 text-pink-400">
|
||||
<MousePointer2 className="w-6 h-6" />
|
||||
</div>
|
||||
<span className="text-2xl font-bold text-white">
|
||||
{topClicks.reduce((acc, curr) => acc + curr._count.id, 0)}
|
||||
</span>
|
||||
</div>
|
||||
<h3 className="text-slate-400 font-medium">Clicks enregistrés (Top 10)</h3>
|
||||
</div>
|
||||
<div className="card">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div className="p-2 rounded-lg bg-amber-500/10 text-amber-400">
|
||||
<Clock className="w-6 h-6" />
|
||||
</div>
|
||||
<span className="text-2xl font-bold text-white">
|
||||
{Math.round(dwellTime.reduce((acc, curr) => acc + (curr._avg.value || 0), 0) / (dwellTime.length || 1))}s
|
||||
</span>
|
||||
</div>
|
||||
<h3 className="text-slate-400 font-medium">Temps moyen / page</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
{/* Top Pages Table */}
|
||||
<div className="card p-0 overflow-hidden">
|
||||
<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">
|
||||
<TrendingUp className="w-5 h-5 text-emerald-400" /> Pages les plus visitées
|
||||
</h2>
|
||||
</div>
|
||||
<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">URL / Chemin</th>
|
||||
<th className="px-6 py-4 text-right">Vues</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-800">
|
||||
{topPages.map((page) => (
|
||||
<tr key={page.path} className="hover:bg-slate-800/30 transition-colors">
|
||||
<td className="px-6 py-4 text-sm font-mono text-slate-300">{page.path}</td>
|
||||
<td className="px-6 py-4 text-right text-sm font-bold text-white">
|
||||
{page._count.id}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Top Clicks Table */}
|
||||
<div className="card p-0 overflow-hidden">
|
||||
<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">
|
||||
<MousePointer2 className="w-5 h-5 text-indigo-400" /> Actions & Clicks
|
||||
</h2>
|
||||
</div>
|
||||
<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">Élément / Texte</th>
|
||||
<th className="px-6 py-4 text-right">Interactions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-800">
|
||||
{topClicks.map((click) => (
|
||||
<tr key={click.label} className="hover:bg-slate-800/30 transition-colors">
|
||||
<td className="px-6 py-4 text-sm text-slate-300 truncate max-w-xs">{click.label || 'Sans label'}</td>
|
||||
<td className="px-6 py-4 text-right text-sm font-bold text-white">
|
||||
{click._count.id}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Dwell Time 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">
|
||||
<Clock className="w-5 h-5 text-amber-400" /> Temps d'attention par page
|
||||
</h2>
|
||||
</div>
|
||||
<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">Page</th>
|
||||
<th className="px-6 py-4">Échantillon</th>
|
||||
<th className="px-6 py-4 text-right">Moyenne de rétention</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-800">
|
||||
{dwellTime.map((time) => (
|
||||
<tr key={time.path} className="hover:bg-slate-800/30 transition-colors">
|
||||
<td className="px-6 py-4 text-sm font-mono text-slate-300">{time.path}</td>
|
||||
<td className="px-6 py-4 text-xs text-slate-500">{time._count.id} sessions</td>
|
||||
<td className="px-6 py-4 text-right">
|
||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-amber-500/10 text-amber-400 border border-amber-500/20">
|
||||
{Math.round(time._avg.value || 0)} secondes
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user