feat: initialize Prisma schema and scaffold core application modules including auth, blogs, events, and business directories
Some checks failed
Build and Push App / build (push) Failing after 47s
Some checks failed
Build and Push App / build (push) Failing after 47s
This commit is contained in:
@@ -125,6 +125,26 @@ async function getMetrics(range: string = 'week', from?: string, to?: string) {
|
||||
prevMobileCount = prevDevices.find(d => d.device === 'Mobile')?._count.id || 0;
|
||||
}
|
||||
|
||||
// 5. Recent Unique IPs
|
||||
const recentUniqueIPs = await prisma.analyticsEvent.groupBy({
|
||||
by: ['ip'],
|
||||
where: currentWhere,
|
||||
_max: { createdAt: true },
|
||||
orderBy: { _max: { createdAt: 'desc' } },
|
||||
take: 20
|
||||
});
|
||||
|
||||
const recentIPs = await Promise.all(
|
||||
recentUniqueIPs.map(async (item) => {
|
||||
return await prisma.analyticsEvent.findFirst({
|
||||
where: {
|
||||
ip: item.ip,
|
||||
createdAt: item._max.createdAt
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
const currentMobileCount = devices.find(d => d.device === 'Mobile')?._count.id || 0;
|
||||
|
||||
return {
|
||||
@@ -134,6 +154,7 @@ async function getMetrics(range: string = 'week', from?: string, to?: string) {
|
||||
browsers,
|
||||
totalEvents,
|
||||
uniqueVisitors,
|
||||
recentIPs,
|
||||
deltas: {
|
||||
events: prevTotalEvents > 0 ? ((totalEvents - prevTotalEvents) / prevTotalEvents) * 100 : null,
|
||||
visitors: prevUniqueVisitors > 0 ? ((uniqueVisitors - prevUniqueVisitors) / prevUniqueVisitors) * 100 : null,
|
||||
@@ -147,7 +168,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, deltas } = await getMetrics(range, searchParams.from, searchParams.to);
|
||||
const { topPages, topCountries, devices, browsers, totalEvents, uniqueVisitors, recentIPs, deltas } = await getMetrics(range, searchParams.from, searchParams.to);
|
||||
|
||||
const rangeLabels: Record<string, string> = {
|
||||
day: 'Dernières 24h',
|
||||
@@ -331,7 +352,7 @@ export default async function MetricsPage({ searchParams: searchParamsPromise }:
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Top Pages Table (Moved and compact) */}
|
||||
{/* Top Pages Table (Restored) */}
|
||||
<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">
|
||||
@@ -357,6 +378,70 @@ export default async function MetricsPage({ searchParams: searchParamsPromise }:
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Recent IP Connections 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">
|
||||
<Users className="w-5 h-5 text-indigo-400" /> Dernières Connexions (IP)
|
||||
</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">Adresse IP</th>
|
||||
<th className="px-6 py-4">Localisation</th>
|
||||
<th className="px-6 py-4 text-center">Appareil / OS</th>
|
||||
<th className="px-6 py-4">Navigateur</th>
|
||||
<th className="px-6 py-4">Dernière Page</th>
|
||||
<th className="px-6 py-4 text-right">Date / Heure</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-800">
|
||||
{recentIPs.map((e, idx) => e && (
|
||||
<tr key={`${e.ip}-${idx}`} className="hover:bg-slate-800/30 transition-colors">
|
||||
<td className="px-6 py-4">
|
||||
<span className="font-mono text-xs text-indigo-400 bg-indigo-400/5 px-2 py-1 rounded border border-indigo-400/10 whitespace-nowrap">
|
||||
{e.ip || '---.---.---.---'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm text-slate-200 font-bold">{e.country || 'Inconnu'}</span>
|
||||
<span className="text-[10px] text-slate-500">{e.city || '---'}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-center">
|
||||
<div className="inline-flex flex-col items-center">
|
||||
<span className={`text-[10px] px-1.5 py-0.5 rounded-full font-bold mb-1 ${
|
||||
e.device === 'Mobile' ? 'bg-amber-500/10 text-amber-500' : 'bg-blue-500/10 text-blue-500'
|
||||
}`}>
|
||||
{e.device}
|
||||
</span>
|
||||
<span className="text-[10px] text-slate-400">{e.os}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-slate-300">
|
||||
{e.browser}
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<span className="text-xs font-mono text-slate-400 truncate max-w-[150px] block" title={e.path}>
|
||||
{e.path}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-right text-xs whitespace-nowrap">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-slate-300">{new Date(e.createdAt).toLocaleDateString('fr-FR')}</span>
|
||||
<span className="text-slate-500">{new Date(e.createdAt).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user