add compomemts&front

This commit is contained in:
2026-08-10 16:16:37 +03:00
parent b0ee07823e
commit b0754e8cdb
27 changed files with 5256 additions and 962 deletions
+311
View File
@@ -0,0 +1,311 @@
import React, { useState, useEffect, useRef } from 'react';
import { motion } from 'framer-motion';
interface CurrencyRate {
currency: string;
rate: number;
change: number;
}
interface MetalRate {
metal: string;
price: number;
change: number;
}
const BottomBar: React.FC = () => {
const [currencyRates, setCurrencyRates] = useState<CurrencyRate[]>([
{ currency: 'USD', rate: 3.22, change: 0.5 },
{ currency: 'EUR', rate: 3.45, change: -0.2 },
{ currency: 'CNY', rate: 0.44, change: 0.1 },
{ currency: 'RUB', rate: 0.035, change: -0.3 },
]);
const [metalRates, setMetalRates] = useState<MetalRate[]>([
{ metal: 'Золото', price: 1950, change: 0.8 },
{ metal: 'Серебро', price: 23.5, change: -0.5 },
{ metal: 'Платина', price: 920, change: 0.3 },
{ metal: 'Палладий', price: 1450, change: -0.7 },
]);
const [isOnline, setIsOnline] = useState(navigator.onLine);
const [isPaused, setIsPaused] = useState(false);
const [currentTime, setCurrentTime] = useState(new Date()); // ✅ Добавлено
const scrollRef = useRef<HTMLDivElement>(null);
const getAllItems = () => {
const currencyItems = currencyRates.map(item => ({
id: `currency-${item.currency}`,
type: 'currency' as const,
label: item.currency,
value: item.rate,
change: item.change,
displayValue: item.rate.toFixed(item.rate < 1 ? 3 : 2),
}));
const metalItems = metalRates.map(item => ({
id: `metal-${item.metal}`,
type: 'metal' as const,
label: item.metal,
value: item.price,
change: item.change,
displayValue: `$${item.price.toFixed(item.price < 10 ? 2 : 0)}`,
}));
return [...currencyItems, ...metalItems];
};
const items = getAllItems();
const doubledItems = [...items, ...items, ...items];
// ✅ Обновление времени каждую секунду
useEffect(() => {
const interval = setInterval(() => {
setCurrentTime(new Date());
}, 1000);
return () => clearInterval(interval);
}, []);
useEffect(() => {
const handleOnline = () => setIsOnline(true);
const handleOffline = () => setIsOnline(false);
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
useEffect(() => {
const loadRates = async () => {
try {
if (isOnline) {
const response = await fetch('http://localhost:3001/api/rates');
if (response.ok) {
const data = await response.json();
setCurrencyRates(data.currency || currencyRates);
setMetalRates(data.metals || metalRates);
}
} else {
const stored = localStorage.getItem('currencyRates');
if (stored) {
const data = JSON.parse(stored);
setCurrencyRates(data.currency || currencyRates);
setMetalRates(data.metals || metalRates);
}
}
} catch (error) {
console.warn('Ошибка загрузки курсов:', error);
}
};
loadRates();
const interval = setInterval(loadRates, 60000);
return () => clearInterval(interval);
}, [isOnline]);
const handleMouseEnter = () => setIsPaused(true);
const handleMouseLeave = () => setIsPaused(false);
const handleTouchStart = () => setIsPaused(true);
const handleTouchEnd = () => setIsPaused(false);
return (
<motion.div
className="bottom-bar"
initial={{ y: 100 }}
animate={{ y: 0 }}
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
style={{
position: 'fixed',
bottom: 0,
left: 0,
right: 0,
background: 'rgba(44, 62, 80, 0.95)',
backdropFilter: 'blur(10px)',
color: '#fff',
padding: '8px 20px',
display: 'flex',
alignItems: 'center',
gap: '16px',
overflow: 'hidden',
zIndex: 1000,
height: '44px',
borderTop: '1px solid rgba(255,255,255,0.1)',
fontSize: '13px',
fontFamily: 'monospace',
}}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onTouchStart={handleTouchStart}
onTouchEnd={handleTouchEnd}
>
{/* Статус */}
<div style={{
display: 'flex',
alignItems: 'center',
gap: '12px',
flexShrink: 0,
zIndex: 2,
background: 'rgba(44, 62, 80, 0.95)',
paddingRight: '12px',
}}>
<span style={{
color: isOnline ? '#2ecc71' : '#e74c3c',
fontSize: '12px',
}}>
{isOnline ? '●' : '○'}
</span>
<span style={{ fontWeight: 600, opacity: 0.7, fontSize: '11px' }}>КУРСЫ</span>
<div style={{
width: '1px',
height: '24px',
background: 'rgba(255,255,255,0.15)',
marginLeft: '4px',
}} />
</div>
{/* Бегущая строка */}
<div
ref={scrollRef}
style={{
flex: 1,
overflow: 'hidden',
position: 'relative',
height: '100%',
display: 'flex',
alignItems: 'center',
maskImage: 'linear-gradient(to right, transparent, black 3%, black 97%, transparent)',
WebkitMaskImage: 'linear-gradient(to right, transparent, black 3%, black 97%, transparent)',
}}
>
<motion.div
animate={{
x: isPaused ? 0 : ['0%', '-33.33%'],
}}
transition={{
duration: 35,
repeat: Infinity,
ease: 'linear',
repeatType: 'loop',
}}
style={{
display: 'flex',
gap: '40px',
whiteSpace: 'nowrap',
willChange: 'transform',
transform: 'translateZ(0)',
backfaceVisibility: 'hidden',
WebkitFontSmoothing: 'antialiased',
MozOsxFontSmoothing: 'grayscale',
textRendering: 'geometricPrecision',
paddingRight: '40px',
height: '100%',
alignItems: 'center',
}}
>
{doubledItems.map((item, index) => (
<div
key={`${item.id}-${index}`}
style={{
display: 'flex',
gap: '8px',
alignItems: 'center',
whiteSpace: 'nowrap',
flexShrink: 0,
fontSmooth: 'always',
WebkitFontSmoothing: 'antialiased',
textRendering: 'geometricPrecision',
}}
>
<span style={{
fontWeight: 600,
color: 'rgba(255,255,255,0.9)',
fontSize: '13px',
letterSpacing: '0.3px',
}}>
{item.label}
</span>
<span style={{
color: 'rgba(255,255,255,0.8)',
fontSize: '13px',
letterSpacing: '0.2px',
}}>
{item.displayValue}
</span>
<span style={{
color: item.change >= 0 ? '#2ecc71' : '#e74c3c',
fontSize: '11px',
fontWeight: 500,
display: 'flex',
alignItems: 'center',
gap: '2px',
letterSpacing: '0.2px',
}}>
{item.change >= 0 ? '▲' : '▼'}
{Math.abs(item.change).toFixed(1)}%
</span>
{item.type === 'metal' && (
<span style={{
fontSize: '9px',
opacity: 0.3,
marginLeft: '-2px',
}}>
$
</span>
)}
</div>
))}
</motion.div>
</div>
{/* ✅ Время с СЕКУНДАМИ и обновлением */}
<div style={{
marginLeft: 'auto',
fontSize: '11px',
opacity: 0.4,
whiteSpace: 'nowrap',
flexShrink: 0,
paddingLeft: '12px',
background: 'rgba(44, 62, 80, 0.95)',
zIndex: 2,
fontSmooth: 'always',
WebkitFontSmoothing: 'antialiased',
}}>
{currentTime.toLocaleString('ru-RU', {
timeZone: 'Europe/Minsk',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
})}
</div>
{/* Пауза */}
{isPaused && (
<div style={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
fontSize: '11px',
color: 'rgba(255,255,255,0.3)',
pointerEvents: 'none',
zIndex: 3,
background: 'rgba(0,0,0,0.5)',
padding: '2px 12px',
borderRadius: '4px',
backdropFilter: 'blur(5px)',
}}>
⏸ Пауза
</div>
)}
</motion.div>
);
};
export default BottomBar;