Services: Admin-Lehrer, Backend-Lehrer, Studio v2, Website, Klausur-Service, School-Service, Voice-Service, Geo-Service, BreakPilot Drive, Agent-Core Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
481 lines
21 KiB
TypeScript
481 lines
21 KiB
TypeScript
'use client'
|
|
|
|
/**
|
|
* Breakpilot Drive - Game Dashboard
|
|
*
|
|
* Admin-Interface fuer das Lernspiel:
|
|
* - Uebersicht: Statistiken und KPIs
|
|
* - Spielen: Embedded WebGL Game
|
|
* - Statistiken: Detaillierte Auswertungen
|
|
*/
|
|
|
|
import AdminLayout from '@/components/admin/AdminLayout'
|
|
import SystemInfoSection, { SYSTEM_INFO_CONFIGS } from '@/components/admin/SystemInfoSection'
|
|
import Link from 'next/link'
|
|
import { useState, useEffect, useCallback } from 'react'
|
|
|
|
// Types
|
|
interface GameStats {
|
|
activePlayers: number
|
|
totalSessions: number
|
|
questionsAnswered: number
|
|
averageAccuracy: number
|
|
totalPlayTimeHours: number
|
|
}
|
|
|
|
interface LeaderboardEntry {
|
|
rank: number
|
|
displayName: string
|
|
score: number
|
|
accuracy: number
|
|
}
|
|
|
|
type TabType = 'overview' | 'play' | 'stats'
|
|
|
|
// Mock data - in production this would come from the API
|
|
const mockStats: GameStats = {
|
|
activePlayers: 42,
|
|
totalSessions: 1234,
|
|
questionsAnswered: 8567,
|
|
averageAccuracy: 0.78,
|
|
totalPlayTimeHours: 156.5,
|
|
}
|
|
|
|
const mockLeaderboard: LeaderboardEntry[] = [
|
|
{ rank: 1, displayName: 'Max M.', score: 25000, accuracy: 0.92 },
|
|
{ rank: 2, displayName: 'Lisa K.', score: 23500, accuracy: 0.88 },
|
|
{ rank: 3, displayName: 'Tim S.', score: 21000, accuracy: 0.85 },
|
|
{ rank: 4, displayName: 'Anna B.', score: 19500, accuracy: 0.82 },
|
|
{ rank: 5, displayName: 'Paul R.', score: 18000, accuracy: 0.79 },
|
|
]
|
|
|
|
// Stat Card Component
|
|
function StatCard({
|
|
title,
|
|
value,
|
|
suffix = '',
|
|
icon,
|
|
trend,
|
|
}: {
|
|
title: string
|
|
value: string | number
|
|
suffix?: string
|
|
icon: React.ReactNode
|
|
trend?: { value: number; positive: boolean }
|
|
}) {
|
|
return (
|
|
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
|
<div className="flex items-start justify-between">
|
|
<div>
|
|
<p className="text-sm text-slate-500 mb-1">{title}</p>
|
|
<p className="text-2xl font-bold text-slate-900">
|
|
{value}{suffix}
|
|
</p>
|
|
{trend && (
|
|
<p className={`text-sm mt-1 ${trend.positive ? 'text-green-600' : 'text-red-600'}`}>
|
|
{trend.positive ? '+' : ''}{trend.value}% vs. letzte Woche
|
|
</p>
|
|
)}
|
|
</div>
|
|
<div className="p-3 bg-primary-50 rounded-lg text-primary-600">
|
|
{icon}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// Leaderboard Component
|
|
function Leaderboard({ entries }: { entries: LeaderboardEntry[] }) {
|
|
return (
|
|
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
|
<h3 className="font-semibold text-slate-900 mb-4">Top 5 Spieler</h3>
|
|
<div className="space-y-3">
|
|
{entries.map((entry) => (
|
|
<div
|
|
key={entry.rank}
|
|
className="flex items-center justify-between py-2 border-b border-slate-100 last:border-0"
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
<span className={`w-6 h-6 rounded-full flex items-center justify-center text-sm font-bold ${
|
|
entry.rank === 1 ? 'bg-yellow-100 text-yellow-700' :
|
|
entry.rank === 2 ? 'bg-slate-100 text-slate-600' :
|
|
entry.rank === 3 ? 'bg-orange-100 text-orange-700' :
|
|
'bg-slate-50 text-slate-500'
|
|
}`}>
|
|
{entry.rank}
|
|
</span>
|
|
<span className="font-medium text-slate-900">{entry.displayName}</span>
|
|
</div>
|
|
<div className="flex items-center gap-4">
|
|
<span className="text-slate-600">{entry.score.toLocaleString()} Punkte</span>
|
|
<span className="text-sm text-slate-500">{Math.round(entry.accuracy * 100)}%</span>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// Main Page Component
|
|
export default function GameDashboardPage() {
|
|
const [activeTab, setActiveTab] = useState<TabType>('overview')
|
|
const [stats, setStats] = useState<GameStats>(mockStats)
|
|
const [leaderboard, setLeaderboard] = useState<LeaderboardEntry[]>(mockLeaderboard)
|
|
const [loading, setLoading] = useState(false)
|
|
const [gameLoaded, setGameLoaded] = useState(false)
|
|
|
|
// Game URL - in production this would be from environment
|
|
const GAME_URL = process.env.NEXT_PUBLIC_GAME_URL || 'http://localhost:3001'
|
|
|
|
// Fetch stats from API
|
|
const fetchStats = useCallback(async () => {
|
|
setLoading(true)
|
|
try {
|
|
// In production: fetch from /api/game/stats
|
|
// const response = await fetch('/api/admin/game/stats')
|
|
// const data = await response.json()
|
|
// setStats(data)
|
|
|
|
// For now, use mock data
|
|
await new Promise(resolve => setTimeout(resolve, 500))
|
|
setStats(mockStats)
|
|
} catch (error) {
|
|
console.error('Failed to fetch stats:', error)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
fetchStats()
|
|
}, [fetchStats])
|
|
|
|
// Tab buttons
|
|
const tabs: { id: TabType; label: string; icon: React.ReactNode }[] = [
|
|
{
|
|
id: 'overview',
|
|
label: 'Uebersicht',
|
|
icon: (
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
|
</svg>
|
|
),
|
|
},
|
|
{
|
|
id: 'play',
|
|
label: 'Spielen',
|
|
icon: (
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
),
|
|
},
|
|
{
|
|
id: 'stats',
|
|
label: 'Statistiken',
|
|
icon: (
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 8v8m-4-5v5m-4-2v2m-2 4h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
|
</svg>
|
|
),
|
|
},
|
|
]
|
|
|
|
return (
|
|
<AdminLayout title="Breakpilot Drive" description="Lernspiel Dashboard">
|
|
{/* Tab Navigation */}
|
|
<div className="flex gap-2 mb-6">
|
|
{tabs.map((tab) => (
|
|
<button
|
|
key={tab.id}
|
|
onClick={() => setActiveTab(tab.id)}
|
|
className={`flex items-center gap-2 px-4 py-2 rounded-lg font-medium transition-colors ${
|
|
activeTab === tab.id
|
|
? 'bg-primary-600 text-white'
|
|
: 'bg-white text-slate-600 border border-slate-200 hover:bg-slate-50'
|
|
}`}
|
|
>
|
|
{tab.icon}
|
|
{tab.label}
|
|
</button>
|
|
))}
|
|
|
|
{/* Wizard Link */}
|
|
<Link
|
|
href="/admin/game/wizard"
|
|
className="flex items-center gap-2 px-4 py-2 rounded-lg font-medium bg-amber-100 text-amber-700 border border-amber-200 hover:bg-amber-200 transition-colors"
|
|
>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
|
|
</svg>
|
|
Lern-Wizard
|
|
</Link>
|
|
|
|
{/* Multiplayer Wizard Link */}
|
|
<Link
|
|
href="/admin/multiplayer/wizard"
|
|
className="flex items-center gap-2 px-4 py-2 rounded-lg font-medium bg-indigo-100 text-indigo-700 border border-indigo-200 hover:bg-indigo-200 transition-colors"
|
|
>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
|
|
</svg>
|
|
Multiplayer
|
|
</Link>
|
|
|
|
{/* Build Pipeline Wizard Link */}
|
|
<Link
|
|
href="/admin/builds/wizard"
|
|
className="flex items-center gap-2 px-4 py-2 rounded-lg font-medium bg-green-100 text-green-700 border border-green-200 hover:bg-green-200 transition-colors"
|
|
>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
|
</svg>
|
|
Builds
|
|
</Link>
|
|
|
|
{/* Refresh Button */}
|
|
<button
|
|
onClick={fetchStats}
|
|
disabled={loading}
|
|
className="ml-auto px-4 py-2 border border-slate-200 rounded-lg text-slate-600 hover:bg-slate-50 disabled:opacity-50 transition-colors"
|
|
>
|
|
{loading ? 'Laden...' : 'Aktualisieren'}
|
|
</button>
|
|
</div>
|
|
|
|
{/* Overview Tab */}
|
|
{activeTab === 'overview' && (
|
|
<>
|
|
{/* Stats Grid */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4 mb-6">
|
|
<StatCard
|
|
title="Aktive Spieler heute"
|
|
value={stats.activePlayers}
|
|
icon={
|
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
|
|
</svg>
|
|
}
|
|
trend={{ value: 12, positive: true }}
|
|
/>
|
|
<StatCard
|
|
title="Spielsessions"
|
|
value={stats.totalSessions.toLocaleString()}
|
|
icon={
|
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
}
|
|
/>
|
|
<StatCard
|
|
title="Quiz-Fragen beantwortet"
|
|
value={stats.questionsAnswered.toLocaleString()}
|
|
icon={
|
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
}
|
|
/>
|
|
<StatCard
|
|
title="Durchschn. Genauigkeit"
|
|
value={Math.round(stats.averageAccuracy * 100)}
|
|
suffix="%"
|
|
icon={
|
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
}
|
|
trend={{ value: 3, positive: true }}
|
|
/>
|
|
<StatCard
|
|
title="Gesamte Spielzeit"
|
|
value={stats.totalPlayTimeHours.toFixed(1)}
|
|
suffix="h"
|
|
icon={
|
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
}
|
|
/>
|
|
</div>
|
|
|
|
{/* Leaderboard & Info */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
<Leaderboard entries={leaderboard} />
|
|
|
|
{/* Quick Actions */}
|
|
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
|
<h3 className="font-semibold text-slate-900 mb-4">Schnellzugriff</h3>
|
|
<div className="space-y-3">
|
|
<button
|
|
onClick={() => setActiveTab('play')}
|
|
className="w-full flex items-center gap-3 px-4 py-3 bg-primary-50 text-primary-700 rounded-lg hover:bg-primary-100 transition-colors"
|
|
>
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
Spiel starten
|
|
</button>
|
|
<a
|
|
href={GAME_URL}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="w-full flex items-center gap-3 px-4 py-3 bg-slate-50 text-slate-700 rounded-lg hover:bg-slate-100 transition-colors"
|
|
>
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
|
</svg>
|
|
Im neuen Tab oeffnen
|
|
</a>
|
|
<button
|
|
onClick={() => setActiveTab('stats')}
|
|
className="w-full flex items-center gap-3 px-4 py-3 bg-slate-50 text-slate-700 rounded-lg hover:bg-slate-100 transition-colors"
|
|
>
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
|
</svg>
|
|
Detaillierte Statistiken
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* Play Tab - Embedded Game */}
|
|
{activeTab === 'play' && (
|
|
<div className="bg-slate-900 rounded-xl overflow-hidden" style={{ height: '70vh' }}>
|
|
{!gameLoaded && (
|
|
<div className="absolute inset-0 flex items-center justify-center bg-slate-900 z-10">
|
|
<div className="text-center text-white">
|
|
<div className="animate-spin w-12 h-12 border-4 border-primary-500 border-t-transparent rounded-full mx-auto mb-4"></div>
|
|
<p>Spiel wird geladen...</p>
|
|
<p className="text-sm text-slate-400 mt-2">Unity WebGL wird initialisiert</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
<iframe
|
|
src={`${GAME_URL}?embed=true`}
|
|
className="w-full h-full border-0"
|
|
allow="autoplay; fullscreen; gamepad"
|
|
onLoad={() => setGameLoaded(true)}
|
|
title="Breakpilot Drive"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* Stats Tab */}
|
|
{activeTab === 'stats' && (
|
|
<div className="space-y-6">
|
|
{/* Coming Soon Notice */}
|
|
<div className="bg-amber-50 border border-amber-200 rounded-xl p-6">
|
|
<div className="flex gap-3">
|
|
<svg className="w-6 h-6 text-amber-600 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
|
</svg>
|
|
<div>
|
|
<h4 className="font-semibold text-amber-900">Statistiken in Entwicklung</h4>
|
|
<p className="text-sm text-amber-800 mt-1">
|
|
Detaillierte Statistiken werden nach dem ersten WebGL-Build und echten Spielsessions verfuegbar sein.
|
|
Aktuell werden Mock-Daten angezeigt.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Placeholder Charts */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
{/* Sessions over Time */}
|
|
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
|
<h3 className="font-semibold text-slate-900 mb-4">Sessions pro Tag</h3>
|
|
<div className="h-64 flex items-center justify-center bg-slate-50 rounded-lg">
|
|
<p className="text-slate-400">Chart kommt nach WebGL Build</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Accuracy by Subject */}
|
|
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
|
<h3 className="font-semibold text-slate-900 mb-4">Genauigkeit nach Fach</h3>
|
|
<div className="space-y-4">
|
|
{[
|
|
{ subject: 'Mathematik', accuracy: 82, color: 'bg-blue-500' },
|
|
{ subject: 'Deutsch', accuracy: 75, color: 'bg-green-500' },
|
|
{ subject: 'Englisch', accuracy: 78, color: 'bg-purple-500' },
|
|
].map((item) => (
|
|
<div key={item.subject}>
|
|
<div className="flex justify-between text-sm mb-1">
|
|
<span className="text-slate-600">{item.subject}</span>
|
|
<span className="font-medium">{item.accuracy}%</span>
|
|
</div>
|
|
<div className="h-2 bg-slate-100 rounded-full overflow-hidden">
|
|
<div
|
|
className={`h-full ${item.color} rounded-full transition-all`}
|
|
style={{ width: `${item.accuracy}%` }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Learning Level Distribution */}
|
|
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
|
<h3 className="font-semibold text-slate-900 mb-4">Lernniveau-Verteilung</h3>
|
|
<div className="h-64 flex items-center justify-center bg-slate-50 rounded-lg">
|
|
<p className="text-slate-400">Chart kommt nach WebGL Build</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Recent Activity */}
|
|
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
|
<h3 className="font-semibold text-slate-900 mb-4">Letzte Aktivitaet</h3>
|
|
<div className="space-y-3">
|
|
{[
|
|
{ time: 'Vor 5 Min', action: 'Max M. hat Level 4 erreicht', type: 'achievement' },
|
|
{ time: 'Vor 12 Min', action: 'Lisa K. hat 10 Fragen richtig beantwortet', type: 'quiz' },
|
|
{ time: 'Vor 18 Min', action: 'Tim S. hat eine neue Session gestartet', type: 'session' },
|
|
{ time: 'Vor 25 Min', action: 'Anna B. hat den Highscore geknackt', type: 'highscore' },
|
|
].map((item, index) => (
|
|
<div key={index} className="flex items-start gap-3 py-2 border-b border-slate-100 last:border-0">
|
|
<span className="text-xs text-slate-400 w-20 flex-shrink-0">{item.time}</span>
|
|
<span className="text-sm text-slate-600">{item.action}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Info Footer */}
|
|
<div className="mt-6 bg-blue-50 border border-blue-200 rounded-xl p-4">
|
|
<div className="flex gap-3">
|
|
<svg className="w-5 h-5 text-blue-600 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
<div>
|
|
<h4 className="font-semibold text-blue-900">Breakpilot Drive</h4>
|
|
<p className="text-sm text-blue-800 mt-1">
|
|
Endless Runner Lernspiel fuer Klasse 2-6. Das Spiel laeuft auf Port 3001.
|
|
Fuer die volle Funktionalitaet muss der Unity WebGL Build erstellt und der Game-Container gestartet werden.
|
|
</p>
|
|
<p className="text-xs text-blue-600 mt-2 font-mono">
|
|
docker-compose --profile game up -d
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* System Info Section - For Internal/External Audits */}
|
|
<div className="mt-8 border-t border-slate-200 pt-8">
|
|
<SystemInfoSection config={SYSTEM_INFO_CONFIGS.game} />
|
|
</div>
|
|
</AdminLayout>
|
|
)
|
|
}
|