refactor: rewrite dashboard components to match prototype design

- Rewrite TrafficProgressBar with multi-segment gradient fill, flex-based
  zone tints, shimmer + highlight overlays, radial glow at fill edge
- Rewrite SubscriptionCardActive with zone header, big percentage on right,
  connect device card with device dots, tariff/days-left two-column row,
  sparkline placeholder, traffic refresh controls
- Rewrite SubscriptionCardExpired with red glow, grid pattern, gradient
  renew button, outline tariffs button
- Rewrite TrialOfferCard with animated glow background, grid pattern,
  icon glow animation, price tag with old price, gradient CTA buttons
- Rewrite StatsGrid with 2x2 card layout, icon+label+chevron header,
  big value numbers, zone-colored balance/earnings cards
- Update Sparkline with color prop and last-point dot indicator
- Update useAnimatedNumber to use easeOutExpo matching prototype
- Add formatTraffic utility for MB/GB/TB unit formatting
- Add mainHex to trafficZone for inline style support
- Update animations to match prototype timing
- Add new i18n keys for redesigned components
This commit is contained in:
Fringg
2026-02-25 10:22:50 +03:00
parent 78fe3c48eb
commit 6b688ad451
12 changed files with 967 additions and 555 deletions

View File

@@ -1,30 +1,26 @@
import { useEffect, useRef, useState } from 'react';
export function useAnimatedNumber(target: number, duration = 600): number {
const [current, setCurrent] = useState(target);
/**
* Animates a number from its current value to a new target.
* Uses easeOutExpo easing matching the prototype.
*/
export function useAnimatedNumber(target: number, duration = 1200): number {
const [value, setValue] = useState(0);
const ref = useRef({ start: 0, startTime: 0, target: 0 });
const rafRef = useRef<number>(0);
const startRef = useRef<number>(0);
const fromRef = useRef<number>(target);
const currentRef = useRef<number>(target);
const prevTargetRef = useRef<number>(target);
// Keep currentRef in sync with state
currentRef.current = current;
useEffect(() => {
if (prevTargetRef.current === target) return;
prevTargetRef.current = target;
fromRef.current = currentRef.current;
startRef.current = performance.now();
ref.current.start = value;
ref.current.target = target;
ref.current.startTime = performance.now();
const animate = (now: number) => {
const elapsed = now - startRef.current;
const elapsed = now - ref.current.startTime;
const progress = Math.min(elapsed / duration, 1);
const eased = 1 - Math.pow(1 - progress, 3);
const value = fromRef.current + (target - fromRef.current) * eased;
setCurrent(value);
currentRef.current = value;
// easeOutExpo
const ease = progress === 1 ? 1 : 1 - Math.pow(2, -10 * progress);
const current = ref.current.start + (ref.current.target - ref.current.start) * ease;
setValue(current);
if (progress < 1) {
rafRef.current = requestAnimationFrame(animate);
}
@@ -34,5 +30,5 @@ export function useAnimatedNumber(target: number, duration = 600): number {
return () => cancelAnimationFrame(rafRef.current);
}, [target, duration]);
return current;
return value;
}