feat: implement event submission flow with image upload and modal interface
This commit is contained in:
@@ -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 où les pages ont été 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>
|
||||
|
||||
Reference in New Issue
Block a user