feat(tutorials): add video training academy management with YouTube parser and live preview
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
|
||||
interface CrashRow {
|
||||
id: string
|
||||
event_type: string | null
|
||||
severity: string
|
||||
error_name: string | null
|
||||
error_message: string | null
|
||||
module: string | null
|
||||
app_version: string | null
|
||||
os: string | null
|
||||
device_id: string | null
|
||||
created_at: string
|
||||
}
|
||||
|
||||
interface LicenseEventRow {
|
||||
id: string
|
||||
event_type: string
|
||||
license_id: string
|
||||
device_id: string | null
|
||||
ip_address: string | null
|
||||
app_version: string | null
|
||||
created_at: string
|
||||
}
|
||||
|
||||
function formatDateTime(iso: string) {
|
||||
return new Date(iso).toLocaleString('tr-TR', { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' })
|
||||
}
|
||||
|
||||
function severityBadge(severity: string) {
|
||||
const map: Record<string, string> = {
|
||||
fatal: 'bg-red-100 text-red-700 border-red-300 dark:bg-red-950/60 dark:text-red-400 dark:border-red-900',
|
||||
error: 'bg-red-50 text-red-700 border-red-200 dark:bg-red-950/40 dark:text-red-400 dark:border-red-900',
|
||||
warning: 'bg-amber-50 text-amber-700 border-amber-200 dark:bg-amber-950/40 dark:text-amber-400 dark:border-amber-900',
|
||||
info: 'bg-blue-50 text-blue-700 border-blue-200 dark:bg-blue-950/40 dark:text-blue-400 dark:border-blue-900',
|
||||
}
|
||||
const cls = map[severity] || 'bg-gray-50 text-gray-600 border-gray-200 dark:bg-gray-900 dark:text-gray-400 dark:border-gray-800'
|
||||
return <span className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium border shrink-0 ${cls}`}>{severity}</span>
|
||||
}
|
||||
|
||||
function TabButton({ active, onClick, children, count }: { active: boolean; onClick: () => void; children: React.ReactNode; count: number }) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={`px-4 py-2.5 text-sm font-medium border-b-2 transition-colors -mb-px ${
|
||||
active
|
||||
? 'border-indigo-600 text-indigo-600 dark:text-indigo-400'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-900 dark:hover:text-white'
|
||||
}`}
|
||||
>
|
||||
{children} <span className="text-xs text-gray-400">({count})</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export default function LogsTabs({ crashes, licenseEvents }: { crashes: CrashRow[]; licenseEvents: LicenseEventRow[] }) {
|
||||
const [tab, setTab] = useState<'crashes' | 'events'>('crashes')
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="border-b border-gray-200 dark:border-gray-800 flex gap-2 mb-4">
|
||||
<TabButton active={tab === 'crashes'} onClick={() => setTab('crashes')} count={crashes.length}>
|
||||
Hata Kayıtları
|
||||
</TabButton>
|
||||
<TabButton active={tab === 'events'} onClick={() => setTab('events')} count={licenseEvents.length}>
|
||||
Lisans Olayları
|
||||
</TabButton>
|
||||
</div>
|
||||
|
||||
{tab === 'crashes' && (
|
||||
<div className="rounded-lg bg-white dark:bg-gray-950 border border-gray-200 dark:border-gray-800 shadow-sm overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-800">
|
||||
<thead className="bg-gray-50 dark:bg-gray-900">
|
||||
<tr>
|
||||
{['Zaman', 'Önem', 'Tür', 'Hata', 'Modül', 'Sürüm / OS'].map((h) => (
|
||||
<th key={h} className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">{h}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-100 dark:divide-gray-800">
|
||||
{crashes.map((c) => (
|
||||
<tr key={c.id} className="hover:bg-gray-50 dark:hover:bg-gray-900/50">
|
||||
<td className="px-4 py-3 whitespace-nowrap text-sm text-gray-500">{formatDateTime(c.created_at)}</td>
|
||||
<td className="px-4 py-3 whitespace-nowrap">{severityBadge(c.severity)}</td>
|
||||
<td className="px-4 py-3 whitespace-nowrap text-sm text-gray-600 dark:text-gray-400">{c.event_type}</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-900 dark:text-white max-w-md truncate" title={c.error_message || undefined}>
|
||||
{c.error_name ? `${c.error_name}: ` : ''}{c.error_message || '—'}
|
||||
</td>
|
||||
<td className="px-4 py-3 whitespace-nowrap text-sm text-gray-500">{c.module || '—'}</td>
|
||||
<td className="px-4 py-3 whitespace-nowrap text-xs text-gray-400">v{c.app_version} · {c.os || '—'}</td>
|
||||
</tr>
|
||||
))}
|
||||
{crashes.length === 0 && (
|
||||
<tr><td colSpan={6} className="px-4 py-8 text-center text-sm text-gray-500">Hata kaydı yok.</td></tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{tab === 'events' && (
|
||||
<div className="rounded-lg bg-white dark:bg-gray-950 border border-gray-200 dark:border-gray-800 shadow-sm overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-800">
|
||||
<thead className="bg-gray-50 dark:bg-gray-900">
|
||||
<tr>
|
||||
{['Zaman', 'Olay', 'Lisans', 'Cihaz', 'IP', 'Sürüm'].map((h) => (
|
||||
<th key={h} className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">{h}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-100 dark:divide-gray-800">
|
||||
{licenseEvents.map((e) => (
|
||||
<tr key={e.id} className="hover:bg-gray-50 dark:hover:bg-gray-900/50">
|
||||
<td className="px-4 py-3 whitespace-nowrap text-sm text-gray-500">{formatDateTime(e.created_at)}</td>
|
||||
<td className="px-4 py-3 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">{e.event_type}</td>
|
||||
<td className="px-4 py-3 whitespace-nowrap text-xs text-gray-400 font-mono">{e.license_id?.slice(0, 8)}…</td>
|
||||
<td className="px-4 py-3 whitespace-nowrap text-xs text-gray-400 font-mono">{e.device_id ? `${e.device_id.slice(0, 8)}…` : '—'}</td>
|
||||
<td className="px-4 py-3 whitespace-nowrap text-sm text-gray-500">{e.ip_address || '—'}</td>
|
||||
<td className="px-4 py-3 whitespace-nowrap text-xs text-gray-400">{e.app_version || '—'}</td>
|
||||
</tr>
|
||||
))}
|
||||
{licenseEvents.length === 0 && (
|
||||
<tr><td colSpan={6} className="px-4 py-8 text-center text-sm text-gray-500">Lisans olayı yok.</td></tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { supabaseAdmin } from '@/lib/supabaseAdmin'
|
||||
import LogsTabs from './LogsTabs'
|
||||
|
||||
export default async function LogsPage() {
|
||||
const [{ data: crashes }, { data: licenseEvents }] = await Promise.all([
|
||||
supabaseAdmin
|
||||
.from('crash_reports')
|
||||
.select('id, event_type, severity, error_name, error_message, module, app_version, os, device_id, created_at')
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(50),
|
||||
supabaseAdmin
|
||||
.from('license_events')
|
||||
.select('id, event_type, license_id, device_id, ip_address, app_version, created_at')
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(50),
|
||||
])
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold tracking-tight text-gray-900 dark:text-white">Loglar</h2>
|
||||
<p className="text-gray-500 dark:text-gray-400 mt-2">Son 50 hata kaydı ve lisans olayı.</p>
|
||||
</div>
|
||||
|
||||
<LogsTabs crashes={crashes || []} licenseEvents={licenseEvents || []} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user