This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/studio-v2/components/companion/lesson-mode/VisualPieTimer.tsx
BreakPilot Dev 0f7be76e41 feat(companion): Migrate Companion from admin-v2 to studio-v2 as pure lesson tool
Remove Companion module entirely from admin-v2. Rebuild in studio-v2 as a
focused lesson timer (no dashboard mode). Direct flow: start → active → ended.
Fix timer bug where lastTickRef reset prevented countdown. Add companion link
to Sidebar and i18n translations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 23:50:20 +01:00

221 lines
6.4 KiB
TypeScript

'use client'
import { Pause, Play } from 'lucide-react'
import { TimerColorStatus } from '@/lib/companion/types'
import {
PIE_TIMER_RADIUS,
PIE_TIMER_CIRCUMFERENCE,
PIE_TIMER_STROKE_WIDTH,
PIE_TIMER_SIZE,
TIMER_COLOR_CLASSES,
TIMER_BG_COLORS,
formatTime,
} from '@/lib/companion/constants'
interface VisualPieTimerProps {
progress: number // 0-1 (how much time has elapsed)
remainingSeconds: number
totalSeconds: number
colorStatus: TimerColorStatus
isPaused: boolean
currentPhaseName: string
phaseColor: string
onTogglePause?: () => void
size?: 'sm' | 'md' | 'lg'
}
const sizeConfig = {
sm: { outer: 120, viewBox: 100, radius: 38, stroke: 6, fontSize: 'text-lg' },
md: { outer: 180, viewBox: 100, radius: 40, stroke: 7, fontSize: 'text-2xl' },
lg: { outer: 240, viewBox: 100, radius: 42, stroke: 8, fontSize: 'text-4xl' },
}
export function VisualPieTimer({
progress,
remainingSeconds,
totalSeconds,
colorStatus,
isPaused,
currentPhaseName,
phaseColor,
onTogglePause,
size = 'lg',
}: VisualPieTimerProps) {
const config = sizeConfig[size]
const circumference = 2 * Math.PI * config.radius
// Calculate stroke-dashoffset for progress
// Progress goes from 0 (full) to 1 (empty), so offset decreases as time passes
const strokeDashoffset = circumference * (1 - progress)
// For overtime, show a pulsing full circle
const isOvertime = colorStatus === 'overtime'
const displayTime = formatTime(remainingSeconds)
// Get color classes based on status
const colorClasses = TIMER_COLOR_CLASSES[colorStatus]
const bgColorClass = TIMER_BG_COLORS[colorStatus]
return (
<div className="flex flex-col items-center">
{/* Timer Circle */}
<div
className={`relative ${bgColorClass} rounded-full p-4 transition-colors duration-300`}
style={{ width: config.outer, height: config.outer }}
>
<svg
width="100%"
height="100%"
viewBox={`0 0 ${config.viewBox} ${config.viewBox}`}
className="transform -rotate-90"
>
{/* Background circle */}
<circle
cx={config.viewBox / 2}
cy={config.viewBox / 2}
r={config.radius}
fill="none"
stroke="currentColor"
strokeWidth={config.stroke}
className="text-slate-200"
/>
{/* Progress circle */}
<circle
cx={config.viewBox / 2}
cy={config.viewBox / 2}
r={config.radius}
fill="none"
stroke={isOvertime ? '#dc2626' : phaseColor}
strokeWidth={config.stroke}
strokeLinecap="round"
strokeDasharray={circumference}
strokeDashoffset={isOvertime ? 0 : strokeDashoffset}
className={`transition-all duration-100 ${isOvertime ? 'animate-pulse' : ''}`}
/>
</svg>
{/* Center Content */}
<div className="absolute inset-0 flex flex-col items-center justify-center">
{/* Time Display */}
<span
className={`
font-mono font-bold ${config.fontSize}
${isOvertime ? 'text-red-600 animate-pulse' : colorStatus === 'critical' ? 'text-red-500' : colorStatus === 'warning' ? 'text-amber-500' : 'text-slate-900'}
`}
>
{displayTime}
</span>
{/* Phase Name */}
<span className="text-sm text-slate-500 mt-1">
{currentPhaseName}
</span>
{/* Paused Indicator */}
{isPaused && (
<span className="text-xs text-amber-600 font-medium mt-1 flex items-center gap-1">
<Pause className="w-3 h-3" />
Pausiert
</span>
)}
{/* Overtime Badge */}
{isOvertime && (
<span className="absolute -bottom-2 px-2 py-0.5 bg-red-600 text-white text-xs font-bold rounded-full">
+{Math.abs(Math.floor(remainingSeconds / 60))} Min
</span>
)}
</div>
{/* Pause/Play Button (overlay) */}
{onTogglePause && (
<button
onClick={onTogglePause}
className={`
absolute inset-0 rounded-full
flex items-center justify-center
opacity-0 hover:opacity-100
bg-black/20 backdrop-blur-sm
transition-opacity duration-200
`}
aria-label={isPaused ? 'Fortsetzen' : 'Pausieren'}
>
{isPaused ? (
<Play className="w-12 h-12 text-white" />
) : (
<Pause className="w-12 h-12 text-white" />
)}
</button>
)}
</div>
{/* Status Text */}
<div className="mt-4 text-center">
{isOvertime ? (
<p className="text-red-600 font-semibold animate-pulse">
Ueberzogen - Zeit fuer die naechste Phase!
</p>
) : colorStatus === 'critical' ? (
<p className="text-red-500 font-medium">
Weniger als 2 Minuten verbleibend
</p>
) : colorStatus === 'warning' ? (
<p className="text-amber-500">
Weniger als 5 Minuten verbleibend
</p>
) : null}
</div>
</div>
)
}
/**
* Compact timer for header/toolbar
*/
export function CompactTimer({
remainingSeconds,
colorStatus,
isPaused,
phaseName,
phaseColor,
}: {
remainingSeconds: number
colorStatus: TimerColorStatus
isPaused: boolean
phaseName: string
phaseColor: string
}) {
const isOvertime = colorStatus === 'overtime'
return (
<div className="flex items-center gap-3 px-4 py-2 bg-white border border-slate-200 rounded-xl">
{/* Phase indicator */}
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: phaseColor }}
/>
{/* Phase name */}
<span className="text-sm font-medium text-slate-600">{phaseName}</span>
{/* Time */}
<span
className={`
font-mono font-bold
${isOvertime ? 'text-red-600 animate-pulse' : colorStatus === 'critical' ? 'text-red-500' : colorStatus === 'warning' ? 'text-amber-500' : 'text-slate-900'}
`}
>
{formatTime(remainingSeconds)}
</span>
{/* Paused badge */}
{isPaused && (
<span className="px-2 py-0.5 bg-amber-100 text-amber-700 text-xs font-medium rounded">
Pausiert
</span>
)}
</div>
)
}