mirror of
https://github.com/gnmyt/myspeed.git
synced 2026-01-22 05:04:38 -06:00
refactor: enhance TestAreaComponent with infinite scrolling, sticky date indicator, and improved loading state handling
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import {useContext} from "react";
|
||||
import {useContext, useEffect, useRef, useState, useCallback} from "react";
|
||||
import {createPortal} from "react-dom";
|
||||
import {ConfigContext} from "@/common/contexts/Config";
|
||||
import {SpeedtestContext} from "@/common/contexts/Speedtests";
|
||||
import Speedtest from "../Speedtest";
|
||||
@@ -6,41 +7,162 @@ import {getIconBySpeed} from "@/common/utils/TestUtil";
|
||||
import "./styles.sass";
|
||||
import {t} from "i18next";
|
||||
|
||||
function TestArea() {
|
||||
const TestArea = () => {
|
||||
const config = useContext(ConfigContext)[0];
|
||||
const [speedtests] = useContext(SpeedtestContext);
|
||||
const {speedtests, loadMoreTests, loading, hasMore} = useContext(SpeedtestContext);
|
||||
const [stickyDate, setStickyDate] = useState(null);
|
||||
const [showStickyDate, setShowStickyDate] = useState(false);
|
||||
const [initialLoadComplete, setInitialLoadComplete] = useState(false);
|
||||
const containerRef = useRef();
|
||||
const lastElementRef = useRef();
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && !initialLoadComplete) {
|
||||
setInitialLoadComplete(true);
|
||||
}
|
||||
}, [loading, initialLoadComplete]);
|
||||
|
||||
useEffect(() => {
|
||||
if (speedtests.length > 0) {
|
||||
const initialDate = getDateFromTest(speedtests[0]);
|
||||
setStickyDate(initialDate);
|
||||
}
|
||||
}, [speedtests]);
|
||||
|
||||
const getDateFromTest = (test) => {
|
||||
const date = new Date(Date.parse(test.created));
|
||||
return date.toLocaleDateString("default", {weekday: 'long', year: 'numeric', month: 'long', day: 'numeric'});
|
||||
};
|
||||
|
||||
const handleScroll = useCallback(() => {
|
||||
const scrollTop = Math.max(window.scrollY, document.documentElement.scrollTop, document.body.scrollTop);
|
||||
|
||||
const shouldShow = scrollTop > 50;
|
||||
setShowStickyDate(shouldShow);
|
||||
|
||||
const windowHeight = window.innerHeight;
|
||||
const documentHeight = Math.max(document.body.scrollHeight, document.body.offsetHeight,
|
||||
document.documentElement.clientHeight, document.documentElement.scrollHeight,
|
||||
document.documentElement.offsetHeight);
|
||||
|
||||
const nearBottom = scrollTop + windowHeight >= documentHeight - 500;
|
||||
const atBottom = scrollTop + windowHeight >= documentHeight - 50;
|
||||
|
||||
if ((nearBottom || atBottom) && hasMore && !loading && speedtests.length > 0) {
|
||||
loadMoreTests();
|
||||
}
|
||||
|
||||
if (shouldShow && speedtests.length > 0) {
|
||||
const testElements = document.querySelectorAll('.speedtest');
|
||||
if (testElements.length > 0) {
|
||||
for (let i = 0; i < testElements.length; i++) {
|
||||
const element = testElements[i];
|
||||
const elementRect = element.getBoundingClientRect();
|
||||
|
||||
if (elementRect.top <= 200 && elementRect.bottom > 50) {
|
||||
if (speedtests[i]) {
|
||||
const newDate = getDateFromTest(speedtests[i]);
|
||||
setStickyDate(newDate);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [speedtests, stickyDate, hasMore, loading, loadMoreTests]);
|
||||
|
||||
useEffect(() => {
|
||||
let ticking = false;
|
||||
|
||||
const throttledScrollHandler = () => {
|
||||
if (!ticking) {
|
||||
requestAnimationFrame(() => {
|
||||
handleScroll();
|
||||
ticking = false;
|
||||
});
|
||||
ticking = true;
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', throttledScrollHandler, {passive: true});
|
||||
document.body.addEventListener('scroll', throttledScrollHandler, {passive: true});
|
||||
|
||||
setTimeout(handleScroll, 100);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('scroll', throttledScrollHandler);
|
||||
document.body.removeEventListener('scroll', throttledScrollHandler);
|
||||
};
|
||||
}, [handleScroll]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!lastElementRef.current || !hasMore || loading) return;
|
||||
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
if (entries[0].isIntersecting && hasMore && !loading) loadMoreTests();
|
||||
}, {threshold: 0.01, rootMargin: '200px 0px'});
|
||||
|
||||
observer.observe(lastElementRef.current);
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [hasMore, loading, loadMoreTests, speedtests.length]);
|
||||
|
||||
if (Object.entries(config).length === 0) return (<></>);
|
||||
|
||||
if (speedtests.length === 0) return <h2 className="error-text">{t("test.not_available")}</h2>
|
||||
if (!initialLoadComplete || (speedtests.length === 0 && loading)) return <></>;
|
||||
|
||||
if (speedtests.length === 0 && initialLoadComplete)
|
||||
return <h2 className="error-text">{t("test.not_available")}</h2>;
|
||||
|
||||
return (
|
||||
<div className="speedtest-area">
|
||||
{speedtests.map ? speedtests.map(test => {
|
||||
const dateArray = test.created.split("-").map((value) => parseInt(value));
|
||||
const date = test.type === "average" ? new Date(dateArray[0], dateArray[1] - 1, dateArray[2])
|
||||
: new Date(Date.parse(test.created));
|
||||
<>
|
||||
{showStickyDate && stickyDate && createPortal(
|
||||
<div className="floating-date-indicator">
|
||||
<span>{stickyDate}</span>
|
||||
</div>, document.body)}
|
||||
|
||||
let item = localStorage.getItem("testTime");
|
||||
if ((item === "3" || item === "4") && test.type !== "average") return;
|
||||
<div className="speedtest-area" ref={containerRef}>
|
||||
{speedtests.map((test, index) => {
|
||||
const date = new Date(Date.parse(test.created));
|
||||
const isLast = index === speedtests.length - 1;
|
||||
|
||||
let id = (test.type === "average") ? date.getDate() + "-" + date.getMonth() : test.id;
|
||||
return (
|
||||
<div key={test.id} ref={isLast ? lastElementRef : null}>
|
||||
<Speedtest
|
||||
time={date}
|
||||
ping={test.ping}
|
||||
pingLevel={getIconBySpeed(test.ping, config.ping, false)}
|
||||
down={test.download}
|
||||
downLevel={getIconBySpeed(test.download, config.download, true)}
|
||||
up={test.upload}
|
||||
upLevel={getIconBySpeed(test.upload, config.upload, true)}
|
||||
error={test.error}
|
||||
url={test.url}
|
||||
type={test.type}
|
||||
duration={test.time}
|
||||
amount={test.amount}
|
||||
resultId={test.resultId}
|
||||
id={test.id}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
return <Speedtest time={date}
|
||||
ping={test.ping} pingLevel={getIconBySpeed(test.ping, config.ping, false)}
|
||||
down={test.download} downLevel={getIconBySpeed(test.download, config.download, true)}
|
||||
up={test.upload} upLevel={getIconBySpeed(test.upload, config.upload, true)}
|
||||
error={test.error}
|
||||
key={id}
|
||||
url={test.url}
|
||||
type={test.type}
|
||||
duration={test.time}
|
||||
amount={test.amount}
|
||||
resultId={test.resultId}
|
||||
id={id}
|
||||
/>
|
||||
}) : ""}
|
||||
</div>
|
||||
{loading && (
|
||||
<div className="loading-more">
|
||||
<p>{t("test.loading_more")}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!hasMore && speedtests.length > 0 && (
|
||||
<div className="end-of-list">
|
||||
<p>{t("test.no_more_tests")}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,42 @@
|
||||
@use "@/common/styles/colors" as *
|
||||
|
||||
.floating-date-indicator
|
||||
position: fixed
|
||||
user-select: none
|
||||
top: 30px
|
||||
display: flex
|
||||
transform: translateX(-50%)
|
||||
left: 50%
|
||||
z-index: 50
|
||||
background: $dark-gray
|
||||
backdrop-filter: blur(10px)
|
||||
border: $light-gray 2px solid
|
||||
border-radius: 12px
|
||||
padding: 12px 24px
|
||||
text-align: center
|
||||
font-weight: 600
|
||||
color: $white
|
||||
box-shadow: 0 0 15px rgba($light-gray, 0.5)
|
||||
animation: slideInDown 0.3s ease-out
|
||||
|
||||
span
|
||||
font-size: 22px
|
||||
letter-spacing: 0.5px
|
||||
|
||||
@keyframes slideInDown
|
||||
from
|
||||
opacity: 0
|
||||
transform: translateX(-50%) translateY(-20px)
|
||||
to
|
||||
opacity: 1
|
||||
transform: translateX(-50%) translateY(0)
|
||||
|
||||
.loading-more, .end-of-list
|
||||
text-align: center
|
||||
padding: 20px
|
||||
color: $subtext
|
||||
font-style: italic
|
||||
|
||||
.error-text
|
||||
margin: 0
|
||||
font-size: 26pt
|
||||
@@ -10,6 +47,13 @@
|
||||
@media (max-width: 605px)
|
||||
.error-text
|
||||
width: 28rem
|
||||
|
||||
.floating-date-indicator
|
||||
top: 15px
|
||||
padding: 10px 20px
|
||||
|
||||
span
|
||||
font-size: 13px
|
||||
|
||||
@media (max-width: 475px)
|
||||
.error-text
|
||||
|
||||
Reference in New Issue
Block a user