mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
feat: add fonts, animations, and shared utilities for dashboard redesign
Add Outfit + IBM Plex Mono fonts, traffic progress animations (shimmer, unlimited flow/pulse, trial glow), traffic zone utility, and animated number hook.
This commit is contained in:
@@ -15,7 +15,7 @@
|
|||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link
|
<link
|
||||||
href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&display=swap"
|
href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=Manrope:wght@400;500;600;700;800&family=Outfit:wght@400;500;600;700&display=swap"
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
/>
|
/>
|
||||||
<script src="https://telegram.org/js/telegram-web-app.js"></script>
|
<script src="https://telegram.org/js/telegram-web-app.js"></script>
|
||||||
|
|||||||
38
src/hooks/useAnimatedNumber.ts
Normal file
38
src/hooks/useAnimatedNumber.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
export function useAnimatedNumber(target: number, duration = 600): number {
|
||||||
|
const [current, setCurrent] = useState(target);
|
||||||
|
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();
|
||||||
|
|
||||||
|
const animate = (now: number) => {
|
||||||
|
const elapsed = now - startRef.current;
|
||||||
|
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;
|
||||||
|
|
||||||
|
if (progress < 1) {
|
||||||
|
rafRef.current = requestAnimationFrame(animate);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
rafRef.current = requestAnimationFrame(animate);
|
||||||
|
return () => cancelAnimationFrame(rafRef.current);
|
||||||
|
}, [target, duration]);
|
||||||
|
|
||||||
|
return current;
|
||||||
|
}
|
||||||
56
src/utils/trafficZone.ts
Normal file
56
src/utils/trafficZone.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
export type TrafficZone = 'normal' | 'warning' | 'danger' | 'critical';
|
||||||
|
|
||||||
|
interface TrafficZoneResult {
|
||||||
|
zone: TrafficZone;
|
||||||
|
textClass: string;
|
||||||
|
dotClass: string;
|
||||||
|
glowColor: string;
|
||||||
|
labelKey: string;
|
||||||
|
gradientFrom: string;
|
||||||
|
gradientTo: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ZONES: Record<TrafficZone, Omit<TrafficZoneResult, 'zone'>> = {
|
||||||
|
normal: {
|
||||||
|
textClass: 'text-success-400',
|
||||||
|
dotClass: 'bg-success-400',
|
||||||
|
glowColor: 'rgba(var(--color-success-500), 0.5)',
|
||||||
|
labelKey: 'dashboard.zone.normal',
|
||||||
|
gradientFrom: 'rgb(var(--color-success-500))',
|
||||||
|
gradientTo: 'rgb(var(--color-success-400))',
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
textClass: 'text-warning-400',
|
||||||
|
dotClass: 'bg-warning-400',
|
||||||
|
glowColor: 'rgba(var(--color-warning-500), 0.5)',
|
||||||
|
labelKey: 'dashboard.zone.warning',
|
||||||
|
gradientFrom: 'rgb(var(--color-warning-500))',
|
||||||
|
gradientTo: 'rgb(var(--color-warning-400))',
|
||||||
|
},
|
||||||
|
danger: {
|
||||||
|
textClass: 'text-warning-300',
|
||||||
|
dotClass: 'bg-warning-300',
|
||||||
|
glowColor: 'rgba(var(--color-warning-400), 0.5)',
|
||||||
|
labelKey: 'dashboard.zone.danger',
|
||||||
|
gradientFrom: 'rgb(var(--color-warning-600))',
|
||||||
|
gradientTo: 'rgb(var(--color-warning-400))',
|
||||||
|
},
|
||||||
|
critical: {
|
||||||
|
textClass: 'text-error-400',
|
||||||
|
dotClass: 'bg-error-400',
|
||||||
|
glowColor: 'rgba(var(--color-error-500), 0.5)',
|
||||||
|
labelKey: 'dashboard.zone.critical',
|
||||||
|
gradientFrom: 'rgb(var(--color-error-500))',
|
||||||
|
gradientTo: 'rgb(var(--color-error-400))',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getTrafficZone(percent: number): TrafficZoneResult {
|
||||||
|
let zone: TrafficZone;
|
||||||
|
if (percent >= 90) zone = 'critical';
|
||||||
|
else if (percent >= 75) zone = 'danger';
|
||||||
|
else if (percent >= 50) zone = 'warning';
|
||||||
|
else zone = 'normal';
|
||||||
|
|
||||||
|
return { zone, ...ZONES[zone] };
|
||||||
|
}
|
||||||
@@ -112,6 +112,8 @@ export default {
|
|||||||
'Roboto',
|
'Roboto',
|
||||||
'sans-serif',
|
'sans-serif',
|
||||||
],
|
],
|
||||||
|
display: ['Outfit', 'Manrope', 'system-ui', 'sans-serif'],
|
||||||
|
mono: ['IBM Plex Mono', 'ui-monospace', 'monospace'],
|
||||||
},
|
},
|
||||||
borderRadius: {
|
borderRadius: {
|
||||||
bento: '24px',
|
bento: '24px',
|
||||||
@@ -173,6 +175,11 @@ export default {
|
|||||||
'move-horizontal': 'moveHorizontal 40s ease infinite',
|
'move-horizontal': 'moveHorizontal 40s ease infinite',
|
||||||
'move-in-circle-fast': 'moveInCircle 20s ease infinite',
|
'move-in-circle-fast': 'moveInCircle 20s ease infinite',
|
||||||
'spotlight-ace': 'spotlightAce 2s ease 0.75s 1 forwards',
|
'spotlight-ace': 'spotlightAce 2s ease 0.75s 1 forwards',
|
||||||
|
// Dashboard traffic animations
|
||||||
|
'traffic-shimmer': 'trafficShimmer 2s ease-in-out infinite',
|
||||||
|
'unlimited-flow': 'unlimitedFlow 3s linear infinite',
|
||||||
|
'unlimited-pulse': 'unlimitedPulse 2s ease-in-out infinite',
|
||||||
|
'trial-glow': 'trialGlow 2s ease-in-out infinite',
|
||||||
},
|
},
|
||||||
keyframes: {
|
keyframes: {
|
||||||
fadeIn: {
|
fadeIn: {
|
||||||
@@ -251,6 +258,23 @@ export default {
|
|||||||
'0%': { opacity: '0', transform: 'translate(-72%, -62%) scale(0.5)' },
|
'0%': { opacity: '0', transform: 'translate(-72%, -62%) scale(0.5)' },
|
||||||
'100%': { opacity: '1', transform: 'translate(-50%, -40%) scale(1)' },
|
'100%': { opacity: '1', transform: 'translate(-50%, -40%) scale(1)' },
|
||||||
},
|
},
|
||||||
|
// Dashboard traffic keyframes
|
||||||
|
trafficShimmer: {
|
||||||
|
'0%': { transform: 'translateX(-100%)' },
|
||||||
|
'100%': { transform: 'translateX(200%)' },
|
||||||
|
},
|
||||||
|
unlimitedFlow: {
|
||||||
|
'0%': { backgroundPosition: '0% 50%' },
|
||||||
|
'100%': { backgroundPosition: '200% 50%' },
|
||||||
|
},
|
||||||
|
unlimitedPulse: {
|
||||||
|
'0%, 100%': { opacity: '0.6', transform: 'scale(1)' },
|
||||||
|
'50%': { opacity: '1', transform: 'scale(1.3)' },
|
||||||
|
},
|
||||||
|
trialGlow: {
|
||||||
|
'0%, 100%': { boxShadow: '0 0 15px rgba(var(--color-warning-500), 0.2)' },
|
||||||
|
'50%': { boxShadow: '0 0 30px rgba(var(--color-warning-500), 0.4)' },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
transitionTimingFunction: {
|
transitionTimingFunction: {
|
||||||
smooth: 'cubic-bezier(0.4, 0, 0.2, 1)',
|
smooth: 'cubic-bezier(0.4, 0, 0.2, 1)',
|
||||||
|
|||||||
Reference in New Issue
Block a user