Split 1257-LOC client page into _types.ts plus nine components under _components/ (TabNavigation/StatCard/FilterBar in shared, CourseCard, EnrollmentCard, CertificatesTab, EnrollmentEditModal, CourseEditModal, SettingsTab, and PageSections for header actions and empty states). Behavior preserved exactly; page.tsx is now a thin wiring shell. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
169 lines
5.1 KiB
TypeScript
169 lines
5.1 KiB
TypeScript
'use client'
|
|
|
|
import React from 'react'
|
|
import {
|
|
CourseCategory,
|
|
EnrollmentStatus,
|
|
COURSE_CATEGORY_INFO,
|
|
ENROLLMENT_STATUS_INFO
|
|
} from '@/lib/sdk/academy/types'
|
|
import { Tab, TabId } from '../_types'
|
|
|
|
// =============================================================================
|
|
// TAB NAVIGATION
|
|
// =============================================================================
|
|
|
|
export function TabNavigation({
|
|
tabs,
|
|
activeTab,
|
|
onTabChange
|
|
}: {
|
|
tabs: Tab[]
|
|
activeTab: TabId
|
|
onTabChange: (tab: TabId) => void
|
|
}) {
|
|
return (
|
|
<div className="border-b border-gray-200">
|
|
<nav className="flex gap-1 -mb-px" aria-label="Tabs">
|
|
{tabs.map(tab => (
|
|
<button
|
|
key={tab.id}
|
|
onClick={() => onTabChange(tab.id)}
|
|
className={`
|
|
px-4 py-3 text-sm font-medium border-b-2 transition-colors
|
|
${activeTab === tab.id
|
|
? 'border-purple-600 text-purple-600'
|
|
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
|
}
|
|
`}
|
|
>
|
|
<span className="flex items-center gap-2">
|
|
{tab.label}
|
|
{tab.count !== undefined && tab.count > 0 && (
|
|
<span className={`
|
|
px-2 py-0.5 text-xs rounded-full
|
|
${tab.countColor || 'bg-gray-100 text-gray-600'}
|
|
`}>
|
|
{tab.count}
|
|
</span>
|
|
)}
|
|
</span>
|
|
</button>
|
|
))}
|
|
</nav>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// =============================================================================
|
|
// STAT CARD
|
|
// =============================================================================
|
|
|
|
export function StatCard({
|
|
label,
|
|
value,
|
|
color = 'gray',
|
|
icon,
|
|
trend
|
|
}: {
|
|
label: string
|
|
value: number | string
|
|
color?: 'gray' | 'blue' | 'yellow' | 'red' | 'green' | 'purple'
|
|
icon?: React.ReactNode
|
|
trend?: { value: number; label: string }
|
|
}) {
|
|
const colorClasses = {
|
|
gray: 'border-gray-200 text-gray-900',
|
|
blue: 'border-blue-200 text-blue-600',
|
|
yellow: 'border-yellow-200 text-yellow-600',
|
|
red: 'border-red-200 text-red-600',
|
|
green: 'border-green-200 text-green-600',
|
|
purple: 'border-purple-200 text-purple-600'
|
|
}
|
|
|
|
return (
|
|
<div className={`bg-white rounded-xl border ${colorClasses[color]} p-6`}>
|
|
<div className="flex items-start justify-between">
|
|
<div>
|
|
<div className={`text-sm ${color === 'gray' ? 'text-gray-500' : `text-${color}-600`}`}>
|
|
{label}
|
|
</div>
|
|
<div className={`text-3xl font-bold mt-1 ${colorClasses[color].split(' ')[1]}`}>
|
|
{value}
|
|
</div>
|
|
{trend && (
|
|
<div className={`text-xs mt-1 ${trend.value >= 0 ? 'text-green-600' : 'text-red-600'}`}>
|
|
{trend.value >= 0 ? '+' : ''}{trend.value} {trend.label}
|
|
</div>
|
|
)}
|
|
</div>
|
|
{icon && (
|
|
<div className={`w-10 h-10 rounded-lg flex items-center justify-center bg-${color}-50`}>
|
|
{icon}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// =============================================================================
|
|
// FILTER BAR
|
|
// =============================================================================
|
|
|
|
export function FilterBar({
|
|
selectedCategory,
|
|
selectedStatus,
|
|
onCategoryChange,
|
|
onStatusChange,
|
|
onClear
|
|
}: {
|
|
selectedCategory: CourseCategory | 'all'
|
|
selectedStatus: EnrollmentStatus | 'all'
|
|
onCategoryChange: (category: CourseCategory | 'all') => void
|
|
onStatusChange: (status: EnrollmentStatus | 'all') => void
|
|
onClear: () => void
|
|
}) {
|
|
const hasFilters = selectedCategory !== 'all' || selectedStatus !== 'all'
|
|
|
|
return (
|
|
<div className="flex items-center gap-4 flex-wrap">
|
|
<span className="text-sm text-gray-500">Filter:</span>
|
|
|
|
{/* Category Filter */}
|
|
<select
|
|
value={selectedCategory}
|
|
onChange={(e) => onCategoryChange(e.target.value as CourseCategory | 'all')}
|
|
className="px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
|
|
>
|
|
<option value="all">Alle Kategorien</option>
|
|
{Object.entries(COURSE_CATEGORY_INFO).map(([cat, info]) => (
|
|
<option key={cat} value={cat}>{info.label}</option>
|
|
))}
|
|
</select>
|
|
|
|
{/* Enrollment Status Filter */}
|
|
<select
|
|
value={selectedStatus}
|
|
onChange={(e) => onStatusChange(e.target.value as EnrollmentStatus | 'all')}
|
|
className="px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
|
|
>
|
|
<option value="all">Alle Status</option>
|
|
{Object.entries(ENROLLMENT_STATUS_INFO).map(([status, info]) => (
|
|
<option key={status} value={status}>{info.label}</option>
|
|
))}
|
|
</select>
|
|
|
|
{/* Clear Filters */}
|
|
{hasFilters && (
|
|
<button
|
|
onClick={onClear}
|
|
className="px-3 py-1.5 text-sm text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-lg transition-colors"
|
|
>
|
|
Filter zuruecksetzen
|
|
</button>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|