fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
480
website/app/admin/game/page.tsx
Normal file
480
website/app/admin/game/page.tsx
Normal file
@@ -0,0 +1,480 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
739
website/app/admin/game/wizard/page.tsx
Normal file
739
website/app/admin/game/wizard/page.tsx
Normal file
@@ -0,0 +1,739 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* Breakpilot Drive - Test & Lern-Wizard
|
||||
*
|
||||
* Interaktiver Wizard zum Kennenlernen aller Breakpilot Drive Features:
|
||||
* - Game Dashboard Funktionen
|
||||
* - API Integration
|
||||
* - WebGL Embedding
|
||||
* - Quiz-System
|
||||
* - Lernniveau-Anpassung
|
||||
* - Statistiken & Leaderboards
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import Link from 'next/link'
|
||||
|
||||
// ==============================================
|
||||
// Types
|
||||
// ==============================================
|
||||
|
||||
type StepStatus = 'pending' | 'active' | 'completed' | 'failed'
|
||||
|
||||
interface WizardStep {
|
||||
id: string
|
||||
name: string
|
||||
icon: string
|
||||
status: StepStatus
|
||||
testable?: boolean
|
||||
}
|
||||
|
||||
interface TestResult {
|
||||
name: string
|
||||
status: 'passed' | 'failed' | 'pending'
|
||||
message: string
|
||||
details?: string
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Constants
|
||||
// ==============================================
|
||||
|
||||
const BACKEND_URL = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8000'
|
||||
const GAME_URL = process.env.NEXT_PUBLIC_GAME_URL || 'http://localhost:3001'
|
||||
|
||||
const STEPS: WizardStep[] = [
|
||||
{ id: 'welcome', name: 'Willkommen', icon: '🎮', status: 'pending' },
|
||||
{ id: 'overview', name: 'Dashboard Uebersicht', icon: '📊', status: 'pending' },
|
||||
{ id: 'stats', name: 'Statistiken', icon: '📈', status: 'pending' },
|
||||
{ id: 'leaderboard', name: 'Leaderboard', icon: '🏆', status: 'pending' },
|
||||
{ id: 'webgl', name: 'WebGL Embedding', icon: '🎯', status: 'pending', testable: true },
|
||||
{ id: 'api', name: 'API Integration', icon: '🔌', status: 'pending', testable: true },
|
||||
{ id: 'quiz', name: 'Quiz-System', icon: '❓', status: 'pending' },
|
||||
{ id: 'learning', name: 'Lernniveau', icon: '📚', status: 'pending' },
|
||||
{ id: 'summary', name: 'Zusammenfassung', icon: '✅', status: 'pending' },
|
||||
]
|
||||
|
||||
const EDUCATION_CONTENT: Record<string, { title: string; content: string[]; tips?: string[] }> = {
|
||||
'welcome': {
|
||||
title: 'Willkommen bei Breakpilot Drive!',
|
||||
content: [
|
||||
'Breakpilot Drive ist ein **Endless Runner Lernspiel** fuer Schueler der Klassen 2-6.',
|
||||
'Das Spiel kombiniert Spielspass mit Lernen:',
|
||||
'• Fahre so weit wie moeglich',
|
||||
'• Beantworte Quiz-Fragen um Punkte zu sammeln',
|
||||
'• Das System passt sich automatisch an dein Lernniveau an',
|
||||
'In diesem Wizard lernst du alle Admin-Features kennen und testest die Integration.',
|
||||
],
|
||||
tips: [
|
||||
'Das Spiel laeuft auf Port 3001 als Unity WebGL Build',
|
||||
'Die API-Endpoints sind unter /api/game/* verfuegbar',
|
||||
],
|
||||
},
|
||||
'overview': {
|
||||
title: 'Das Game Dashboard',
|
||||
content: [
|
||||
'Das Dashboard unter **/admin/game** bietet drei Hauptbereiche:',
|
||||
'**1. Uebersicht-Tab:**',
|
||||
'• Statistik-Karten mit KPIs (Spieler, Sessions, Genauigkeit)',
|
||||
'• Top 5 Leaderboard',
|
||||
'• Schnellzugriff-Buttons',
|
||||
'**2. Spielen-Tab:**',
|
||||
'• Embedded WebGL Game im iframe',
|
||||
'• Direkt im Admin-Panel spielbar',
|
||||
'**3. Statistiken-Tab:**',
|
||||
'• Genauigkeit nach Fach',
|
||||
'• Aktivitaets-Feed',
|
||||
'• Lernniveau-Verteilung',
|
||||
],
|
||||
tips: [
|
||||
'Die Tabs sind oben im Dashboard als Buttons sichtbar',
|
||||
'Klicke auf "Aktualisieren" um die neuesten Daten zu laden',
|
||||
],
|
||||
},
|
||||
'stats': {
|
||||
title: 'Statistiken verstehen',
|
||||
content: [
|
||||
'Die Statistik-Karten zeigen wichtige KPIs:',
|
||||
'**Aktive Spieler heute:** Anzahl der Spieler mit mindestens einer Session heute',
|
||||
'**Spielsessions:** Gesamtzahl aller abgeschlossenen Spielsessions',
|
||||
'**Quiz-Fragen beantwortet:** Kumulative Anzahl beantworteter Fragen',
|
||||
'**Durchschnittliche Genauigkeit:** Prozent der richtig beantworteten Fragen',
|
||||
'**Gesamte Spielzeit:** Summe aller Spielzeiten in Stunden',
|
||||
'Trends zeigen die Veraenderung zur Vorwoche.',
|
||||
],
|
||||
tips: [
|
||||
'Gruene Trends = Verbesserung',
|
||||
'Rote Trends = Bereich mit Aufmerksamkeitsbedarf',
|
||||
],
|
||||
},
|
||||
'leaderboard': {
|
||||
title: 'Leaderboard & Gamification',
|
||||
content: [
|
||||
'Das Leaderboard motiviert Schueler durch:',
|
||||
'**Ranking:** Top 5 Spieler nach Gesamtpunktzahl',
|
||||
'**Goldene Medaille:** Platz 1 ist besonders hervorgehoben',
|
||||
'**Genauigkeit:** Zeigt wie viele Fragen richtig beantwortet wurden',
|
||||
'Spaeter kommen hinzu:',
|
||||
'• Klassen-Ranglisten',
|
||||
'• Wochen/Monats-Ranglisten',
|
||||
'• Achievements & Badges',
|
||||
],
|
||||
tips: [
|
||||
'Leaderboards koennen pro Klasse oder schulweit sein',
|
||||
'Datenschutz: Nur Vornamen + erster Buchstabe des Nachnamens werden gezeigt',
|
||||
],
|
||||
},
|
||||
'webgl': {
|
||||
title: 'WebGL Embedding',
|
||||
content: [
|
||||
'Das Spiel wird als **Unity WebGL Build** eingebettet:',
|
||||
'**Technologie:**',
|
||||
'• Unity 6 (Version 6000.0)',
|
||||
'• Universal Render Pipeline (URP)',
|
||||
'• WebAssembly (WASM) fuer Performance',
|
||||
'**Embedding:**',
|
||||
'• Das Spiel laeuft in einem iframe auf Port 3001',
|
||||
'• Parameter wie ?embed=true optimieren fuer Einbettung',
|
||||
'• Fullscreen und Gamepad werden unterstuetzt',
|
||||
'**Wichtig:** Der Game-Container muss laufen:',
|
||||
'`docker-compose --profile game up -d`',
|
||||
],
|
||||
tips: [
|
||||
'Bei Ladefehlern: Container-Status pruefen',
|
||||
'CORS muss korrekt konfiguriert sein',
|
||||
],
|
||||
},
|
||||
'api': {
|
||||
title: 'API Integration',
|
||||
content: [
|
||||
'Die Game API stellt folgende Endpoints bereit:',
|
||||
'**GET /api/game/learning-level**',
|
||||
'→ Aktuelles Lernniveau des Spielers',
|
||||
'**GET /api/game/questions?difficulty=3&count=5**',
|
||||
'→ Quiz-Fragen basierend auf Schwierigkeit',
|
||||
'**POST /api/game/session**',
|
||||
'→ Spielsession speichern (Score, Zeit, Antworten)',
|
||||
'**GET /api/game/achievements**',
|
||||
'→ Freigeschaltete Achievements',
|
||||
'**GET /api/game/leaderboard?limit=10**',
|
||||
'→ Top-Spieler Rangliste',
|
||||
],
|
||||
tips: [
|
||||
'Alle Endpoints erfordern JWT-Token in Produktion',
|
||||
'Im Dev-Modus ist Auth optional',
|
||||
],
|
||||
},
|
||||
'quiz': {
|
||||
title: 'Das Quiz-System',
|
||||
content: [
|
||||
'Quiz-Fragen erscheinen waehrend des Spiels:',
|
||||
'**Quick-Modus (5 Sekunden):**',
|
||||
'• 2-3 Antwortmoeglichkeiten',
|
||||
'• Wird durch visuelle Trigger ausgeloest (Bruecke, Baum)',
|
||||
'• Schnelle Punkte bei richtiger Antwort',
|
||||
'**Pause-Modus (unbegrenzt):**',
|
||||
'• 4 Antwortmoeglichkeiten',
|
||||
'• Spieler kann nachdenken',
|
||||
'• Mehr Punkte moeglich',
|
||||
'**Faecher:** Mathematik, Deutsch, Englisch',
|
||||
'**LLM-Generierung:** Fragen werden dynamisch erstellt',
|
||||
],
|
||||
tips: [
|
||||
'Fragen werden im Valkey-Cache gespeichert',
|
||||
'Schwierigkeit passt sich automatisch an',
|
||||
],
|
||||
},
|
||||
'learning': {
|
||||
title: 'Adaptives Lernniveau',
|
||||
content: [
|
||||
'Das System passt sich automatisch an:',
|
||||
'**5 Lernstufen:**',
|
||||
'• Level 1: Klasse 2-3 (Beginner)',
|
||||
'• Level 2: Klasse 3-4 (Elementary)',
|
||||
'• Level 3: Klasse 4-5 (Intermediate)',
|
||||
'• Level 4: Klasse 5-6 (Advanced)',
|
||||
'• Level 5: Klasse 6+ (Expert)',
|
||||
'**Anpassung:**',
|
||||
'• ≥80% richtig ueber 10 Fragen → Level Up',
|
||||
'• <40% richtig ueber 5 Fragen → Level Down',
|
||||
'• Schwache Faecher werden identifiziert',
|
||||
'**State Engine:** Nutzt die bestehende Breakpilot State Machine',
|
||||
],
|
||||
tips: [
|
||||
'Eltern sehen das Niveau ihrer Kinder im Dashboard',
|
||||
'Lehrer sehen Klassen-Durchschnitte',
|
||||
],
|
||||
},
|
||||
'summary': {
|
||||
title: 'Zusammenfassung',
|
||||
content: [
|
||||
'Du hast alle Breakpilot Drive Features kennengelernt:',
|
||||
'✅ Dashboard mit Uebersicht, Spielen, Statistiken',
|
||||
'✅ Statistik-Karten und KPIs',
|
||||
'✅ Leaderboard & Gamification',
|
||||
'✅ WebGL Embedding',
|
||||
'✅ API Integration',
|
||||
'✅ Quiz-System mit Quick/Pause-Modus',
|
||||
'✅ Adaptives Lernniveau',
|
||||
'**Naechste Schritte:**',
|
||||
'• Teste das Dashboard unter /admin/game',
|
||||
'• Starte den Game-Container',
|
||||
'• Spiele eine Runde im "Spielen"-Tab',
|
||||
],
|
||||
tips: [
|
||||
'Bei Fragen: Siehe docs/breakpilot-drive/README.md',
|
||||
'API-Doku: docs/breakpilot-drive/architecture.md',
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Components
|
||||
// ==============================================
|
||||
|
||||
function WizardStepper({
|
||||
steps,
|
||||
currentStep,
|
||||
onStepClick
|
||||
}: {
|
||||
steps: WizardStep[]
|
||||
currentStep: number
|
||||
onStepClick: (index: number) => void
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center justify-between mb-8 overflow-x-auto pb-4">
|
||||
{steps.map((step, index) => (
|
||||
<div key={step.id} className="flex items-center">
|
||||
<button
|
||||
onClick={() => onStepClick(index)}
|
||||
className={`flex flex-col items-center min-w-[80px] p-2 rounded-lg transition-colors ${
|
||||
index === currentStep
|
||||
? 'bg-primary-100 text-primary-700'
|
||||
: step.status === 'completed'
|
||||
? 'bg-green-100 text-green-700 cursor-pointer hover:bg-green-200'
|
||||
: step.status === 'failed'
|
||||
? 'bg-red-100 text-red-700 cursor-pointer hover:bg-red-200'
|
||||
: 'text-slate-400 hover:bg-slate-100'
|
||||
}`}
|
||||
>
|
||||
<span className="text-2xl mb-1">{step.icon}</span>
|
||||
<span className="text-xs font-medium text-center">{step.name}</span>
|
||||
{step.status === 'completed' && <span className="text-xs text-green-600">✓</span>}
|
||||
{step.status === 'failed' && <span className="text-xs text-red-600">✗</span>}
|
||||
</button>
|
||||
{index < steps.length - 1 && (
|
||||
<div className={`h-0.5 w-8 mx-1 ${
|
||||
index < currentStep ? 'bg-green-400' : 'bg-slate-200'
|
||||
}`} />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function EducationCard({ stepId }: { stepId: string }) {
|
||||
const content = EDUCATION_CONTENT[stepId]
|
||||
if (!content) return null
|
||||
|
||||
return (
|
||||
<div className="bg-primary-50 border border-primary-200 rounded-lg p-6 mb-6">
|
||||
<h3 className="text-lg font-semibold text-primary-800 mb-4 flex items-center">
|
||||
<span className="mr-2">📖</span>
|
||||
{content.title}
|
||||
</h3>
|
||||
<div className="space-y-2 text-primary-900">
|
||||
{content.content.map((line, index) => (
|
||||
<p
|
||||
key={index}
|
||||
className={`${line.startsWith('•') ? 'ml-4' : ''} ${line.startsWith('**') ? 'font-semibold mt-3' : ''}`}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: line
|
||||
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(/`(.*?)`/g, '<code class="bg-primary-100 px-1 rounded">$1</code>')
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{content.tips && content.tips.length > 0 && (
|
||||
<div className="mt-4 pt-4 border-t border-primary-200">
|
||||
<p className="text-sm font-semibold text-primary-700 mb-2">💡 Tipps:</p>
|
||||
{content.tips.map((tip, index) => (
|
||||
<p key={index} className="text-sm text-primary-700 ml-4">• {tip}</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TestResultCard({ results }: { results: TestResult[] }) {
|
||||
return (
|
||||
<div className="bg-white border border-slate-200 rounded-lg p-4 mb-4">
|
||||
<h4 className="font-semibold text-slate-800 mb-3">Test-Ergebnisse</h4>
|
||||
<div className="space-y-2">
|
||||
{results.map((result, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`flex items-center justify-between p-3 rounded-lg ${
|
||||
result.status === 'passed' ? 'bg-green-50' :
|
||||
result.status === 'failed' ? 'bg-red-50' : 'bg-slate-50'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`text-lg ${
|
||||
result.status === 'passed' ? 'text-green-600' :
|
||||
result.status === 'failed' ? 'text-red-600' : 'text-slate-400'
|
||||
}`}>
|
||||
{result.status === 'passed' ? '✓' : result.status === 'failed' ? '✗' : '○'}
|
||||
</span>
|
||||
<div>
|
||||
<p className="font-medium text-slate-800">{result.name}</p>
|
||||
<p className="text-sm text-slate-600">{result.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
{result.details && (
|
||||
<code className="text-xs bg-slate-100 px-2 py-1 rounded">{result.details}</code>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function InteractiveDemo({ stepId }: { stepId: string }) {
|
||||
// Step-specific interactive demos
|
||||
if (stepId === 'stats') {
|
||||
return (
|
||||
<div className="bg-white border border-slate-200 rounded-lg p-4 mb-4">
|
||||
<h4 className="font-semibold text-slate-800 mb-3">Live-Vorschau: Statistik-Karten</h4>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{[
|
||||
{ label: 'Aktive Spieler', value: '42', icon: '👥', color: 'blue' },
|
||||
{ label: 'Genauigkeit', value: '78%', icon: '✓', color: 'green' },
|
||||
{ label: 'Spielzeit', value: '156h', icon: '⏱️', color: 'purple' },
|
||||
].map((stat) => (
|
||||
<div key={stat.label} className={`bg-${stat.color}-50 rounded-lg p-4 text-center`}>
|
||||
<span className="text-2xl">{stat.icon}</span>
|
||||
<p className="text-2xl font-bold mt-1">{stat.value}</p>
|
||||
<p className="text-sm text-slate-600">{stat.label}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (stepId === 'leaderboard') {
|
||||
return (
|
||||
<div className="bg-white border border-slate-200 rounded-lg p-4 mb-4">
|
||||
<h4 className="font-semibold text-slate-800 mb-3">Live-Vorschau: Leaderboard</h4>
|
||||
<div className="space-y-2">
|
||||
{[
|
||||
{ rank: 1, name: 'Max M.', score: 25000, color: 'yellow' },
|
||||
{ rank: 2, name: 'Lisa K.', score: 23500, color: 'slate' },
|
||||
{ rank: 3, name: 'Tim S.', score: 21000, color: 'orange' },
|
||||
].map((entry) => (
|
||||
<div key={entry.rank} className="flex items-center justify-between py-2 border-b border-slate-100">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className={`w-8 h-8 rounded-full bg-${entry.color}-100 flex items-center justify-center font-bold`}>
|
||||
{entry.rank}
|
||||
</span>
|
||||
<span className="font-medium">{entry.name}</span>
|
||||
</div>
|
||||
<span className="text-slate-600">{entry.score.toLocaleString()} Punkte</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (stepId === 'learning') {
|
||||
return (
|
||||
<div className="bg-white border border-slate-200 rounded-lg p-4 mb-4">
|
||||
<h4 className="font-semibold text-slate-800 mb-3">Live-Vorschau: Lernniveau</h4>
|
||||
<div className="space-y-3">
|
||||
{[
|
||||
{ subject: 'Mathematik', level: 3.2, color: 'blue' },
|
||||
{ subject: 'Deutsch', level: 2.8, color: 'green' },
|
||||
{ subject: 'Englisch', level: 3.5, color: 'purple' },
|
||||
].map((item) => (
|
||||
<div key={item.subject}>
|
||||
<div className="flex justify-between text-sm mb-1">
|
||||
<span>{item.subject}</span>
|
||||
<span className="font-medium">Level {item.level.toFixed(1)}</span>
|
||||
</div>
|
||||
<div className="h-3 bg-slate-100 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full bg-${item.color}-500 rounded-full`}
|
||||
style={{ width: `${(item.level / 5) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Main Component
|
||||
// ==============================================
|
||||
|
||||
export default function GameWizardPage() {
|
||||
const [currentStep, setCurrentStep] = useState(0)
|
||||
const [steps, setSteps] = useState<WizardStep[]>(STEPS)
|
||||
const [testResults, setTestResults] = useState<TestResult[]>([])
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
const currentStepData = steps[currentStep]
|
||||
const isWelcome = currentStepData?.id === 'welcome'
|
||||
const isSummary = currentStepData?.id === 'summary'
|
||||
|
||||
// Test functions
|
||||
const runWebGLTest = useCallback(async () => {
|
||||
setIsLoading(true)
|
||||
const results: TestResult[] = []
|
||||
|
||||
// Test 1: Game URL erreichbar
|
||||
try {
|
||||
const response = await fetch(GAME_URL, { mode: 'no-cors' })
|
||||
results.push({
|
||||
name: 'Game Server Erreichbarkeit',
|
||||
status: 'passed',
|
||||
message: 'Der Game-Server antwortet',
|
||||
details: GAME_URL,
|
||||
})
|
||||
} catch {
|
||||
results.push({
|
||||
name: 'Game Server Erreichbarkeit',
|
||||
status: 'failed',
|
||||
message: 'Game-Server nicht erreichbar. Container gestartet?',
|
||||
details: 'docker-compose --profile game up -d',
|
||||
})
|
||||
}
|
||||
|
||||
// Test 2: Iframe simulieren
|
||||
results.push({
|
||||
name: 'Iframe Embedding',
|
||||
status: 'passed',
|
||||
message: 'Iframe-Einbettung ist konfiguriert',
|
||||
details: '?embed=true',
|
||||
})
|
||||
|
||||
setTestResults(results)
|
||||
setIsLoading(false)
|
||||
|
||||
// Update step status
|
||||
const allPassed = results.every(r => r.status === 'passed')
|
||||
setSteps(prev => prev.map(s =>
|
||||
s.id === 'webgl' ? { ...s, status: allPassed ? 'completed' : 'failed' } : s
|
||||
))
|
||||
}, [])
|
||||
|
||||
const runAPITest = useCallback(async () => {
|
||||
setIsLoading(true)
|
||||
const results: TestResult[] = []
|
||||
|
||||
// Test 1: Learning Level API
|
||||
try {
|
||||
const response = await fetch(`${BACKEND_URL}/api/game/learning-level?user_id=test-user`)
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
results.push({
|
||||
name: 'Learning Level Endpoint',
|
||||
status: 'passed',
|
||||
message: 'API antwortet korrekt',
|
||||
details: `Level: ${data.overall_level || 'Mock'}`,
|
||||
})
|
||||
} else {
|
||||
results.push({
|
||||
name: 'Learning Level Endpoint',
|
||||
status: 'failed',
|
||||
message: `HTTP ${response.status}`,
|
||||
})
|
||||
}
|
||||
} catch {
|
||||
results.push({
|
||||
name: 'Learning Level Endpoint',
|
||||
status: 'failed',
|
||||
message: 'Backend nicht erreichbar',
|
||||
details: 'docker-compose up -d backend',
|
||||
})
|
||||
}
|
||||
|
||||
// Test 2: Questions API
|
||||
try {
|
||||
const response = await fetch(`${BACKEND_URL}/api/game/questions?difficulty=3&count=2`)
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
results.push({
|
||||
name: 'Questions Endpoint',
|
||||
status: 'passed',
|
||||
message: 'Quiz-Fragen API funktioniert',
|
||||
details: `${data.questions?.length || 0} Fragen`,
|
||||
})
|
||||
} else {
|
||||
results.push({
|
||||
name: 'Questions Endpoint',
|
||||
status: 'failed',
|
||||
message: `HTTP ${response.status}`,
|
||||
})
|
||||
}
|
||||
} catch {
|
||||
results.push({
|
||||
name: 'Questions Endpoint',
|
||||
status: 'failed',
|
||||
message: 'Endpoint nicht erreichbar',
|
||||
})
|
||||
}
|
||||
|
||||
// Test 3: Leaderboard API
|
||||
try {
|
||||
const response = await fetch(`${BACKEND_URL}/api/game/leaderboard?limit=5`)
|
||||
if (response.ok) {
|
||||
results.push({
|
||||
name: 'Leaderboard Endpoint',
|
||||
status: 'passed',
|
||||
message: 'Leaderboard API funktioniert',
|
||||
})
|
||||
} else {
|
||||
results.push({
|
||||
name: 'Leaderboard Endpoint',
|
||||
status: 'failed',
|
||||
message: `HTTP ${response.status}`,
|
||||
})
|
||||
}
|
||||
} catch {
|
||||
results.push({
|
||||
name: 'Leaderboard Endpoint',
|
||||
status: 'failed',
|
||||
message: 'Endpoint nicht erreichbar',
|
||||
})
|
||||
}
|
||||
|
||||
setTestResults(results)
|
||||
setIsLoading(false)
|
||||
|
||||
// Update step status
|
||||
const passedCount = results.filter(r => r.status === 'passed').length
|
||||
setSteps(prev => prev.map(s =>
|
||||
s.id === 'api' ? { ...s, status: passedCount >= 2 ? 'completed' : 'failed' } : s
|
||||
))
|
||||
}, [])
|
||||
|
||||
const goToNext = () => {
|
||||
if (currentStep < steps.length - 1) {
|
||||
setSteps(prev => prev.map((step, idx) =>
|
||||
idx === currentStep && step.status === 'pending'
|
||||
? { ...step, status: 'completed' }
|
||||
: step
|
||||
))
|
||||
setCurrentStep(prev => prev + 1)
|
||||
setTestResults([])
|
||||
}
|
||||
}
|
||||
|
||||
const goToPrev = () => {
|
||||
if (currentStep > 0) {
|
||||
setCurrentStep(prev => prev - 1)
|
||||
setTestResults([])
|
||||
}
|
||||
}
|
||||
|
||||
const handleStepClick = (index: number) => {
|
||||
setCurrentStep(index)
|
||||
setTestResults([])
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-100 py-8">
|
||||
<div className="max-w-4xl mx-auto px-4">
|
||||
{/* Header */}
|
||||
<div className="bg-white rounded-lg shadow-lg p-6 mb-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-800">🎮 Breakpilot Drive - Lern-Wizard</h1>
|
||||
<p className="text-slate-600 mt-1">
|
||||
Interaktive Tour durch alle Game-Features
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/admin/game"
|
||||
className="text-primary-600 hover:text-primary-800 text-sm"
|
||||
>
|
||||
← Zurueck zum Dashboard
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stepper */}
|
||||
<div className="bg-white rounded-lg shadow-lg p-6 mb-6">
|
||||
<WizardStepper
|
||||
steps={steps}
|
||||
currentStep={currentStep}
|
||||
onStepClick={handleStepClick}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="bg-white rounded-lg shadow-lg p-6">
|
||||
{/* Step Header */}
|
||||
<div className="flex items-center mb-6">
|
||||
<span className="text-4xl mr-4">{currentStepData?.icon}</span>
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-slate-800">
|
||||
Schritt {currentStep + 1}: {currentStepData?.name}
|
||||
</h2>
|
||||
<p className="text-slate-500 text-sm">
|
||||
{currentStep + 1} von {steps.length}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Education Card */}
|
||||
<EducationCard stepId={currentStepData?.id || ''} />
|
||||
|
||||
{/* Interactive Demo */}
|
||||
<InteractiveDemo stepId={currentStepData?.id || ''} />
|
||||
|
||||
{/* Test Section for testable steps */}
|
||||
{currentStepData?.testable && (
|
||||
<div className="mb-6">
|
||||
{testResults.length === 0 ? (
|
||||
<div className="text-center py-6">
|
||||
<button
|
||||
onClick={currentStepData.id === 'webgl' ? runWebGLTest : runAPITest}
|
||||
disabled={isLoading}
|
||||
className={`px-6 py-3 rounded-lg font-medium transition-colors ${
|
||||
isLoading
|
||||
? 'bg-slate-400 cursor-not-allowed'
|
||||
: 'bg-green-600 text-white hover:bg-green-700'
|
||||
}`}
|
||||
>
|
||||
{isLoading ? '⏳ Tests laufen...' : '🧪 Integration testen'}
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<TestResultCard results={testResults} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Welcome Start Button */}
|
||||
{isWelcome && (
|
||||
<div className="text-center py-8">
|
||||
<button
|
||||
onClick={goToNext}
|
||||
className="bg-primary-600 text-white px-8 py-3 rounded-lg font-medium hover:bg-primary-700 transition-colors"
|
||||
>
|
||||
🚀 Tour starten
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Summary Actions */}
|
||||
{isSummary && (
|
||||
<div className="text-center py-6 space-y-4">
|
||||
<div className="flex justify-center gap-4">
|
||||
<Link
|
||||
href="/admin/game"
|
||||
className="px-6 py-3 bg-primary-600 text-white rounded-lg font-medium hover:bg-primary-700 transition-colors"
|
||||
>
|
||||
📊 Zum Dashboard
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => {
|
||||
setCurrentStep(0)
|
||||
setSteps(STEPS.map(s => ({ ...s, status: 'pending' })))
|
||||
}}
|
||||
className="px-6 py-3 bg-slate-200 text-slate-700 rounded-lg font-medium hover:bg-slate-300 transition-colors"
|
||||
>
|
||||
🔄 Wizard neu starten
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Navigation */}
|
||||
{!isWelcome && (
|
||||
<div className="flex justify-between mt-8 pt-6 border-t">
|
||||
<button
|
||||
onClick={goToPrev}
|
||||
disabled={currentStep === 0}
|
||||
className={`px-6 py-2 rounded-lg transition-colors ${
|
||||
currentStep === 0
|
||||
? 'bg-slate-200 text-slate-400 cursor-not-allowed'
|
||||
: 'bg-slate-200 text-slate-700 hover:bg-slate-300'
|
||||
}`}
|
||||
>
|
||||
← Zurueck
|
||||
</button>
|
||||
|
||||
{!isSummary && (
|
||||
<button
|
||||
onClick={goToNext}
|
||||
className="bg-primary-600 text-white px-6 py-2 rounded-lg hover:bg-primary-700 transition-colors"
|
||||
>
|
||||
Weiter →
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer Info */}
|
||||
<div className="text-center text-slate-500 text-sm mt-6">
|
||||
Breakpilot Drive - Endless Runner Lernspiel fuer Klasse 2-6
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user