be126a7a39
Build pitch-deck / build-push-deploy (push) Successful in 1m22s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-consent (push) Successful in 31s
CI / test-python-voice (push) Successful in 30s
CI / test-bqas (push) Successful in 31s
NavigationFAB and SlideOverview now accept slideNames prop and render only the active slide list (filtered for showcase mode). Adds AI presenter start button to the FAB footer so it's accessible even when intro-presenter slide is hidden. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
204 lines
7.9 KiB
TypeScript
204 lines
7.9 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useCallback } from 'react'
|
|
import { motion, AnimatePresence } from 'framer-motion'
|
|
import { Menu, X, Maximize, Minimize, Bot, Sun, Moon } from 'lucide-react'
|
|
import { Language } from '@/lib/types'
|
|
import { t } from '@/lib/i18n'
|
|
|
|
interface NavigationFABProps {
|
|
currentIndex: number
|
|
totalSlides: number
|
|
visitedSlides: Set<number>
|
|
onGoToSlide: (index: number) => void
|
|
lang: Language
|
|
onToggleLanguage: () => void
|
|
slideNames?: string[]
|
|
onPresenterStart?: () => void
|
|
}
|
|
|
|
export default function NavigationFAB({
|
|
currentIndex,
|
|
totalSlides,
|
|
visitedSlides,
|
|
onGoToSlide,
|
|
lang,
|
|
onToggleLanguage,
|
|
slideNames: slideNamesProp,
|
|
onPresenterStart,
|
|
}: NavigationFABProps) {
|
|
const [isOpen, setIsOpen] = useState(false)
|
|
const [isFullscreen, setIsFullscreen] = useState(false)
|
|
const [isLightMode, setIsLightMode] = useState(false)
|
|
|
|
const toggleTheme = useCallback(() => {
|
|
setIsLightMode(prev => {
|
|
const next = !prev
|
|
document.documentElement.classList.toggle('theme-light', next)
|
|
return next
|
|
})
|
|
}, [])
|
|
const i = t(lang)
|
|
const activeSlideNames = slideNamesProp ?? i.slideNames
|
|
|
|
const toggleFullscreen = useCallback(() => {
|
|
if (!document.fullscreenElement) {
|
|
document.documentElement.requestFullscreen()
|
|
setIsFullscreen(true)
|
|
} else {
|
|
document.exitFullscreen()
|
|
setIsFullscreen(false)
|
|
}
|
|
}, [])
|
|
|
|
return (
|
|
<div className="fixed bottom-6 right-6 z-50">
|
|
<AnimatePresence mode="wait">
|
|
{!isOpen ? (
|
|
<motion.button
|
|
key="fab"
|
|
initial={{ scale: 0 }}
|
|
animate={{ scale: 1 }}
|
|
exit={{ scale: 0 }}
|
|
whileHover={{ scale: 1.1 }}
|
|
whileTap={{ scale: 0.95 }}
|
|
onClick={() => setIsOpen(true)}
|
|
className="w-14 h-14 rounded-full bg-indigo-600 hover:bg-indigo-500
|
|
flex items-center justify-center shadow-lg shadow-indigo-600/30
|
|
transition-colors"
|
|
>
|
|
<Menu className="w-6 h-6 text-white" />
|
|
</motion.button>
|
|
) : (
|
|
<motion.div
|
|
key="panel"
|
|
initial={{ opacity: 0, scale: 0.9, y: 20 }}
|
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
exit={{ opacity: 0, scale: 0.9, y: 20 }}
|
|
transition={{ duration: 0.2 }}
|
|
className="w-[300px] max-h-[80vh] rounded-2xl overflow-hidden
|
|
bg-black/80 backdrop-blur-xl border border-white/10
|
|
shadow-2xl shadow-black/50"
|
|
>
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between px-4 py-3 border-b border-white/10">
|
|
<span className="text-sm font-semibold text-white">{i.nav.slides}</span>
|
|
<button
|
|
onClick={() => setIsOpen(false)}
|
|
className="w-7 h-7 rounded-full bg-white/10 flex items-center justify-center
|
|
hover:bg-white/20 transition-colors"
|
|
>
|
|
<X className="w-4 h-4 text-white/60" />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Slide List */}
|
|
<div className="overflow-y-auto max-h-[55vh] py-2">
|
|
{activeSlideNames.map((name, idx) => {
|
|
const isActive = idx === currentIndex
|
|
const isVisited = visitedSlides.has(idx)
|
|
const isAI = idx === totalSlides - 1
|
|
|
|
return (
|
|
<button
|
|
key={idx}
|
|
onClick={() => onGoToSlide(idx)}
|
|
className={`
|
|
w-full flex items-center gap-3 px-4 py-2.5 text-left
|
|
transition-all text-sm
|
|
${isActive
|
|
? 'bg-indigo-500/20 border-l-2 border-indigo-500 text-white'
|
|
: 'hover:bg-white/[0.06] text-white/60 hover:text-white border-l-2 border-transparent'
|
|
}
|
|
`}
|
|
>
|
|
<span className={`
|
|
w-6 h-6 rounded-full flex items-center justify-center text-xs font-mono shrink-0
|
|
${isActive
|
|
? 'bg-indigo-500 text-white'
|
|
: isVisited
|
|
? 'bg-white/10 text-white/60'
|
|
: 'bg-white/5 text-white/30'
|
|
}
|
|
`}>
|
|
{idx + 1}
|
|
</span>
|
|
<span className="flex-1 truncate">{name}</span>
|
|
{isAI && <Bot className="w-4 h-4 text-indigo-400 shrink-0" />}
|
|
{isActive && (
|
|
<span className="w-2 h-2 rounded-full bg-indigo-400 shrink-0" />
|
|
)}
|
|
</button>
|
|
)
|
|
})}
|
|
</div>
|
|
|
|
{/* Footer */}
|
|
<div className="border-t border-white/10 px-4 py-3 space-y-2">
|
|
{/* Language Toggle */}
|
|
<button
|
|
onClick={onToggleLanguage}
|
|
className="w-full flex items-center justify-between px-3 py-2 rounded-lg
|
|
bg-white/[0.05] hover:bg-white/[0.1] transition-colors text-sm"
|
|
>
|
|
<span className="text-white/50">{i.nav.language}</span>
|
|
<div className="flex items-center gap-1">
|
|
<span className={`px-2 py-0.5 rounded text-xs font-medium ${lang === 'de' ? 'bg-indigo-500 text-white' : 'text-white/40'}`}>
|
|
DE
|
|
</span>
|
|
<span className={`px-2 py-0.5 rounded text-xs font-medium ${lang === 'en' ? 'bg-indigo-500 text-white' : 'text-white/40'}`}>
|
|
EN
|
|
</span>
|
|
</div>
|
|
</button>
|
|
|
|
{/* Theme Toggle */}
|
|
<button
|
|
onClick={toggleTheme}
|
|
className="w-full flex items-center justify-between px-3 py-2 rounded-lg
|
|
bg-white/[0.05] hover:bg-white/[0.1] transition-colors text-sm"
|
|
>
|
|
<span className="text-white/50">{lang === 'de' ? 'Modus' : 'Mode'}</span>
|
|
<div className="flex items-center gap-1">
|
|
<span className={`px-2 py-0.5 rounded text-xs font-medium flex items-center gap-1 ${!isLightMode ? 'bg-indigo-500 text-white' : 'text-white/40'}`}>
|
|
<Moon className="w-3 h-3" /> {lang === 'de' ? 'Nacht' : 'Dark'}
|
|
</span>
|
|
<span className={`px-2 py-0.5 rounded text-xs font-medium flex items-center gap-1 ${isLightMode ? 'bg-amber-500 text-white' : 'text-white/40'}`}>
|
|
<Sun className="w-3 h-3" /> {lang === 'de' ? 'Tag' : 'Light'}
|
|
</span>
|
|
</div>
|
|
</button>
|
|
|
|
{/* AI Presenter */}
|
|
{onPresenterStart && (
|
|
<button
|
|
onClick={() => { onPresenterStart(); setIsOpen(false) }}
|
|
className="w-full flex items-center justify-between px-3 py-2 rounded-lg
|
|
bg-indigo-500/10 hover:bg-indigo-500/20 transition-colors text-sm"
|
|
>
|
|
<span className="text-indigo-300">{lang === 'de' ? 'KI-Präsentation starten' : 'Start AI Presenter'}</span>
|
|
<Bot className="w-4 h-4 text-indigo-400" />
|
|
</button>
|
|
)}
|
|
|
|
{/* Fullscreen */}
|
|
<button
|
|
onClick={toggleFullscreen}
|
|
className="w-full flex items-center justify-between px-3 py-2 rounded-lg
|
|
bg-white/[0.05] hover:bg-white/[0.1] transition-colors text-sm"
|
|
>
|
|
<span className="text-white/50">{i.nav.fullscreen}</span>
|
|
{isFullscreen ? (
|
|
<Minimize className="w-4 h-4 text-white/50" />
|
|
) : (
|
|
<Maximize className="w-4 h-4 text-white/50" />
|
|
)}
|
|
</button>
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
</div>
|
|
)
|
|
}
|