Enhance ToggleTheme component and add Uptime link in Footer with translations

- Updated ToggleTheme component styles for better visibility in dark mode.
- Added a new Uptime link in the Footer for system status monitoring.
- Introduced translations for Uptime in English, Dutch, German, and French to support multilingual users.
This commit is contained in:
becarta
2025-06-08 01:13:56 +02:00
parent 7f06a0d546
commit 46fa503dda
8 changed files with 442 additions and 4 deletions

View File

@@ -0,0 +1,43 @@
import React, { useState, useEffect } from 'react';
const REFRESH_INTERVAL = 300; // seconds
export default function UpdateTimer({ onRefresh }) {
const [secondsLeft, setSecondsLeft] = useState(REFRESH_INTERVAL);
useEffect(() => {
const interval = setInterval(() => {
setSecondsLeft((prev) => {
if (prev > 1) {
return prev - 1;
} else {
if (typeof onRefresh === 'function') onRefresh();
return REFRESH_INTERVAL;
}
});
}, 1000);
return () => clearInterval(interval);
}, [onRefresh]);
const percent = (secondsLeft / REFRESH_INTERVAL) * 100;
const coverPercent = 100 - percent;
return (
<div className="w-full flex flex-col items-center mt-2 mb-2 max-w-5xl mx-auto">
<div className="flex justify-center items-center mb-0.5 w-full">
<span className="text-base font-semibold text-gray-300 dark:text-gray-200 text-center w-full">Next update in {secondsLeft}s</span>
</div>
<div
className="w-full h-3 rounded bg-gray-300 dark:bg-gray-700 overflow-hidden relative"
style={{
background: 'linear-gradient(90deg, #22c55e 0%, #eab308 60%, #ef4444 100%)',
}}
>
<div
className="h-3 bg-gray-300 dark:bg-gray-700 absolute top-0 right-0 transition-all duration-300"
style={{ width: `${coverPercent}%` }}
></div>
</div>
</div>
);
}

View File

@@ -0,0 +1,15 @@
---
import UptimeStatusIsland from './UptimeStatusIsland.jsx';
import UpdateTimer from './UpdateTimer.jsx';
---
<UpdateTimer client:only="react" onRefresh={() => {}} />
<UptimeStatusIsland client:only="react" />
<style>
/**** Custom grid for 40 heartbeat squares ****/
.grid-cols-40 {
display: grid;
grid-template-columns: repeat(40, minmax(0, 1fr));
}
</style>

View File

