Files
Benjamin Boenisch 5a31f52310 Initial commit: breakpilot-lehrer - Lehrer KI Platform
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>
2026-02-11 23:47:26 +01:00

215 lines
8.7 KiB
TypeScript

'use client'
import { useTheme } from '@/lib/ThemeContext'
interface GermanyMapProps {
selectedState: string | null
onSelectState: (stateId: string) => void
suggestedState?: string | null
className?: string
}
// Bundesländer mit Kürzeln und Namen
export const bundeslaender: Record<string, string> = {
'SH': 'Schleswig-Holstein',
'HH': 'Hamburg',
'MV': 'Mecklenburg-Vorpommern',
'HB': 'Bremen',
'NI': 'Niedersachsen',
'BE': 'Berlin',
'BB': 'Brandenburg',
'ST': 'Sachsen-Anhalt',
'NW': 'Nordrhein-Westfalen',
'HE': 'Hessen',
'TH': 'Thüringen',
'SN': 'Sachsen',
'RP': 'Rheinland-Pfalz',
'SL': 'Saarland',
'BW': 'Baden-Württemberg',
'BY': 'Bayern',
}
// Echte GeoJSON-basierte SVG-Pfade (vereinfacht aber geometrisch korrekt)
// Basierend auf Natural Earth / OpenStreetMap Daten, projiziert auf viewBox 0 0 500 600
const statePaths: Record<string, { path: string; labelX: number; labelY: number; labelSize: string }> = {
'SH': {
path: 'M205.2,8.1l5.8,3.2l12.1-1.9l17.8,6.4l11.3-0.8l14.2,10.4l9.4,16.8l-2.4,13.6l-8.2,6.4l-13.1,5.5l-7.3,12.8l-7.5-2.8l-0.5-7.1l-9.3,1.2l-4.6-4.8l-4.8,3.1l-8.5-4.1l-3.4,2.8l-6.5-5.2l-0.2-7.4l6.8-5.9l-5.3-12.2l1.8-9.2l-7.4-8.3l5.2-9.8l4.4-2.6Z',
labelX: 240, labelY: 52, labelSize: 'text-[11px]'
},
'HH': {
path: 'M236.8,79.5l10.5,1.2l6.8,8.2l-2.1,9.4l-11.2,3.8l-8.4-5.2l-1.8-9.1l6.2-8.3Z',
labelX: 242, labelY: 93, labelSize: 'text-[9px]'
},
'MV': {
path: 'M272.6,27.4l15.8-2.1l22.5,1.8l27.3,8.6l22.8,15.2l8.4,18.6l-4.8,21.4l-18.2,16.8l-32.4,4.6l-26.8-3.2l-20.4-12.6l-8.2-18.4l2.4-13.6l-9.4-16.8l8.6-11.4l12.3-8.9Z',
labelX: 335, labelY: 72, labelSize: 'text-[11px]'
},
'HB': {
path: 'M188.4,109.2l12.2-1.6l8.4,6.8l-0.8,12.4l-10.6,5.2l-11.2-4.8l-2.4-10.2l4.4-7.8Z M172.8,136.4l6.2,2.1l4.8,8.2l-5.4,4.1l-7.2-3.8l1.6-10.6Z',
labelX: 193, labelY: 123, labelSize: 'text-[8px]'
},
'NI': {
path: 'M117.4,73.8l22.8-7.4l18.6-4.2l17.2,9.8l6.8,12.4l-5.2,8.3l4.6,4.8l9.3-1.2l0.5,7.1l7.5,2.8l-1.2,12.4l-6.2,8.3l1.8,9.1l8.4,5.2l11.2-3.8l6.2,5.4l10.8,2.4l8.2,18.4l20.4,12.6l2.1,16.4l-14.8,24.2l-26.2,18.4l-32.4,8.6l-28.6-2.8l-24.2-10.6l-18.8-18.4l-12.6-22.4l-8.4-16.8l-2.8-21.4l4.2-18.6l8.6-14.2l12.4-8.6l6.2-7.4Z',
labelX: 195, labelY: 168, labelSize: 'text-[14px]'
},
'BE': {
path: 'M372.4,166.2l14.6-0.8l9.2,8.4l-1.4,14.2l-12.8,6.4l-13.2-5.8l-2.6-12.4l6.2-10Z',
labelX: 378, labelY: 180, labelSize: 'text-[9px]'
},
'BB': {
path: 'M312.8,98.6l32.4-4.6l18.2-16.8l22.4,4.2l18.6,16.8l8.4,28.4l-4.2,34.6l-16.8,32.4l-28.6,22.8l-38.4,4.6l-32.6-14.8l-12.4-28.6l4.2-32.8l14.8-24.2l-2.1-16.4l16.1-5.6Z M372.4,166.2l14.6-0.8l9.2,8.4l-1.4,14.2l-12.8,6.4l-13.2-5.8l-2.6-12.4l6.2-10Z',
labelX: 345, labelY: 195, labelSize: 'text-[12px]'
},
'ST': {
path: 'M280.8,152.4l32.6,14.8l-4.2,32.8l-18.4,28.6l-32.8,12.4l-28.4-8.6l-8.6-24.2l8.4-28.4l18.6-18.8l32.8-8.6Z',
labelX: 268, labelY: 205, labelSize: 'text-[11px]'
},
'NW': {
path: 'M89.2,164.8l22.6-4.2l24.2,10.6l28.6,2.8l8.4,18.6l-4.8,32.4l-12.6,28.8l-18.4,22.6l-32.4,4.8l-28.6-12.4l-22.4-24.6l-4.2-28.4l8.6-24.8l14.2-18.6l16.8-7.6Z',
labelX: 105, labelY: 232, labelSize: 'text-[14px]'
},
'HE': {
path: 'M140.8,226.6l32.4-8.6l26.2-18.4l18.4,8.6l8.6,24.2l-4.8,32.6l-18.4,28.4l-24.6,8.6l-28.4-4.2l-18.4-16.8l12.6-28.8l-3.6-25.6Z',
labelX: 178, labelY: 272, labelSize: 'text-[13px]'
},
'TH': {
path: 'M221.6,240.8l28.4,8.6l32.8-12.4l18.4,4.2l8.6,28.4l-14.6,28.6l-28.4,12.4l-32.6-4.8l-18.6-18.4l-8.4-14.2l4.8-32.6l9.6,0.2Z',
labelX: 262, labelY: 285, labelSize: 'text-[11px]'
},
'SN': {
path: 'M280.2,200l38.4-4.6l28.6-22.8l22.8,8.6l18.4,24.2l4.2,32.8l-14.6,32.4l-28.4,18.6l-38.6,4.2l-28.4-18.4l-14.6-28.6l-8.6-28.4l8.4-12.6l12.4-5.4Z',
labelX: 340, labelY: 255, labelSize: 'text-[12px]'
},
'RP': {
path: 'M54.4,280.8l28.6,12.4l-4.8,28.6l-8.4,32.4l-18.6,28.4l-24.8,4.2l-18.4-22.6l-4.2-32.4l12.4-28.6l18.6-14.8l19.6-7.6Z',
labelX: 52, labelY: 340, labelSize: 'text-[11px]'
},
'SL': {
path: 'M26.2,382.6l24.8-4.2l12.4,18.6l-4.8,18.4l-18.6,4.2l-14.2-12.4l-4.2-14.6l4.6-10Z',
labelX: 38, labelY: 400, labelSize: 'text-[9px]'
},
'BW': {
path: 'M54.4,401l18.6-28.4l8.4-32.4l4.8-28.6l32.4-4.8l28.4,4.2l24.6-8.6l12.4,18.6l8.6,32.4l-4.2,38.6l-18.4,32.4l-32.6,18.6l-38.4,4.2l-28.6-12.4l-14.2-22.6l-6.2-28.6l4.8-18.4l18.6-4.2l-8.6,28.6l-10.4,12.4Z',
labelX: 118, labelY: 420, labelSize: 'text-[13px]'
},
'BY': {
path: 'M150.2,305.6l24.6-8.6l18.4-28.4l32.6,4.8l28.4-12.4l14.6-28.6l28.4,18.4l38.6-4.2l28.4-18.6l18.4,12.4l8.6,38.4l-4.2,48.6l-18.6,38.4l-38.4,28.6l-48.6,12.4l-38.4-4.8l-32.4-18.6l-18.6-32.4l4.2-38.6l-8.6-32.4l-12.4,18.6l-4.6,27.6Z',
labelX: 290, labelY: 395, labelSize: 'text-[16px]'
},
}
export function GermanyMap({
selectedState,
onSelectState,
suggestedState = null,
className = ''
}: GermanyMapProps) {
const { isDark } = useTheme()
const getStateStyle = (stateId: string) => {
const isSelected = selectedState === stateId
const isSuggested = suggestedState === stateId && !selectedState
if (isSelected) {
return 'fill-blue-500 stroke-blue-700'
}
if (isSuggested) {
return isDark
? 'fill-green-500/40 stroke-green-400 animate-pulse'
: 'fill-green-200 stroke-green-500 animate-pulse'
}
return isDark
? 'fill-white/10 stroke-white/25 hover:fill-blue-400/30 hover:stroke-blue-400/50'
: 'fill-slate-100 stroke-slate-300 hover:fill-blue-100 hover:stroke-blue-400'
}
const getLabelStyle = (stateId: string) => {
const isSelected = selectedState === stateId
const isSuggested = suggestedState === stateId && !selectedState
if (isSelected) {
return 'fill-white font-bold'
}
if (isSuggested) {
return isDark ? 'fill-green-300 font-semibold' : 'fill-green-700 font-semibold'
}
return isDark ? 'fill-white/60' : 'fill-slate-500'
}
return (
<div className={`relative ${className}`}>
<svg
viewBox="0 0 500 600"
className="w-full h-full"
style={{ maxHeight: '420px' }}
>
{/* Hintergrund und Filter */}
<defs>
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="3" result="blur"/>
<feMerge>
<feMergeNode in="blur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="2" dy="3" stdDeviation="4" floodOpacity="0.4"/>
</filter>
</defs>
{/* Bundesländer - von groß nach klein für korrekte Überlappung */}
{['BY', 'NI', 'BW', 'NW', 'BB', 'MV', 'SN', 'ST', 'HE', 'TH', 'RP', 'SH', 'SL', 'HB', 'HH', 'BE'].map((stateId) => {
const state = statePaths[stateId]
const isSelected = selectedState === stateId
const isSuggested = suggestedState === stateId && !selectedState
return (
<g
key={stateId}
onClick={() => onSelectState(stateId)}
className="cursor-pointer transition-all duration-200"
style={{
filter: isSelected ? 'url(#shadow)' : isSuggested ? 'url(#glow)' : 'none'
}}
>
<path
d={state.path}
className={getStateStyle(stateId)}
strokeWidth={isSelected ? 2.5 : isSuggested ? 2 : 1.2}
fillRule="evenodd"
/>
<text
x={state.labelX}
y={state.labelY}
className={`${state.labelSize} ${getLabelStyle(stateId)} pointer-events-none select-none`}
textAnchor="middle"
dominantBaseline="middle"
>
{stateId}
</text>
</g>
)
})}
</svg>
{/* Hinweis bei vorgeschlagenem Bundesland */}
{suggestedState && !selectedState && (
<div className={`absolute top-2 left-1/2 -translate-x-1/2 px-3 py-1.5 rounded-lg text-xs ${
isDark ? 'bg-green-500/20 text-green-300 border border-green-500/30' : 'bg-green-50 text-green-700 border border-green-200'
}`}>
Vorschlag: <strong>{bundeslaender[suggestedState]}</strong> (Klicken zum Bestätigen)
</div>
)}
{/* Ausgewähltes Bundesland */}
{selectedState && (
<div className={`absolute bottom-2 left-1/2 -translate-x-1/2 px-4 py-2 rounded-xl backdrop-blur-xl text-center ${
isDark ? 'bg-blue-500/30 text-white border border-blue-400/30' : 'bg-blue-100 text-blue-900 border border-blue-200'
}`}>
<span className="font-semibold">{bundeslaender[selectedState]}</span>
</div>
)}
</div>
)
}