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 [userLocale, setUserLocale] = useState(null);
|
||||
const [secondsToNextUpdate, setSecondsToNextUpdate] = useState(null);
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
const [showLeftShadow, setShowLeftShadow] = useState(false);
|
||||
const heartbeatContainerRefs = useRef({});
|
||||
|
||||
useEffect(() => {
|
||||
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 (
|
||||
<div>
|
||||
<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">
|
||||
{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">
|
||||
<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>
|
||||
{isMobile ? (
|
||||
<>
|
||||
<div className="flex items-center min-w-0 mb-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-row items-center justify-between w-full gap-3">
|
||||
{monitor.certExpiryDaysRemaining !== undefined && (
|
||||
<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)}`}
|
||||
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`}
|
||||
onClick={() => toggleBadge(monitor.id, 'cert')}
|
||||
style={{ cursor: 'pointer', position: 'relative' }}
|
||||
@@ -333,19 +345,11 @@ export default function UptimeStatusIsland() {
|
||||
</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>
|
||||
)}
|
||||
{monitor.avgPing !== undefined && (
|
||||
<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)}`}
|
||||
className={`px-2 py-1 rounded-full text-xs font-medium flex items-center justify-center ${getAvgPingBg(monitor.avgPing)}`}
|
||||
onClick={() => toggleBadge(monitor.id, 'avgPing')}
|
||||
style={{ cursor: 'pointer', position: 'relative' }}
|
||||
>
|
||||
@@ -356,18 +360,11 @@ export default function UptimeStatusIsland() {
|
||||
</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>
|
||||
)}
|
||||
{monitor.uptime24h !== undefined && (
|
||||
<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)}`}
|
||||
className={`px-2 py-1 rounded-full text-xs font-medium flex items-center justify-center ${getUptime24hBg(monitor.uptime24h)}`}
|
||||
onClick={() => toggleBadge(monitor.id, 'uptime')}
|
||||
style={{ cursor: 'pointer', position: 'relative' }}
|
||||
>
|
||||
@@ -378,17 +375,122 @@ export default function UptimeStatusIsland() {
|
||||
</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>
|
||||
)}
|
||||
{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) => {
|
||||
<div
|
||||
className={`heartbeat-history-container ${isMobile ? 'mobile' : 'desktop'} bg-gray-200 dark:bg-slate-700 rounded px-2 py-1 w-full`}
|
||||
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 hb = hbArr.slice(-40)[i];
|
||||
return (
|
||||
const slice = isMobile ? hbArr.slice(-10) : hbArr.slice(-40);
|
||||
// Show most recent first (left), so reverse the slice
|
||||
const ordered = slice;
|
||||
return ordered.map((hb, i) => (
|
||||
<Tippy
|
||||
key={i}
|
||||
content={<HeartbeatPopup hb={hb} userZone={userZone} monitor={monitor} />}
|
||||
@@ -402,8 +504,8 @@ export default function UptimeStatusIsland() {
|
||||
style={{ cursor: 'pointer' }}
|
||||
></span>
|
||||
</Tippy>
|
||||
);
|
||||
})}
|
||||
));
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -421,6 +523,28 @@ export default function UptimeStatusIsland() {
|
||||
display: grid;
|
||||
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>
|
||||
</div>
|
||||
);
|
||||
|
Reference in New Issue
Block a user