Files
nextjs-boilerplate/app/[locale]/admin/page.tsx
T

95 lines
4.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { auth } from '@/lib/auth'
import { supabaseAdmin } from '@/lib/supabaseAdmin'
import Link from 'next/link'
function timeAgo(iso: string) {
const diffMs = Date.now() - new Date(iso).getTime()
const mins = Math.floor(diffMs / 60000)
if (mins < 1) return 'az önce'
if (mins < 60) return `${mins} dk önce`
const hours = Math.floor(mins / 60)
if (hours < 24) return `${hours} sa önce`
const days = Math.floor(hours / 24)
return `${days} gün önce`
}
export default async function AdminDashboardPage() {
const session = await auth()
const now = new Date().toISOString()
const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString()
const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString()
const [
{ count: totalUsers },
{ count: activeLicenses },
{ count: trialExpiringSoon },
{ count: crashes24h },
{ data: recentCrashes },
{ data: recentLicenseEvents },
] = await Promise.all([
supabaseAdmin.from('profiles').select('id', { count: 'exact', head: true }),
supabaseAdmin.from('licenses').select('id', { count: 'exact', head: true }).eq('status', 'active'),
supabaseAdmin.from('profiles').select('id', { count: 'exact', head: true }).eq('license_status', 'trial').gte('trial_end_date', now).lte('trial_end_date', new Date(Date.now() + 3 * 24 * 60 * 60 * 1000).toISOString()),
supabaseAdmin.from('crash_reports').select('id', { count: 'exact', head: true }).gte('created_at', oneDayAgo),
supabaseAdmin.from('crash_reports').select('id, error_message, severity, app_version, created_at').order('created_at', { ascending: false }).limit(5),
supabaseAdmin.from('license_events').select('id, event_type, license_id, created_at').order('created_at', { ascending: false }).limit(5),
])
const activity = [
...(recentCrashes || []).map((c) => ({ id: c.id, text: `Hata: ${c.error_message || c.severity} (v${c.app_version})`, created_at: c.created_at, tone: 'danger' as const })),
...(recentLicenseEvents || []).map((e) => ({ id: e.id, text: `Lisans olayı: ${e.event_type}`, created_at: e.created_at, tone: 'neutral' as const })),
].sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()).slice(0, 8)
const stats = [
{ name: 'Toplam Kullanıcı', stat: totalUsers ?? 0, href: '/admin/users' },
{ name: 'Aktif Lisans', stat: activeLicenses ?? 0, href: '/admin/licenses' },
{ name: 'Denemesi 3 Gün İçinde Bitecek', stat: trialExpiringSoon ?? 0, href: '/admin/users' },
{ name: 'Son 24 Saatte Hata', stat: crashes24h ?? 0, href: '/admin/logs' },
]
return (
<div className="space-y-6">
<div>
<h2 className="text-2xl font-bold tracking-tight text-gray-900 dark:text-white">Dashboard</h2>
<p className="text-gray-500 dark:text-gray-400 mt-2">
Hoş geldiniz, {session?.user?.name || session?.user?.email}. İşte AyrisLegal&apos;ın canlı durumu.
</p>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
{stats.map((item) => (
<Link
key={item.name}
href={item.href}
className="overflow-hidden rounded-lg bg-white dark:bg-gray-950 border border-gray-200 dark:border-gray-800 px-4 py-5 shadow-sm sm:p-6 hover:border-gray-300 dark:hover:border-gray-700 transition-colors"
>
<dt className="truncate text-sm font-medium text-gray-500 dark:text-gray-400">{item.name}</dt>
<dd className="mt-1 text-3xl font-semibold tracking-tight text-gray-900 dark:text-white">
{item.stat}
</dd>
</Link>
))}
</div>
<div className="rounded-lg bg-white dark:bg-gray-950 border border-gray-200 dark:border-gray-800 shadow-sm">
<div className="p-6">
<h3 className="text-lg font-medium text-gray-900 dark:text-white">Son Aktiviteler</h3>
<div className="mt-4 border-t border-gray-100 dark:border-gray-800">
{activity.length === 0 ? (
<div className="py-4 text-sm text-gray-500">Henüz aktivite kaydı bulunmuyor.</div>
) : (
activity.map((a) => (
<div key={a.id} className="py-3 border-b border-gray-100 dark:border-gray-800 last:border-0 flex items-center justify-between gap-4">
<span className={`text-sm ${a.tone === 'danger' ? 'text-red-600 dark:text-red-400' : 'text-gray-700 dark:text-gray-300'}`}>{a.text}</span>
<span className="text-xs text-gray-400 whitespace-nowrap">{timeAgo(a.created_at)}</span>
</div>
))
)}
</div>
</div>
</div>
</div>
)
}