Enhance UptimeStatusIsland component with mobile responsiveness and scroll tracking
- Added state management for mobile detection and scroll position tracking in the UptimeStatusIsland component. - Implemented responsive design adjustments for heartbeat display based on screen size. - Introduced visual indicators for scroll position on mobile to improve user experience. - Updated styles for heartbeat history container to accommodate mobile layout and scrolling behavior.
This commit is contained in:
@@ -96,6 +96,9 @@ export default function UptimeStatusIsland() {
|
|||||||
const [userZone, setUserZone] = useState(null);
|
const [userZone, setUserZone] = useState(null);
|
||||||
const [userLocale, setUserLocale] = useState(null);
|
const [userLocale, setUserLocale] = useState(null);
|
||||||
const [secondsToNextUpdate, setSecondsToNextUpdate] = useState(null);
|
const [secondsToNextUpdate, setSecondsToNextUpdate] = useState(null);
|
||||||
|
const [isMobile, setIsMobile] = useState(false);
|
||||||
|
const [showLeftShadow, setShowLeftShadow] = useState(false);
|
||||||
|
const heartbeatContainerRefs = useRef({});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setUserZone(Intl.DateTimeFormat().resolvedOptions().timeZone);
|
setUserZone(Intl.DateTimeFormat().resolvedOptions().timeZone);
|
||||||
@@ -260,6 +263,19 @@ export default function UptimeStatusIsland() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const checkMobile = () => setIsMobile(window.innerWidth < 640);
|
||||||
|
checkMobile();
|
||||||
|
window.addEventListener('resize', checkMobile);
|
||||||
|
return () => window.removeEventListener('resize', checkMobile);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Track scroll position for left shadow on mobile heartbeat bar
|
||||||
|
const handleHeartbeatScroll = (monitorId, e) => {
|
||||||
|
if (!isMobile) return;
|
||||||
|
setShowLeftShadow(e.target.scrollLeft > 0);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div style={{ width: '100%', margin: '0 auto', maxWidth: 1600, padding: '24px 0 8px 0' }}>
|
<div style={{ width: '100%', margin: '0 auto', maxWidth: 1600, padding: '24px 0 8px 0' }}>
|
||||||
@@ -305,21 +321,17 @@ export default function UptimeStatusIsland() {
|
|||||||
<ul className="space-y-6">
|
<ul className="space-y-6">
|
||||||
{group.monitorList.map((monitor) => (
|
{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">
|
<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">
|
||||||
<div className="grid grid-cols-5 items-center w-full gap-2">
|
{isMobile ? (
|
||||||
<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>
|
<div className="flex items-center min-w-0 mb-2">
|
||||||
<span className="font-semibold text-lg text-gray-800 dark:text-gray-100 truncate">{monitor.name}</span>
|
<span className={`inline-block w-3 h-3 rounded-full ${getStatusColor(monitor.validCert)} mr-2`}></span>
|
||||||
</div>
|
<span className="font-semibold text-lg text-gray-800 dark:text-gray-100 truncate">{monitor.name}</span>
|
||||||
<div className="flex flex-col items-center col-span-1 relative">
|
</div>
|
||||||
{monitor.certExpiryDaysRemaining !== undefined && (
|
<div className="flex flex-row items-center justify-between w-full gap-3">
|
||||||
<>
|
{monitor.certExpiryDaysRemaining !== undefined && (
|
||||||
<span className={`hidden sm:inline 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>
|
|
||||||
<span
|
<span
|
||||||
ref={getBadgeRef(monitor.id, 'cert')}
|
ref={getBadgeRef(monitor.id, 'cert')}
|
||||||
className={`sm:hidden px-2 py-0 rounded-full text-xs font-medium flex items-center justify-center ${getCertBg(monitor.certExpiryDaysRemaining)}`}
|
className={`px-2 py-1 rounded-full text-xs font-medium flex items-center justify-center ${getCertBg(monitor.certExpiryDaysRemaining)}`}
|
||||||
title={monitor.certExpiryDaysRemaining < 0 ? 'Certificate expired!' : `Certificate expires in ${monitor.certExpiryDaysRemaining} days`}
|
title={monitor.certExpiryDaysRemaining < 0 ? 'Certificate expired!' : `Certificate expires in ${monitor.certExpiryDaysRemaining} days`}
|
||||||
onClick={() => toggleBadge(monitor.id, 'cert')}
|
onClick={() => toggleBadge(monitor.id, 'cert')}
|
||||||
style={{ cursor: 'pointer', position: 'relative' }}
|
style={{ cursor: 'pointer', position: 'relative' }}
|
||||||
@@ -333,19 +345,11 @@ export default function UptimeStatusIsland() {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</>
|
)}
|
||||||
)}
|
{monitor.avgPing !== undefined && (
|
||||||
</div>
|
|
||||||
<div className="flex flex-col items-center col-span-1 relative">
|
|
||||||
{monitor.avgPing !== undefined && (
|
|
||||||
<>
|
|
||||||
<span className={`hidden sm:inline px-2 py-0 rounded-full text-xs font-medium ${getAvgPingBg(monitor.avgPing)}`}
|
|
||||||
title={`Avg. response: ${monitor.avgPing} ms`}>
|
|
||||||
Avg: {monitor.avgPing} ms
|
|
||||||
</span>
|
|
||||||
<span
|
<span
|
||||||
ref={getBadgeRef(monitor.id, 'avgPing')}
|
ref={getBadgeRef(monitor.id, 'avgPing')}
|
||||||
className={`sm:hidden px-2 py-0 rounded-full text-xs font-medium flex items-center justify-center ${getAvgPingBg(monitor.avgPing)}`}
|
className={`px-2 py-1 rounded-full text-xs font-medium flex items-center justify-center ${getAvgPingBg(monitor.avgPing)}`}
|
||||||
onClick={() => toggleBadge(monitor.id, 'avgPing')}
|
onClick={() => toggleBadge(monitor.id, 'avgPing')}
|
||||||
style={{ cursor: 'pointer', position: 'relative' }}
|
style={{ cursor: 'pointer', position: 'relative' }}
|
||||||
>
|
>
|
||||||
@@ -356,18 +360,11 @@ export default function UptimeStatusIsland() {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</>
|
)}
|
||||||
)}
|
{monitor.uptime24h !== undefined && (
|
||||||
</div>
|
|
||||||
<div className="flex flex-col items-center col-span-1 relative">
|
|
||||||
{monitor.uptime24h !== undefined && (
|
|
||||||
<>
|
|
||||||
<span className={`hidden sm:inline px-2 py-0 rounded-full text-xs font-medium ${getUptime24hBg(monitor.uptime24h)}`}>
|
|
||||||
24h: {monitor.uptime24h.toFixed(1)}%
|
|
||||||
</span>
|
|
||||||
<span
|
<span
|
||||||
ref={getBadgeRef(monitor.id, 'uptime')}
|
ref={getBadgeRef(monitor.id, 'uptime')}
|
||||||
className={`sm:hidden px-2 py-0 rounded-full text-xs font-medium flex items-center justify-center ${getUptime24hBg(monitor.uptime24h)}`}
|
className={`px-2 py-1 rounded-full text-xs font-medium flex items-center justify-center ${getUptime24hBg(monitor.uptime24h)}`}
|
||||||
onClick={() => toggleBadge(monitor.id, 'uptime')}
|
onClick={() => toggleBadge(monitor.id, 'uptime')}
|
||||||
style={{ cursor: 'pointer', position: 'relative' }}
|
style={{ cursor: 'pointer', position: 'relative' }}
|
||||||
>
|
>
|
||||||
@@ -378,17 +375,122 @@ export default function UptimeStatusIsland() {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</>
|
)}
|
||||||
)}
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-5 items-center w-full gap-2">
|
||||||
|
<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>
|
||||||
|
<div className="flex flex-col items-center col-span-1 relative">
|
||||||
|
{monitor.certExpiryDaysRemaining !== undefined && (
|
||||||
|
<>
|
||||||
|
<span className={`hidden sm:inline 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>
|
||||||
|
<span
|
||||||
|
ref={getBadgeRef(monitor.id, 'cert')}
|
||||||
|
className={`sm:hidden px-2 py-0 rounded-full text-xs font-medium flex items-center justify-center ${getCertBg(monitor.certExpiryDaysRemaining)}`}
|
||||||
|
title={monitor.certExpiryDaysRemaining < 0 ? 'Certificate expired!' : `Certificate expires in ${monitor.certExpiryDaysRemaining} days`}
|
||||||
|
onClick={() => toggleBadge(monitor.id, 'cert')}
|
||||||
|
style={{ cursor: 'pointer', position: 'relative' }}
|
||||||
|
>
|
||||||
|
<FiAward className="w-4 h-4" />
|
||||||
|
{openBadge[monitor.id]?.cert && (
|
||||||
|
<span className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 z-20 bg-white dark:bg-slate-800 text-gray-900 dark:text-gray-100 rounded-lg shadow-lg px-3 py-2 text-xs whitespace-nowrap border border-gray-200 dark:border-gray-700">
|
||||||
|
{monitor.certExpiryDaysRemaining < 0
|
||||||
|
? 'Expired!'
|
||||||
|
: `Expires in ${monitor.certExpiryDaysRemaining} days`}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center col-span-1 relative">
|
||||||
|
{monitor.avgPing !== undefined && (
|
||||||
|
<>
|
||||||
|
<span className={`hidden sm:inline px-2 py-0 rounded-full text-xs font-medium ${getAvgPingBg(monitor.avgPing)}`}
|
||||||
|
title={`Avg. response: ${monitor.avgPing} ms`}>
|
||||||
|
Avg: {monitor.avgPing} ms
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
ref={getBadgeRef(monitor.id, 'avgPing')}
|
||||||
|
className={`sm:hidden px-2 py-0 rounded-full text-xs font-medium flex items-center justify-center ${getAvgPingBg(monitor.avgPing)}`}
|
||||||
|
onClick={() => toggleBadge(monitor.id, 'avgPing')}
|
||||||
|
style={{ cursor: 'pointer', position: 'relative' }}
|
||||||
|
>
|
||||||
|
<FiActivity className="w-4 h-4" />
|
||||||
|
{openBadge[monitor.id]?.avgPing && (
|
||||||
|
<span className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 z-20 bg-white dark:bg-slate-800 text-gray-900 dark:text-gray-100 rounded-lg shadow-lg px-3 py-2 text-xs whitespace-nowrap border border-gray-200 dark:border-gray-700">
|
||||||
|
{monitor.avgPing} ms
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center col-span-1 relative">
|
||||||
|
{monitor.uptime24h !== undefined && (
|
||||||
|
<>
|
||||||
|
<span className={`hidden sm:inline px-2 py-0 rounded-full text-xs font-medium ${getUptime24hBg(monitor.uptime24h)}`}>
|
||||||
|
24h: {monitor.uptime24h.toFixed(1)}%
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
ref={getBadgeRef(monitor.id, 'uptime')}
|
||||||
|
className={`sm:hidden px-2 py-0 rounded-full text-xs font-medium flex items-center justify-center ${getUptime24hBg(monitor.uptime24h)}`}
|
||||||
|
onClick={() => toggleBadge(monitor.id, 'uptime')}
|
||||||
|
style={{ cursor: 'pointer', position: 'relative' }}
|
||||||
|
>
|
||||||
|
<FiPercent className="w-4 h-4" />
|
||||||
|
{openBadge[monitor.id]?.uptime && (
|
||||||
|
<span className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 z-20 bg-white dark:bg-slate-800 text-gray-900 dark:text-gray-100 rounded-lg shadow-lg px-3 py-2 text-xs whitespace-nowrap border border-gray-200 dark:border-gray-700">
|
||||||
|
{monitor.uptime24h.toFixed(1)}%
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
{monitor.heartbeatHistory && monitor.heartbeatHistory.length > 0 && (
|
{monitor.heartbeatHistory && monitor.heartbeatHistory.length > 0 && (
|
||||||
<div className="flex items-center w-full">
|
<div
|
||||||
<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">
|
className={`heartbeat-history-container ${isMobile ? 'mobile' : 'desktop'} bg-gray-200 dark:bg-slate-700 rounded px-2 py-1 w-full`}
|
||||||
{Array.from({ length: 40 }).map((_, i) => {
|
style={{
|
||||||
|
overflowX: isMobile ? 'auto' : 'visible',
|
||||||
|
WebkitOverflowScrolling: 'touch',
|
||||||
|
width: '100%',
|
||||||
|
maxWidth: '100vw',
|
||||||
|
minWidth: 0,
|
||||||
|
position: 'relative',
|
||||||
|
}}
|
||||||
|
ref={el => { if (el && isMobile) heartbeatContainerRefs.current[monitor.id] = el; }}
|
||||||
|
onScroll={e => handleHeartbeatScroll(monitor.id, e)}
|
||||||
|
>
|
||||||
|
{/* Left shadow overlay for mobile, only if scrolled */}
|
||||||
|
{isMobile && showLeftShadow && (
|
||||||
|
<div className="heartbeat-left-shadow" />
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className={isMobile ? 'grid-cols-10' : 'grid-cols-40'}
|
||||||
|
style={{
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: `repeat(${isMobile ? 10 : 40}, 1fr)`,
|
||||||
|
width: '100%',
|
||||||
|
minWidth: 0,
|
||||||
|
gap: '2px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(() => {
|
||||||
const hbArr = monitor.heartbeatHistory ?? [];
|
const hbArr = monitor.heartbeatHistory ?? [];
|
||||||
const hb = hbArr.slice(-40)[i];
|
const slice = isMobile ? hbArr.slice(-10) : hbArr.slice(-40);
|
||||||
return (
|
// Show most recent first (left), so reverse the slice
|
||||||
|
const ordered = slice;
|
||||||
|
return ordered.map((hb, i) => (
|
||||||
<Tippy
|
<Tippy
|
||||||
key={i}
|
key={i}
|
||||||
content={<HeartbeatPopup hb={hb} userZone={userZone} monitor={monitor} />}
|
content={<HeartbeatPopup hb={hb} userZone={userZone} monitor={monitor} />}
|
||||||
@@ -402,8 +504,8 @@ export default function UptimeStatusIsland() {
|
|||||||
style={{ cursor: 'pointer' }}
|
style={{ cursor: 'pointer' }}
|
||||||
></span>
|
></span>
|
||||||
</Tippy>
|
</Tippy>
|
||||||
);
|
));
|
||||||
})}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -421,6 +523,28 @@ export default function UptimeStatusIsland() {
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(40, minmax(0, 1fr));
|
grid-template-columns: repeat(40, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
.grid-cols-10 {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(10, 1fr);
|
||||||
|
}
|
||||||
|
.heartbeat-history-container.mobile {
|
||||||
|
overflow-x: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
max-width: 100vw;
|
||||||
|
}
|
||||||
|
.heartbeat-left-shadow {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 24px;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 10;
|
||||||
|
background: linear-gradient(to right, rgba(0,0,0,0.18) 0%, rgba(0,0,0,0.00) 100%);
|
||||||
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
Reference in New Issue
Block a user