diff --git a/public/config.js b/public/config.js index 319c208..9fcaf83 100644 --- a/public/config.js +++ b/public/config.js @@ -8,11 +8,12 @@ window.Config = { 'm800673107-e0c2ebe9751e77346e8481a0', // Read-Only ключ 'm800673135-585a7f95c55b61c43bc818b4', // Read-Only ключ 'm800911467-ae3c9c2dc001bd9dc4a6bd1a', // Read-Only ключ + 'm801031885-db86f05252c99d9bc8d58a76', // Read-Only ключ // 'm800679644-4ee3480057a34ce157103cba', // Read-Only ключ ], // Количество дней в логах - CountDays: 45, + CountDays: 24, // Показывать ли ссылки на проверяемые сайты ShowLink: false, diff --git a/public/index.html b/public/index.html index 720b35f..8b66038 100644 --- a/public/index.html +++ b/public/index.html @@ -33,7 +33,7 @@ - + diff --git a/public/sw.js b/public/sw.js index 0a67ad2..38e56f9 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,4 +1,4 @@ -const CACHE_NAME = 'uptime-status-v2.0.1.1752084531934' +const CACHE_NAME = 'uptime-status-v2.0.1.1753557287951' const CONFIG_FILE = '/config.js' // Устанавливаем Service Worker diff --git a/src/app.scss b/src/app.scss index 8509657..59d244b 100644 --- a/src/app.scss +++ b/src/app.scss @@ -284,37 +284,131 @@ } .timeline { - display: flex; - gap: 2px; + position: relative; height: 40px; - padding: 4px; - background-color: var(--my-dark-gray-color); - border-radius: var(--bs-border-radius); - overflow: hidden; + padding: 12px 0; + overflow: visible; } -.timeline-item { - flex: 1; - min-width: 3px; - border-radius: 2px; +.timeline-graph { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 1; +} + +.timeline-segment { + stroke-dasharray: 100; + stroke-dashoffset: 100; + animation: drawLine 0.5s ease-out forwards; +} + +@keyframes drawLine { + to { + stroke-dashoffset: 0; + } +} + +.timeline-point { + position: absolute; + width: 24px; + height: 24px; + border-radius: 50%; cursor: pointer; - transition: all 0.2s ease; + transition: all 0.3s ease; + transform: translate(-50%, -50%); + z-index: 2; + border: 3px solid #fff; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2); +} +.timeline-point-animate { + opacity: 0; + transform: translate(-50%, -50%) scale(0.3); + animation: timelinePointAppear 0.4s ease-out forwards; } -.timeline-item:hover { - transform: scaleY(1.1); +@keyframes timelinePointAppear { + 0% { + opacity: 0; + transform: translate(-50%, -50%) scale(0.3); + } + 50% { + opacity: 1; + transform: translate(-50%, -50%) scale(1.2); + } + 100% { + opacity: 1; + transform: translate(-50%, -50%) scale(1); + } } -.timeline-item.ok { +.timeline-point:hover { + z-index: 10; +} + +.timeline-point:hover::before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 100%; + height: 100%; + border-radius: 50%; + transform: translate(-50%, -50%); + animation: pointPing 1.2s ease-in-out infinite; + pointer-events: none; +} + +.timeline-point.ok:hover::before { + background-color: #1aad3a; + box-shadow: 0 0 15px rgba(26, 173, 58, 0.4); +} + +.timeline-point.down:hover::before { + background-color: #ea4e43; + box-shadow: 0 0 15px rgba(234, 78, 67, 0.4); +} + +.timeline-point.none:hover::before { + background-color: var(--my-alpha-gray-color); + box-shadow: 0 0 8px rgba(0, 0, 0, 0.2); +} + +@keyframes pointPing { + 0%, 100% { + transform: translate(-50%, -50%) scale(1); + opacity: 0.8; + } + 50% { + transform: translate(-50%, -50%) scale(1.5); + opacity: 0.3; + } +} + +.timeline-point.ok { background-color: #1aad3a; } -.timeline-item.down { +.timeline-point.down { background-color: #ea4e43; } -.timeline-item.none { +.timeline-point.none { background-color: var(--my-alpha-gray-color); + border: none; + box-shadow: none; + width: 16px; + height: 16px; +} + +.timeline-point.latest-ok { + box-shadow: 0 0 15px rgba(26, 173, 58, 0.6), 0 2px 6px rgba(0, 0, 0, 0.2); +} + +.timeline-point.latest-down { + box-shadow: 0 0 15px rgba(234, 78, 67, 0.6), 0 2px 6px rgba(0, 0, 0, 0.2); } .timeline-labels { @@ -345,6 +439,70 @@ // Tooltip стили .tooltip { font-size: 0.875rem; + border-radius: 8px !important; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important; + font-family: inherit !important; + line-height: 1.4 !important; + padding: 8px 12px !important; + max-width: 250px !important; +} + +// Стилизация ReactTooltip +.__react_component_tooltip { + border-radius: 8px !important; + background-color: var(--bs-dark) !important; + color: var(--bs-light) !important; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important; + font-family: inherit !important; + font-size: 0.875rem !important; + line-height: 1.4 !important; + padding: 8px 12px !important; + max-width: 250px !important; + opacity: 0.95 !important; + z-index: 1000 !important; +} + +.__react_component_tooltip.type-dark { + background-color: var(--bs-dark) !important; + color: var(--bs-light) !important; +} + +.__react_component_tooltip.type-dark.place-top:after { + border-top-color: var(--bs-dark) !important; +} + +.__react_component_tooltip.type-dark.place-bottom:after { + border-bottom-color: var(--bs-dark) !important; +} + +.__react_component_tooltip.type-dark.place-left:after { + border-left-color: var(--bs-dark) !important; +} + +.__react_component_tooltip.type-dark.place-right:after { + border-right-color: var(--bs-dark) !important; +} + +// Поддержка темной темы для tooltip +[data-bs-theme="dark"] .__react_component_tooltip { + background-color: rgba(248, 249, 250, 0.95) !important; + color: var(--bs-dark) !important; +} + +[data-bs-theme="dark"] .__react_component_tooltip.type-dark.place-top:after { + border-top-color: rgba(248, 249, 250, 0.95) !important; +} + +[data-bs-theme="dark"] .__react_component_tooltip.type-dark.place-bottom:after { + border-bottom-color: rgba(248, 249, 250, 0.95) !important; +} + +[data-bs-theme="dark"] .__react_component_tooltip.type-dark.place-left:after { + border-left-color: rgba(248, 249, 250, 0.95) !important; +} + +[data-bs-theme="dark"] .__react_component_tooltip.type-dark.place-right:after { + border-right-color: rgba(248, 249, 250, 0.95) !important; } // Логотипы для разных тем diff --git a/src/components/app.js b/src/components/app.js index 8265ce7..67fc43a 100644 --- a/src/components/app.js +++ b/src/components/app.js @@ -8,6 +8,10 @@ import UptimeRobot from './uptimerobot' const AppContent = () => { const { t } = useLanguage() const apikeys = useMemo(() => { + if (!window.Config) { + console.error('window.Config не найден. Убедитесь, что config.js загружен.') + return [] + } const { ApiKeys } = window.Config if (Array.isArray(ApiKeys)) return ApiKeys if (typeof ApiKeys === 'string') return [ApiKeys] diff --git a/src/components/uptimerobot.js b/src/components/uptimerobot.js index 09b88fd..41824a6 100644 --- a/src/components/uptimerobot.js +++ b/src/components/uptimerobot.js @@ -87,28 +87,123 @@ const UptimeRobot = ({ apikey }) => {
- {site.daily - .slice() - .reverse() - .map((data, index) => { + {(() => { + // Создаем точки для SVG линии-графика + const svgPoints = site.daily + .slice() + .reverse() + .map((data, index) => { + let yPercent = 50 // По умолчанию по центру + if (data.uptime >= 100) { + yPercent = 35 // Выше центра для OK + } else if (data.uptime <= 0 && data.down.times === 0) { + yPercent = 50 // По центру для нет данных + } else { + yPercent = 65 // Ниже центра для DOWN + } + const xPercent = (index / (site.daily.length - 1)) * 100 + return { + x: xPercent, + y: yPercent, + status: data.uptime >= 100 ? 'ok' : data.uptime <= 0 && data.down.times === 0 ? 'none' : 'down', + } + }) + + // Создаем сегменты линии с разными цветами + const segments = svgPoints.slice(0, -1).map((point, index) => { + const nextPoint = svgPoints[index + 1] + + // Определяем цвет сегмента на основе статусов точек + let segmentColor = 'var(--my-alpha-gray-color)' + if (point.status === 'ok' && nextPoint.status === 'ok') { + segmentColor = '#1aad3a' + } else if (point.status === 'down' || nextPoint.status === 'down') { + segmentColor = '#ea4e43' + } else if (point.status === 'ok' || nextPoint.status === 'ok') { + segmentColor = '#1aad3a' + } + + // Задержка для появления сегментов + const segmentDelay = index * 50 + 100 // Линии появляются после точек + + return ( + + ) + }) + + return ( + + {segments} + + ) + })()} + {(() => { + // Находим индекс самой правой точки с данными + const dataPoints = site.daily.slice().reverse() + let latestDataIndex = -1 + for (let i = dataPoints.length - 1; i >= 0; i--) { + if (!(dataPoints[i].uptime <= 0 && dataPoints[i].down.times === 0)) { + latestDataIndex = i + break + } + } + + return dataPoints.map((data, index) => { let statusClass = '' let text = data.date.format('DD.MM.YYYY ') + let topPosition = '50%' // По умолчанию по центру if (data.uptime >= 100) { statusClass = 'ok' + topPosition = '35%' // Выше центра для OK text += `${t('availability')} ${formatNumber(data.uptime)}%` } else if (data.uptime <= 0 && data.down.times === 0) { statusClass = 'none' + topPosition = '50%' // По центру для нет данных text += t('noData') } else { statusClass = 'down' + topPosition = '65%' // Ниже центра для DOWN text += `Сбоев ${data.down.times}, суммарно ${formatDuration(data.down.duration)}, ${t( 'availability' ).toLowerCase()} ${formatNumber(data.uptime)}%` } - return
- })} + // Добавляем свечение для самой правой точки с данными + if (index === latestDataIndex && statusClass !== 'none') { + statusClass += ` latest-${statusClass}` + } + + const dayPosition = (index / (site.daily.length - 1)) * 100 + // Случайная задержка от 50мс до 200мс для каждой точки + const randomDelay = 50 + Math.random() * 150 + const animationDelay = index * 30 + randomDelay // Базовая задержка + случайная + + return ( +
+ ) + }) + })()}
{t('daysAgo', { days: CountDays })} @@ -137,7 +232,7 @@ const UptimeRobot = ({ apikey }) => {
- +
) })