@@ -0,0 +1,151 @@
import React, { useState, useEffect, useCallback } from 'react';
function getStatusColor(validCert) {
if (validCert === false) return 'bg-red-600';
if (validCert === true) return 'bg-green-600';
return 'bg-gray-400';
}
function getHeartbeatColor(status) {
if (status === 1) return 'bg-green-600'; // UP
if (status === 0) return 'bg-red-600'; // DOWN
if (status === 2) return 'bg-yellow-400'; // PENDING
if (status === 3) return 'bg-blue-500'; // MAINTENANCE
return 'bg-gray-300';
}
function getCertBg(days) {
if (typeof days !== 'number') return 'bg-gray-400 dark:bg-gray-600';
if (days < 0) return 'bg-red-600 animate-pulse border-2 border-red-500 dark:bg-red-800';
if (days < 5) return 'bg-red-600 dark:bg-red-800';
if (days <= 15) return 'bg-yellow-400 dark:bg-yellow-500 text-gray-900';
return 'bg-green-600 dark:bg-green-700';
}
function getCertText(days) {
if (typeof days !== 'number') return 'Cert Exp';
if (days < 0) return 'Cert Expired!';
return 'Cert Exp';
}
function getAvgPingBg(avgPing) {
if (typeof avgPing !== 'number') return 'bg-gray-300 dark:bg-gray-700';
if (avgPing <= 80) return 'bg-green-600 text-white dark:bg-green-700 dark:text-white';
if (avgPing <= 200) return 'bg-yellow-400 text-gray-900 dark:bg-yellow-500 dark:text-gray-900';
return 'bg-red-600 text-white dark:bg-red-800 dark:text-white';
}
function getUptime24hBg(uptime) {
if (typeof uptime !== 'number') return 'bg-gray-300 dark:bg-gray-700';
if (uptime >= 99.9) return 'bg-green-600 text-white dark:bg-green-700 dark:text-white';
if (uptime >= 99) return 'bg-yellow-400 text-gray-900 dark:bg-yellow-500 dark:text-gray-900';
return 'bg-red-600 text-white dark:bg-red-800 dark:text-white';
}
export default function UptimeStatusIsland() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const fetchData = useCallback(async () => {
setLoading(true);
setError(null);
try {
const response = await fetch('/api/uptime');
if (!response.ok) throw new Error('Failed to fetch uptime data');
const json = await response.json();
setData(json);
} catch (err) {
setError(err.message || 'Unknown error');
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchData();
}, [fetchData]);
return (
<div>
<div className="grid gap-10 md:grid-cols-2">
{loading ? (
<div className="text-center text-gray-500 dark:text-gray-400">Loading...</div>
) : error ? (
<div className="text-center text-red-500 dark:text-red-400">{error}</div>
) : data && data.publicGroupList && data.publicGroupList.length > 0 ? (
data.publicGroupList.map((group) => (
<section key={group.id} className="bg-white dark:bg-slate-800 rounded-2xl shadow-lg p-8 flex flex-col">
<h2 className="text-2xl font-bold mb-6 text-gray-900 dark:text-white">{group.name}</h2>
<ul className="space-y-6">
{group.monitorList.map((monitor) => (
<li key={monitor.id} className="flex flex-col gap-2 bg-gray-50 dark:bg-slate-900 rounded-xl px-5 py-4 shadow border border-gray-200 dark:border-gray-700">
{/* First row: 5-column grid for fixed badge placement, name gets col-span-2 */}
<div className="grid grid-cols-5 items-center w-full gap-2">
{/* Name (left, col-span-2, min-w-0) */}
<div className="flex items-center min-w-0 col-span-2">
<span className={`inline-block w-3 h-3 rounded-full ${getStatusColor(monitor.validCert)} mr-2`}></span>
<span className="font-semibold text-lg text-gray-800 dark:text-gray-100 truncate">{monitor.name}</span>
</div>
{/* Cert Exp (center-left, col-span-1) */}
<div className="flex justify-center col-span-1">
{monitor.certExpiryDaysRemaining !== undefined && (
<span className={`px-2 py-0 rounded-full text-xs font-medium text-white ${getCertBg(monitor.certExpiryDaysRemaining)}`}
title={monitor.certExpiryDaysRemaining < 0 ? 'Certificate expired!' : `Certificate expires in ${monitor.certExpiryDaysRemaining} days`}>
{getCertText(monitor.certExpiryDaysRemaining)}
</span>
)}
</div>
{/* Avg. (center-right, col-span-1) */}
<div className="flex justify-center col-span-1">
{monitor.avgPing !== undefined && (
<span className={`px-2 py-0 rounded-full text-xs font-medium ${getAvgPingBg(monitor.avgPing)}`}>
Avg.: {monitor.avgPing < 100 ? monitor.avgPing.toFixed(1) : Math.round(monitor.avgPing)} ms
</span>
)}
</div>
{/* 24h (right, col-span-1) */}
<div className="flex justify-end col-span-1">
{monitor.uptime24h !== undefined && (
<span className={`px-2 py-0 rounded-full text-xs font-medium ${getUptime24hBg(monitor.uptime24h)}`}>
24h: {monitor.uptime24h.toFixed(1)}%
</span>
)}
</div>
</div>
{/* Second row: Heartbeat bar, always full width, evenly distributed squares */}
{monitor.heartbeatHistory && monitor.heartbeatHistory.length > 0 && (
<div className="flex items-center w-full">
<div className="grid grid-cols-40 gap-x-0.5 bg-gray-200 dark:bg-slate-700 rounded px-2 py-1 w-full overflow-x-auto">
{Array.from({ length: 40 }).map((_, i) => {
const hbArr = monitor.heartbeatHistory ?? [];
const hb = hbArr.slice(-40)[i];
return (
<span
key={i}
title={hb ? `Status: ${hb.status === 1 ? 'Up' : 'Down'}\nTime: ${hb.time}` : ''}
className={`w-full h-4 rounded-sm ${hb ? getHeartbeatColor(hb.status) : 'bg-gray-400 dark:bg-gray-600'} block`}
></span>
);
})}
</div>
</div>
)}
</li>
))}
</ul>
</section>
))
) : (
<div className="text-center text-gray-500 dark:text-gray-400">No status data available</div>
)}
</div>
<style>{`
.grid-cols-40 {
display: grid;
grid-template-columns: repeat(40, minmax(0, 1fr));
}
`}</style>
</div>
);
}

View File

@@ -13,7 +13,7 @@ export interface Props {
const {
label = 'Toggle between Dark and Light mode',
class:
className = 'text-muted dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 rounded-lg text-sm p-2.5 inline-flex items-center',
className = 'text-muted dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 rounded-lg text-sm p-2.5 inline-flex items-center',
iconClass = 'w-6 h-6',
iconName = 'tabler:sun',
} = Astro.props;

View File

@@ -125,6 +125,12 @@ const {
</a>
))
}
<a
class="hover:text-gray-700 hover:underline dark:hover:text-gray-200 transition duration-150 ease-in-out"
href="/uptime"
>
Uptime
</a>
</div>
</div>
</div>