feat: add pitch-deck service to core infrastructure
Migrated pitch-deck from breakpilot-pwa to breakpilot-core. Container: bp-core-pitch-deck on port 3012. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
160
pitch-deck/components/NavigationFAB.tsx
Normal file
160
pitch-deck/components/NavigationFAB.tsx
Normal file
@@ -0,0 +1,160 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useCallback } from 'react'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { Menu, X, Maximize, Minimize, Bot } 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
|
||||
}
|
||||
|
||||
export default function NavigationFAB({
|
||||
currentIndex,
|
||||
totalSlides,
|
||||
visitedSlides,
|
||||
onGoToSlide,
|
||||
lang,
|
||||
onToggleLanguage,
|
||||
}: NavigationFABProps) {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [isFullscreen, setIsFullscreen] = useState(false)
|
||||
const i = t(lang)
|
||||
|
||||
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">
|
||||
{i.slideNames.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>
|
||||
|
||||
{/* 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user