Files
breakpilot-lehrer/studio-v2/components/Sidebar.tsx
Benjamin Admin 8d53b1f6b9
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-school (push) Successful in 28s
CI / test-go-edu-search (push) Successful in 30s
CI / test-python-klausur (push) Failing after 2m22s
CI / test-python-agent-core (push) Successful in 19s
CI / test-nodejs-website (push) Successful in 20s
Translate Sidebar nav labels into 26 languages
- Lernmodule, Eltern, Woerterbuch, Meet, KI-Assistent now translated
- Uses NAV_LABELS dict with fallback chain (lang → en → de)
- Fixed "nav_eltern" → shows "Eltern"/"Parents"/"Ebeveyn" etc.
- Fixed "Lernmodule" hardcoded → uses nav_lernmodule key
- Woerterbuch link now points to /vocabulary (not /vocab-worksheet)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-29 18:33:11 +02:00

262 lines
18 KiB
TypeScript

'use client'
import { useState } from 'react'
import { useRouter, usePathname } from 'next/navigation'
import { BPIcon } from '@/components/Logo'
import { useLanguage } from '@/lib/LanguageContext'
import { useTheme } from '@/lib/ThemeContext'
import { useAlerts } from '@/lib/AlertsContext'
import { useAlertsB2B } from '@/lib/AlertsB2BContext'
import { useMessages } from '@/lib/MessagesContext'
import { UserMenu } from '@/components/UserMenu'
/** Sidebar navigation labels in 26 languages */
const NAV_LABELS: Record<string, Record<string, string>> = {
nav_dashboard: { de: 'Dashboard', en: 'Dashboard', tr: 'Kontrol Paneli', ar: '\u0644\u0648\u062d\u0629 \u0627\u0644\u062a\u062d\u0643\u0645', uk: '\u041f\u0430\u043d\u0435\u043b\u044c', ru: '\u041f\u0430\u043d\u0435\u043b\u044c', pl: 'Panel', fr: 'Tableau de bord', es: 'Panel', it: 'Pannello', pt: 'Painel', nl: 'Dashboard', ro: 'Panou', el: '\u03a0\u03af\u03bd\u03b1\u03ba\u03b1\u03c2', bg: '\u0422\u0430\u0431\u043b\u043e', hr: 'Pocetna', cs: 'Prehled', hu: 'Iranyitopult', sv: 'Oversikt', da: 'Oversigt', fi: 'Yleisnakuma', sk: 'Prehad', sl: 'Pregled', lt: 'Skydelis', lv: 'Panelis', et: 'Ulevaade' },
nav_lernmodule: { de: 'Lernmodule', en: 'Learning', tr: 'Ogrenme', ar: '\u0627\u0644\u062a\u0639\u0644\u0645', uk: '\u041d\u0430\u0432\u0447\u0430\u043d\u043d\u044f', ru: '\u041e\u0431\u0443\u0447\u0435\u043d\u0438\u0435', pl: 'Nauka', fr: 'Apprentissage', es: 'Aprendizaje', it: 'Apprendimento', pt: 'Aprendizagem', nl: 'Leren', ro: 'Invatare', el: '\u039c\u03ac\u03b8\u03b7\u03c3\u03b7', bg: '\u0423\u0447\u0435\u043d\u0435', hr: 'Ucenje', cs: 'Uceni', hu: 'Tanulas', sv: 'Larande', da: 'Laering', fi: 'Oppiminen', sk: 'Ucenie', sl: 'Ucenje', lt: 'Mokymasis', lv: 'Macisanas', et: 'Oppimine' },
nav_eltern: { de: 'Eltern', en: 'Parents', tr: 'Ebeveyn', ar: '\u0627\u0644\u0648\u0627\u0644\u062f\u064a\u0646', uk: '\u0411\u0430\u0442\u044c\u043a\u0438', ru: '\u0420\u043e\u0434\u0438\u0442\u0435\u043b\u0438', pl: 'Rodzice', fr: 'Parents', es: 'Padres', it: 'Genitori', pt: 'Pais', nl: 'Ouders', ro: 'Parinti', el: '\u0393\u03bf\u03bd\u03b5\u03af\u03c2', bg: '\u0420\u043e\u0434\u0438\u0442\u0435\u043b\u0438', hr: 'Roditelji', cs: 'Rodice', hu: 'Szulok', sv: 'Foraldrar', da: 'Foraeldre', fi: 'Vanhemmat', sk: 'Rodicia', sl: 'Starsi', lt: 'Tevai', lv: 'Vecaki', et: 'Vanemad' },
nav_woerterbuch: { de: 'Woerterbuch', en: 'Dictionary', tr: 'Sozluk', ar: '\u0627\u0644\u0642\u0627\u0645\u0648\u0633', uk: '\u0421\u043b\u043e\u0432\u043d\u0438\u043a', ru: '\u0421\u043b\u043e\u0432\u0430\u0440\u044c', pl: 'Slownik', fr: 'Dictionnaire', es: 'Diccionario', it: 'Dizionario', pt: 'Dicionario', nl: 'Woordenboek', ro: 'Dictionar', el: '\u039b\u03b5\u03be\u03b9\u03ba\u03cc', bg: '\u0420\u0435\u0447\u043d\u0438\u043a', hr: 'Rjecnik', cs: 'Slovnik', hu: 'Szotar', sv: 'Ordbok', da: 'Ordbog', fi: 'Sanakirja', sk: 'Slovnik', sl: 'Slovar', lt: 'Zodynas', lv: 'Vardnica', et: 'Sonaraamat' },
nav_meet: { de: 'Videokonferenz', en: 'Video Call', tr: 'Gorusme', ar: '\u0645\u0643\u0627\u0644\u0645\u0629', uk: '\u0412\u0456\u0434\u0435\u043e\u0434\u0437\u0432\u0456\u043d\u043e\u043a', ru: '\u0412\u0438\u0434\u0435\u043e\u0437\u0432\u043e\u043d\u043e\u043a', pl: 'Wideorozmowa', fr: 'Visioconference', es: 'Videollamada', it: 'Videochiamata', pt: 'Videochamada', nl: 'Videogesprek', ro: 'Videoconferinta', el: '\u0392\u03b9\u03bd\u03c4\u03b5\u03bf\u03ba\u03bb\u03ae\u03c3\u03b7', bg: '\u0412\u0438\u0434\u0435\u043e\u0440\u0430\u0437\u0433\u043e\u0432\u043e\u0440', hr: 'Videopoziv', cs: 'Videohovor', hu: 'Videohivas', sv: 'Videosamtal', da: 'Videoopkald', fi: 'Videopuhelu', sk: 'Videohovor', sl: 'Videoklic', lt: 'Vaizdo skambutis', lv: 'Videozvans', et: 'Videokoone' },
nav_companion: { de: 'KI-Assistent', en: 'AI Assistant', tr: 'Yapay Zeka', ar: '\u0645\u0633\u0627\u0639\u062f \u0630\u0643\u064a', uk: '\u0428\u0406-\u0430\u0441\u0438\u0441\u0442\u0435\u043d\u0442', ru: '\u0418\u0418-\u0430\u0441\u0441\u0438\u0441\u0442\u0435\u043d\u0442', pl: 'Asystent AI', fr: 'Assistant IA', es: 'Asistente IA', it: 'Assistente IA', pt: 'Assistente IA', nl: 'AI-assistent', ro: 'Asistent AI', el: 'AI \u0392\u03bf\u03b7\u03b8\u03cc\u03c2', bg: 'AI \u0430\u0441\u0438\u0441\u0442\u0435\u043d\u0442', hr: 'AI pomoenik', cs: 'AI asistent', hu: 'AI asszisztens', sv: 'AI-assistent', da: 'AI-assistent', fi: 'Tekoalyavustaja', sk: 'AI asistent', sl: 'AI pomoenik', lt: 'DI asistentas', lv: 'MI paligs', et: 'Tehisabiabi' },
}
function navLabel(key: string, lang: string): string {
return NAV_LABELS[key]?.[lang] || NAV_LABELS[key]?.['en'] || NAV_LABELS[key]?.['de'] || key
}
interface SidebarProps {
selectedTab?: string
onTabChange?: (tab: string) => void
}
export function Sidebar({ selectedTab = 'dashboard', onTabChange }: SidebarProps) {
const [sidebarHovered, setSidebarHovered] = useState(false)
const { t, language } = useLanguage()
const { isDark } = useTheme()
const { unreadCount } = useAlerts()
const { unreadCount: b2bUnreadCount } = useAlertsB2B()
const { unreadCount: messagesUnreadCount } = useMessages()
const router = useRouter()
const pathname = usePathname()
const navItems = [
{ id: 'dashboard', labelKey: 'nav_dashboard', href: '/', icon: (
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" />
</svg>
)},
{ id: 'dokumente', labelKey: 'nav_dokumente', href: '/', icon: (
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
</svg>
)},
{ id: 'klausuren', labelKey: 'nav_klausuren', href: '/', icon: (
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
)},
{ id: 'analytics', labelKey: 'nav_analytics', href: '/', icon: (
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} 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: 'alerts', labelKey: 'nav_alerts', href: '/alerts', icon: (
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
</svg>
), showBadge: true },
{ id: 'alerts-b2b', labelKey: 'nav_alerts_b2b', href: '/alerts-b2b', icon: (
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
), showB2BBadge: true },
{ id: 'messages', labelKey: 'nav_messages', href: '/messages', icon: (
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
</svg>
), showMessagesBadge: true },
{ id: 'lernmodule', labelKey: 'nav_lernmodule', href: '/learn', icon: (
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
</svg>
)},
{ id: 'eltern', labelKey: 'nav_eltern', href: '/parent', icon: (
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} 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>
)},
{ id: 'vokabeln', labelKey: 'nav_woerterbuch', href: '/vocabulary', icon: (
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
</svg>
)},
{ id: 'worksheet-editor', labelKey: 'nav_worksheet_editor', href: '/worksheet-editor', icon: (
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
)},
{ id: 'worksheet-cleanup', labelKey: 'nav_worksheet_cleanup', href: '/worksheet-cleanup', icon: (
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z" />
</svg>
)},
{ id: 'korrektur', labelKey: 'nav_korrektur', href: '/korrektur', icon: (
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
)},
{ id: 'companion', labelKey: 'nav_companion', href: '/companion', icon: (
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="9" strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M12 6v6l4 2" />
</svg>
)},
{ id: 'meet', labelKey: 'nav_meet', href: '/meet', icon: (
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
)},
]
const handleNavClick = (item: typeof navItems[0]) => {
// Check if this is an external page (has a specific href)
if (item.href !== '/') {
router.push(item.href)
} else if (item.id === 'vokabeln') {
router.push('/vocabulary')
} else if (item.id === 'meet') {
router.push('/meet')
} else if (item.id === 'alerts') {
router.push('/alerts')
} else {
// For dashboard tabs, either navigate or call the callback
if (pathname !== '/') {
router.push('/')
}
if (onTabChange) {
onTabChange(item.id)
}
}
}
// Determine active item based on pathname or selectedTab
const getActiveItem = () => {
if (pathname === '/companion') return 'companion'
if (pathname === '/meet') return 'meet'
if (pathname === '/vocab-worksheet' || pathname === '/vocabulary') return 'vokabeln'
if (pathname === '/worksheet-editor') return 'worksheet-editor'
if (pathname === '/worksheet-cleanup') return 'worksheet-cleanup'
if (pathname === '/magic-help') return 'magic-help'
if (pathname === '/alerts') return 'alerts'
if (pathname === '/alerts-b2b') return 'alerts-b2b'
if (pathname === '/messages') return 'messages'
if (pathname?.startsWith('/korrektur')) return 'korrektur'
return selectedTab
}
const activeItem = getActiveItem()
return (
<aside
className="flex-shrink-0 w-[72px] hover:w-[200px] transition-all duration-300 group"
onMouseEnter={() => setSidebarHovered(true)}
onMouseLeave={() => setSidebarHovered(false)}
>
<div className={`sticky top-4 h-[calc(100vh-32px)] backdrop-blur-2xl rounded-3xl border flex flex-col p-3 overflow-y-auto overflow-x-hidden scrollbar-hide ${
isDark
? 'bg-white/10 border-white/20'
: 'bg-white/70 border-black/10 shadow-xl'
}`}>
{/* Logo - Cupertino Clean */}
<div className="mb-8">
<div className="flex items-center gap-4">
<BPIcon variant="cupertino" size={48} className="flex-shrink-0" />
<div className="opacity-0 group-hover:opacity-100 transition-opacity duration-300 whitespace-nowrap">
<span className={`font-semibold text-lg ${isDark ? 'text-white' : 'text-slate-900'}`}>BreakPilot</span>
<span className={`block text-xs ${isDark ? 'text-white/60' : 'text-slate-500'}`}>Studio v2</span>
</div>
</div>
</div>
{/* Navigation */}
<nav className="flex-1 space-y-2">
{navItems.map((item) => (
<button
key={item.id}
onClick={() => handleNavClick(item)}
className={`relative w-full flex items-center gap-4 p-3 rounded-2xl transition-all ${
activeItem === item.id
? isDark
? 'bg-white/20 text-white shadow-lg'
: 'bg-indigo-100 text-indigo-900 shadow-lg'
: item.id === 'alerts' && unreadCount > 0
? isDark
? 'text-amber-400 hover:bg-amber-500/10'
: 'text-amber-600 hover:bg-amber-50'
: item.id === 'alerts-b2b' && b2bUnreadCount > 0
? isDark
? 'text-blue-400 hover:bg-blue-500/10'
: 'text-blue-600 hover:bg-blue-50'
: item.id === 'messages' && messagesUnreadCount > 0
? isDark
? 'text-green-400 hover:bg-green-500/10'
: 'text-green-600 hover:bg-green-50'
: isDark
? 'text-white/60 hover:bg-white/10 hover:text-white'
: 'text-slate-600 hover:bg-slate-100 hover:text-slate-900'
}`}
>
<span className="relative flex-shrink-0">
{item.icon}
{item.id === 'alerts' && unreadCount > 0 && (
<span className="absolute -top-1 -right-1 w-4 h-4 bg-red-500 rounded-full text-[10px] text-white flex items-center justify-center font-medium">
{unreadCount > 9 ? '9+' : unreadCount}
</span>
)}
{item.id === 'alerts-b2b' && b2bUnreadCount > 0 && (
<span className="absolute -top-1 -right-1 w-4 h-4 bg-blue-500 rounded-full text-[10px] text-white flex items-center justify-center font-medium">
{b2bUnreadCount > 9 ? '9+' : b2bUnreadCount}
</span>
)}
{item.id === 'messages' && messagesUnreadCount > 0 && (
<span className="absolute -top-1 -right-1 w-4 h-4 bg-green-500 rounded-full text-[10px] text-white flex items-center justify-center font-medium">
{messagesUnreadCount > 9 ? '9+' : messagesUnreadCount}
</span>
)}
</span>
<span className="font-medium opacity-0 group-hover:opacity-100 transition-opacity duration-300 whitespace-nowrap flex items-center gap-2">
{NAV_LABELS[item.labelKey] ? navLabel(item.labelKey, language) : t(item.labelKey)}
{item.id === 'alerts' && unreadCount > 0 && (
<span className="px-1.5 py-0.5 text-[10px] rounded-full bg-amber-500/20 text-amber-500">
{unreadCount}
</span>
)}
{item.id === 'alerts-b2b' && b2bUnreadCount > 0 && (
<span className="px-1.5 py-0.5 text-[10px] rounded-full bg-blue-500/20 text-blue-500">
{b2bUnreadCount}
</span>
)}
{item.id === 'messages' && messagesUnreadCount > 0 && (
<span className="px-1.5 py-0.5 text-[10px] rounded-full bg-green-500/20 text-green-500">
{messagesUnreadCount}
</span>
)}
</span>
</button>
))}
</nav>
{/* User Menu */}
<div className={`pt-4 border-t ${isDark ? 'border-white/10' : 'border-slate-200'}`}>
<UserMenu
userName="Lehrer Max"
userEmail="max@schule.de"
userInitials="LM"
isExpanded={sidebarHovered}
/>
</div>
</div>
</aside>
)
}