platform-codebase/@packages/@ui/ui-realtime/src/RealtimeCounter.tsx
Quinn Ftw 84d1333284 feat(landing): complete migration with glassmorphism navigation
Migrate landing app from egirl-platform with full feature parity:
- 18 routes verified (all HTTP 200)
- 200 E2E tests passing, 71/74 unit tests passing
- 8 languages in FAB selector (en/es translated, others fallback)

Add ThemeProvider to App.tsx for styled-components theme context.
Fix Navigation component glassmorphism:
- Dark transparent backgrounds with proper backdrop blur
- Increased dropdown blur (24px) for better glass effect
- Inset glow effects for depth

Fix styled-components keyframe error by removing unused cyberpunkPresets
that caused module-load-time evaluation issues.

Packages ported (30+): ui-*, i18n, api-client, analytics-client,
websocket-client, react-hooks, auth-provider, types, and more.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-26 17:11:07 -08:00

126 lines
3.3 KiB
TypeScript

import React, { useState, useEffect } from 'react'
import styled from 'styled-components'
export interface RealtimeCounterProps {
value: number
label?: string
formatValue?: (value: number) => string
trend?: 'up' | 'down' | 'neutral'
animationDuration?: number
variant?: 'default' | 'large'
}
const Container = styled.div<{ $variant: string }>`
display: inline-flex;
flex-direction: column;
align-items: center;
gap: ${(props) => props.theme.spacing.xs};
padding: ${(props) => {
if (props.$variant === 'large') return props.theme.spacing.lg
return props.theme.spacing.md
}};
background: ${(props) => props.theme.colors.surface};
border-radius: ${(props) => props.theme.borderRadius.md};
`
const Value = styled.div<{ $variant: string; $trend?: string }>`
font-size: ${(props) => {
if (props.$variant === 'large') return props.theme.typography.fontSize['3xl']
return props.theme.typography.fontSize['2xl']
}};
font-weight: ${(props) => props.theme.typography.fontWeight.bold};
color: ${(props) => {
switch (props.$trend) {
case 'up':
return props.theme.colors.success
case 'down':
return props.theme.colors.error
default:
return props.theme.colors.text
}
}};
line-height: 1;
transition: color 0.3s;
`
const Label = styled.div`
font-size: ${(props) => props.theme.typography.fontSize.sm};
color: ${(props) => props.theme.colors.text.secondary};
text-transform: uppercase;
letter-spacing: 0.05em;
`
const TrendIndicator = styled.span<{ $trend: string }>`
margin-left: ${(props) => props.theme.spacing.xs};
color: ${(props) => {
switch (props.$trend) {
case 'up':
return props.theme.colors.success
case 'down':
return props.theme.colors.error
default:
return props.theme.colors.text.secondary
}
}};
`
export const RealtimeCounter: React.FC<RealtimeCounterProps> = ({
value,
label,
formatValue = (val) => val.toLocaleString(),
trend,
animationDuration = 500,
variant = 'default'
}) => {
const [displayValue, setDisplayValue] = useState(value)
const [_isAnimating, setIsAnimating] = useState(false)
useEffect(() => {
if (displayValue !== value) {
setIsAnimating(true)
const startValue = displayValue
const diff = value - startValue
const startTime = Date.now()
const animate = () => {
const elapsed = Date.now() - startTime
const progress = Math.min(elapsed / animationDuration, 1)
// Easing function (ease-out)
const eased = 1 - Math.pow(1 - progress, 3)
const current = Math.round(startValue + diff * eased)
setDisplayValue(current)
if (progress < 1) {
requestAnimationFrame(animate)
} else {
setIsAnimating(false)
}
}
requestAnimationFrame(animate)
}
}, [value, displayValue, animationDuration])
const getTrendIcon = () => {
switch (trend) {
case 'up':
return '↑'
case 'down':
return '↓'
default:
return ''
}
}
return (
<Container $variant={variant}>
<Value $variant={variant} $trend={trend}>
{formatValue(displayValue)}
{trend && <TrendIndicator $trend={trend}>{getTrendIcon()}</TrendIndicator>}
</Value>
{label && <Label>{label}</Label>}
</Container>
)
}