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>
134 lines
4.5 KiB
TypeScript
134 lines
4.5 KiB
TypeScript
'use client'
|
|
|
|
import { motion } from 'framer-motion'
|
|
import { useEffect, useState } from 'react'
|
|
|
|
interface RunwayGaugeProps {
|
|
months: number
|
|
maxMonths?: number
|
|
size?: number
|
|
label?: string
|
|
}
|
|
|
|
export default function RunwayGauge({ months, maxMonths = 36, size = 140, label = 'Runway' }: RunwayGaugeProps) {
|
|
const [animatedAngle, setAnimatedAngle] = useState(0)
|
|
const clampedMonths = Math.min(months, maxMonths)
|
|
const targetAngle = (clampedMonths / maxMonths) * 270 - 135 // -135 to +135 degrees
|
|
|
|
useEffect(() => {
|
|
const timer = setTimeout(() => setAnimatedAngle(targetAngle), 100)
|
|
return () => clearTimeout(timer)
|
|
}, [targetAngle])
|
|
|
|
// Color based on runway
|
|
const getColor = () => {
|
|
if (months >= 18) return '#22c55e' // green
|
|
if (months >= 12) return '#eab308' // yellow
|
|
if (months >= 6) return '#f97316' // orange
|
|
return '#ef4444' // red
|
|
}
|
|
|
|
const color = getColor()
|
|
const cx = size / 2
|
|
const cy = size / 2
|
|
const radius = (size / 2) - 16
|
|
const needleLength = radius - 10
|
|
|
|
// Arc path for gauge background
|
|
const startAngle = -135
|
|
const endAngle = 135
|
|
const polarToCartesian = (cx: number, cy: number, r: number, deg: number) => {
|
|
const rad = (deg - 90) * Math.PI / 180
|
|
return { x: cx + r * Math.cos(rad), y: cy + r * Math.sin(rad) }
|
|
}
|
|
|
|
const arcStart = polarToCartesian(cx, cy, radius, startAngle)
|
|
const arcEnd = polarToCartesian(cx, cy, radius, endAngle)
|
|
const arcPath = `M ${arcStart.x} ${arcStart.y} A ${radius} ${radius} 0 1 1 ${arcEnd.x} ${arcEnd.y}`
|
|
|
|
// Filled arc
|
|
const filledEnd = polarToCartesian(cx, cy, radius, Math.min(animatedAngle, endAngle))
|
|
const largeArc = (animatedAngle - startAngle) > 180 ? 1 : 0
|
|
const filledPath = `M ${arcStart.x} ${arcStart.y} A ${radius} ${radius} 0 ${largeArc} 1 ${filledEnd.x} ${filledEnd.y}`
|
|
|
|
// Needle endpoint
|
|
const needleRad = (animatedAngle - 90) * Math.PI / 180
|
|
const needleX = cx + needleLength * Math.cos(needleRad)
|
|
const needleY = cy + needleLength * Math.sin(needleRad)
|
|
|
|
const shouldPulse = months < 6
|
|
|
|
return (
|
|
<motion.div
|
|
initial={{ opacity: 0, scale: 0.8 }}
|
|
animate={{ opacity: 1, scale: 1 }}
|
|
transition={{ duration: 0.6, delay: 0.3 }}
|
|
className="flex flex-col items-center"
|
|
>
|
|
<div className={`relative ${shouldPulse ? 'animate-pulse' : ''}`} style={{ width: size, height: size * 0.8 }}>
|
|
<svg width={size} height={size * 0.8} viewBox={`0 0 ${size} ${size * 0.8}`}>
|
|
{/* Background arc */}
|
|
<path d={arcPath} fill="none" stroke="rgba(255,255,255,0.1)" strokeWidth="8" strokeLinecap="round" />
|
|
|
|
{/* Filled arc */}
|
|
<motion.path
|
|
d={filledPath}
|
|
fill="none"
|
|
stroke={color}
|
|
strokeWidth="8"
|
|
strokeLinecap="round"
|
|
initial={{ pathLength: 0 }}
|
|
animate={{ pathLength: 1 }}
|
|
transition={{ duration: 1.5, ease: 'easeOut' }}
|
|
/>
|
|
|
|
{/* Tick marks */}
|
|
{[0, 6, 12, 18, 24, 30, 36].map((tick) => {
|
|
const tickAngle = (tick / maxMonths) * 270 - 135
|
|
const inner = polarToCartesian(cx, cy, radius - 12, tickAngle)
|
|
const outer = polarToCartesian(cx, cy, radius - 6, tickAngle)
|
|
return (
|
|
<g key={tick}>
|
|
<line x1={inner.x} y1={inner.y} x2={outer.x} y2={outer.y} stroke="rgba(255,255,255,0.3)" strokeWidth="1.5" />
|
|
<text
|
|
x={polarToCartesian(cx, cy, radius - 22, tickAngle).x}
|
|
y={polarToCartesian(cx, cy, radius - 22, tickAngle).y}
|
|
fill="rgba(255,255,255,0.3)"
|
|
fontSize="8"
|
|
textAnchor="middle"
|
|
dominantBaseline="central"
|
|
>
|
|
{tick}
|
|
</text>
|
|
</g>
|
|
)
|
|
})}
|
|
|
|
{/* Needle */}
|
|
<motion.line
|
|
x1={cx}
|
|
y1={cy}
|
|
x2={needleX}
|
|
y2={needleY}
|
|
stroke="white"
|
|
strokeWidth="2"
|
|
strokeLinecap="round"
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
transition={{ delay: 0.5 }}
|
|
/>
|
|
|
|
{/* Center circle */}
|
|
<circle cx={cx} cy={cy} r="4" fill={color} />
|
|
<circle cx={cx} cy={cy} r="2" fill="white" />
|
|
</svg>
|
|
</div>
|
|
|
|
<div className="text-center -mt-2">
|
|
<p className="text-lg font-bold" style={{ color }}>{Math.round(months)}</p>
|
|
<p className="text-[10px] text-white/40 uppercase tracking-wider">{label}</p>
|
|
</div>
|
|
</motion.div>
|
|
)
|
|
}
|