refactor: Consolidate standalone services into admin-v2, add new SDK modules
Remove standalone services (ai-compliance-sdk root, developer-portal, dsms-gateway, dsms-node, night-scheduler) and legacy compliance/dsgvo pages. Add new SDK pipeline modules (academy, document-crawler, dsb-portal, incidents, whistleblower, reporting, sso, multi-tenant, industry-templates). Add drafting engine, legal corpus files (AT/CH/DE), pitch-deck, blog and Förderantrag pages. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
133
pitch-deck/components/ui/RunwayGauge.tsx
Normal file
133
pitch-deck/components/ui/RunwayGauge.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user