feat(tutorials): add video training academy management with YouTube parser and live preview

This commit is contained in:
mstfyldz
2026-08-22 13:49:00 +03:00
parent 2d149f1178
commit 7f690cc6d3
29 changed files with 2476 additions and 1034 deletions
+61 -14
View File
@@ -1,34 +1,74 @@
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 projenizin genel görünümü.
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">
{/* Placeholder Stat Cards */}
{[
{ name: 'Toplam Kullanıcı', stat: '1,245' },
{ name: 'Aktif Oturumlar', stat: '42' },
{ name: 'Yeni Kayıtlar', stat: '8' },
{ name: 'Sistem Durumu', stat: 'Online' },
].map((item) => (
<div
{stats.map((item) => (
<Link
key={item.name}
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"
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>
</div>
</Link>
))}
</div>
@@ -36,9 +76,16 @@ export default async function AdminDashboardPage() {
<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">
<div className="py-4 text-sm text-gray-500">
Henüz aktivite kaydı bulunmuyor.
</div>
{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>