feat: implement event submission flow with image upload and modal interface

This commit is contained in:
2026-05-10 21:47:28 +02:00
parent 7db35361d9
commit 197594f84f
31 changed files with 1481 additions and 75 deletions

View File

@@ -13,8 +13,11 @@ import {
Search,
ArrowUp,
ArrowDown,
Calendar
Calendar,
Map as MapIcon
} from 'lucide-react';
import AnalyticsMap from '@/components/AnalyticsMap';
import CountryHistogram from '@/components/CountryHistogram';
function ComparisonBadge({ value, isPercent = true }: { value: number | null, isPercent?: boolean }) {
if (value === null) return null;
@@ -76,12 +79,28 @@ async function getMetrics(range: string = 'week', from?: string, to?: string) {
const topCountries = await prisma.analyticsEvent.groupBy({
by: ['country'],
where: currentWhere,
where: { type: 'PAGE_VIEW', ...currentWhere },
_count: { id: true },
orderBy: { _count: { id: 'desc' } },
take: 10
take: 50 // Increased to get more map data
});
// Unique visitors per country
const uniqueVisitorsPerCountryRaw = await prisma.analyticsEvent.groupBy({
by: ['country', 'ip'],
where: { type: 'PAGE_VIEW', ...currentWhere }
});
const uniqueVisitorsPerCountryMap: Record<string, number> = {};
uniqueVisitorsPerCountryRaw.forEach(item => {
const country = item.country || 'Unknown';
uniqueVisitorsPerCountryMap[country] = (uniqueVisitorsPerCountryMap[country] || 0) + 1;
});
const uniqueVisitorsPerCountry = Object.entries(uniqueVisitorsPerCountryMap)
.map(([country, count]) => ({ country, count }))
.sort((a, b) => b.count - a.count);
const devices = await prisma.analyticsEvent.groupBy({
by: ['device'],
where: currentWhere,
@@ -156,6 +175,7 @@ async function getMetrics(range: string = 'week', from?: string, to?: string) {
totalEvents,
uniqueVisitors,
recentIPs,
uniqueVisitorsPerCountry,
deltas: {
events: prevTotalEvents > 0 ? ((totalEvents - prevTotalEvents) / prevTotalEvents) * 100 : null,
visitors: prevUniqueVisitors > 0 ? ((uniqueVisitors - prevUniqueVisitors) / prevUniqueVisitors) * 100 : null,
@@ -169,7 +189,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 }> }) {
const searchParams = await searchParamsPromise;
const range = searchParams.range || 'week';
const { topPages, topCountries, devices, browsers, totalEvents, uniqueVisitors, recentIPs, deltas } = await getMetrics(range, searchParams.from, searchParams.to);
const { topPages, topCountries, devices, browsers, totalEvents, uniqueVisitors, recentIPs, deltas, uniqueVisitorsPerCountry } = await getMetrics(range, searchParams.from, searchParams.to);
const rangeLabels: Record<string, string> = {
day: 'Dernières 24h',
@@ -237,13 +257,13 @@ export default async function MetricsPage({ searchParams: searchParamsPromise }:
<Users className="w-6 h-6" />
</div>
<div className="text-right">
<div className="text-2xl font-bold text-white">{uniqueVisitors}</div>
<div className="text-2xl font-bold text-white">{uniqueVisitors.toLocaleString()}</div>
<ComparisonBadge value={deltas.visitors} />
</div>
</div>
<h3 className="text-slate-400 text-sm font-medium">Visiteurs uniques (IP)</h3>
<p className="text-[10px] text-slate-500 uppercase mt-1 font-bold tracking-wider">
{searchParams.from ? `Du ${searchParams.from} au ${searchParams.to}` : rangeLabels[range]}
Personnes réelles distinctes
</p>
</div>
@@ -253,13 +273,13 @@ export default async function MetricsPage({ searchParams: searchParamsPromise }:
<Eye className="w-6 h-6" />
</div>
<div className="text-right">
<div className="text-2xl font-bold text-white">{totalEvents}</div>
<div className="text-2xl font-bold text-white">{totalEvents.toLocaleString()}</div>
<ComparisonBadge value={deltas.events} />
</div>
</div>
<h3 className="text-slate-400 text-sm font-medium">Événements enregistrés</h3>
<h3 className="text-slate-400 text-sm font-medium">Impressions (Pages vues)</h3>
<p className="text-[10px] text-slate-500 uppercase mt-1 font-bold tracking-wider">
{searchParams.from ? `Du ${searchParams.from} au ${searchParams.to}` : rangeLabels[range]}
Nombre total de fois les pages ont é vues
</p>
</div>
@@ -275,13 +295,29 @@ export default async function MetricsPage({ searchParams: searchParamsPromise }:
<ComparisonBadge value={deltas.mobile} />
</div>
</div>
<h3 className="text-slate-400 text-sm font-medium">Part du trafic Mobile</h3>
<h3 className="text-slate-400 text-sm font-medium">Trafic Mobile</h3>
<p className="text-[10px] text-slate-500 uppercase mt-1 font-bold tracking-wider">
{searchParams.from ? `Du ${searchParams.from} au ${searchParams.to}` : rangeLabels[range]}
Part des appareils mobiles
</p>
</div>
</div>
{/* World Map Section */}
<AnalyticsMap
data={uniqueVisitorsPerCountry}
title="Répartition Géographique (Visiteurs Uniques)"
/>
{/* Histogram Section */}
<CountryHistogram
title="Comparatif Impressions vs Visiteurs Uniques"
data={topCountries.map(c => ({
country: c.country || 'Inconnu',
impressions: c._count.id,
visitors: uniqueVisitorsPerCountry.find(uv => uv.country === c.country)?.count || 0
}))}
/>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Top Countries Table */}
<div className="card p-0 overflow-hidden">
@@ -294,18 +330,28 @@ export default async function MetricsPage({ searchParams: searchParamsPromise }:
<thead className="bg-slate-900/50 text-xs font-semibold text-slate-500 uppercase tracking-wider">
<tr>
<th className="px-6 py-4">Pays</th>
<th className="px-6 py-4 text-right">Visites</th>
<th className="px-6 py-4 text-right">Impressions</th>
<th className="px-6 py-4 text-right">V. Uniques</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800">
{topCountries.map((c) => (
<tr key={c.country} className="hover:bg-slate-800/30 transition-colors">
<td className="px-6 py-4 text-sm text-slate-300 font-bold">{c.country || 'Inconnu'}</td>
<td className="px-6 py-4 text-right text-sm font-bold text-white">
{c._count.id}
</td>
</tr>
))}
{topCountries.map((c) => {
const uniqueCount = uniqueVisitorsPerCountry.find(uv => uv.country === c.country)?.count || 0;
return (
<tr key={c.country} className="hover:bg-slate-800/30 transition-colors text-xs">
<td className="px-6 py-4 text-slate-300 font-bold flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-indigo-500"></span>
{c.country || 'Inconnu'}
</td>
<td className="px-6 py-4 text-right font-medium text-slate-400">
{c._count.id.toLocaleString()}
</td>
<td className="px-6 py-4 text-right text-sm font-bold text-white">
{uniqueCount.toLocaleString()}
</td>
</tr>
);
})}
</tbody>
</table>
</div>