Skip to content

Commit e9103f8

Browse files
authored
nce web dashboard with new hooks and layout improvements (#320)
* nce web dashboard with new hooks and layout improvements * fix(Hyperspeed): improve resume and tick methods to handle disposed and paused states correctly * style: unify nav-height variable across Pricing and LandingPage stylesheets
1 parent 9ccebb3 commit e9103f8

10 files changed

Lines changed: 730 additions & 272 deletions

File tree

apps/web-dashboard/src/App.jsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Routes, Route } from 'react-router-dom';
22
import { Toaster } from 'react-hot-toast';
3+
import ScrollToTop from './components/Layout/ScrollToTop';
34
import Login from './pages/Login';
45
import Signup from './pages/Signup';
56
import MainLayout from './components/Layout/MainLayout';
@@ -51,6 +52,8 @@ function AppContent() {
5152
}}
5253
/>
5354

55+
<ScrollToTop />
56+
5457
<Routes>
5558
<Route path="/" element={<LandingPage />} />
5659
<Route path="/pricing" element={<Pricing />} />

apps/web-dashboard/src/components/Hyperspeed/Hyperspeed.jsx

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,7 @@ const Hyperspeed = ({ effectOptions = DEFAULT_EFFECT_OPTIONS }) => {
361361
alpha: true
362362
});
363363
this.renderer.setSize(initW, initH, false);
364-
this.renderer.setPixelRatio(window.devicePixelRatio);
364+
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5));
365365
this.composer = new EffectComposer(this.renderer);
366366
container.append(this.renderer.domElement);
367367

@@ -382,6 +382,7 @@ const Hyperspeed = ({ effectOptions = DEFAULT_EFFECT_OPTIONS }) => {
382382
this.clock = new THREE.Clock();
383383
this.assets = {};
384384
this.disposed = false;
385+
this.paused = false;
385386

386387
this.road = new Road(this, options);
387388
this.leftCarLights = new CarLights(
@@ -584,8 +585,20 @@ const Hyperspeed = ({ effectOptions = DEFAULT_EFFECT_OPTIONS }) => {
584585
this.composer.render(delta);
585586
}
586587

588+
pause() {
589+
this.paused = true;
590+
}
591+
592+
resume() {
593+
if (this.disposed || !this.paused) return;
594+
this.paused = false;
595+
this.clock.getDelta();
596+
requestAnimationFrame(this.tick);
597+
}
598+
587599
dispose() {
588600
this.disposed = true;
601+
this.paused = true;
589602

590603
if (this.scene) {
591604
this.scene.traverse(object => {
@@ -639,7 +652,7 @@ const Hyperspeed = ({ effectOptions = DEFAULT_EFFECT_OPTIONS }) => {
639652
}
640653

641654
tick() {
642-
if (this.disposed) return;
655+
if (this.disposed || this.paused) return;
643656

644657
if (!this.hasValidSize) {
645658
const w = this.container.offsetWidth;
@@ -1183,6 +1196,39 @@ const Hyperspeed = ({ effectOptions = DEFAULT_EFFECT_OPTIONS }) => {
11831196
};
11841197
}, [effectOptions]);
11851198

1199+
useEffect(() => {
1200+
const el = hyperspeed.current;
1201+
if (!el) return;
1202+
1203+
const syncPauseState = (shouldPause) => {
1204+
if (!appRef.current) return;
1205+
if (shouldPause) appRef.current.pause();
1206+
else appRef.current.resume();
1207+
};
1208+
1209+
const observer = new IntersectionObserver(
1210+
([entry]) => syncPauseState(!entry.isIntersecting),
1211+
{ rootMargin: '80px', threshold: 0 }
1212+
);
1213+
observer.observe(el);
1214+
1215+
const onVisibilityChange = () => {
1216+
if (document.hidden) {
1217+
syncPauseState(true);
1218+
return;
1219+
}
1220+
const rect = el.getBoundingClientRect();
1221+
const inView = rect.bottom > 0 && rect.top < window.innerHeight;
1222+
syncPauseState(!inView);
1223+
};
1224+
document.addEventListener('visibilitychange', onVisibilityChange);
1225+
1226+
return () => {
1227+
observer.disconnect();
1228+
document.removeEventListener('visibilitychange', onVisibilityChange);
1229+
};
1230+
}, []);
1231+
11861232
return <div id="lights" ref={hyperspeed}></div>;
11871233
};
11881234

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { useEffect } from 'react';
2+
import { useLocation } from 'react-router-dom';
3+
4+
export default function ScrollToTop() {
5+
const { pathname, hash } = useLocation();
6+
7+
useEffect(() => {
8+
if (hash) return;
9+
window.scrollTo(0, 0);
10+
}, [pathname, hash]);
11+
12+
return null;
13+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { useEffect, useRef, useState } from 'react';
2+
3+
const DEFAULT_ROOT_MARGIN = '200px 0px';
4+
const DEFAULT_THRESHOLD = 0;
5+
6+
export function useInView({
7+
rootMargin = DEFAULT_ROOT_MARGIN,
8+
threshold = DEFAULT_THRESHOLD,
9+
} = {}) {
10+
const ref = useRef(null);
11+
const [inView, setInView] = useState(false);
12+
13+
useEffect(() => {
14+
const node = ref.current;
15+
if (!node) return;
16+
17+
const observer = new IntersectionObserver(([entry]) => {
18+
if (entry.isIntersecting) {
19+
setInView(true);
20+
observer.disconnect();
21+
}
22+
}, { rootMargin, threshold });
23+
24+
observer.observe(node);
25+
return () => observer.disconnect();
26+
}, [rootMargin, threshold]);
27+
28+
return [ref, inView];
29+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { useEffect, useState } from 'react';
2+
3+
export function usePrefersReducedMotion() {
4+
const [prefersReducedMotion, setPrefersReducedMotion] = useState(() => {
5+
if (typeof window === 'undefined') return false;
6+
return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
7+
});
8+
9+
useEffect(() => {
10+
const media = window.matchMedia('(prefers-reduced-motion: reduce)');
11+
const onChange = (event) => setPrefersReducedMotion(event.matches);
12+
media.addEventListener('change', onChange);
13+
return () => media.removeEventListener('change', onChange);
14+
}, []);
15+
16+
return prefersReducedMotion;
17+
}

0 commit comments

Comments
 (0)