rework/lightweight #12
80
app/components/SmoothScroll.tsx
Normal file
80
app/components/SmoothScroll.tsx
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import type { JSX } from "solid-js";
|
||||||
|
import { createSignal, onCleanup } from "solid-js";
|
||||||
|
|
||||||
|
type SmoothScrollProps = JSX.IntrinsicElements["div"] & {
|
||||||
|
children: JSX.Element;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function SmoothScroll(props: SmoothScrollProps) {
|
||||||
|
const [isScrolling, setIsScrolling] = createSignal(false);
|
||||||
|
let animationFrameId: number | null = null;
|
||||||
|
|
||||||
|
const easeOutQuad = (t: number, b: number, c: number, d: number) => {
|
||||||
|
const time = t / d;
|
||||||
|
return -c * time * (time - 2) + b;
|
||||||
|
};
|
||||||
|
|
||||||
|
const smoothScroll = (deltaY: number) => {
|
||||||
|
const scrollSpeed = 3;
|
||||||
|
const currentScroll = window.scrollY;
|
||||||
|
const targetScroll = deltaY * scrollSpeed;
|
||||||
|
const duration = 300;
|
||||||
|
const startTime = performance.now();
|
||||||
|
|
||||||
|
const animateScroll = (currentTime: number) => {
|
||||||
|
const elapsedTime = currentTime - startTime;
|
||||||
|
const ease = easeOutQuad(
|
||||||
|
elapsedTime,
|
||||||
|
currentScroll,
|
||||||
|
targetScroll,
|
||||||
|
duration,
|
||||||
|
);
|
||||||
|
|
||||||
|
window.scrollTo(0, ease);
|
||||||
|
|
||||||
|
if (elapsedTime < duration) {
|
||||||
|
animationFrameId = requestAnimationFrame(animateScroll);
|
||||||
|
} else {
|
||||||
|
setIsScrolling(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
animationFrameId = requestAnimationFrame(animateScroll);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isMobile = () => {
|
||||||
|
const regex =
|
||||||
|
/Mobi|Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i;
|
||||||
|
return regex.test(navigator.userAgent);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleWheel = (event: WheelEvent) => {
|
||||||
|
if (isMobile()) return;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
if (isScrolling()) {
|
||||||
|
if (animationFrameId !== null) {
|
||||||
|
cancelAnimationFrame(animationFrameId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
smoothScroll(event.deltaY);
|
||||||
|
setIsScrolling(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsScrolling(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
if (animationFrameId !== null) {
|
||||||
|
cancelAnimationFrame(animationFrameId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div {...props} onWheel={handleWheel} style={{ "touch-action": "auto" }}>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -68,7 +68,7 @@ export function Snippet(props: SnippetProps) {
|
|||||||
<div class="pt-4 pl-4">
|
<div class="pt-4 pl-4">
|
||||||
<TrafficLightsIcon class="h-2.5 w-auto stroke-slate-500/30" />
|
<TrafficLightsIcon class="h-2.5 w-auto stroke-slate-500/30" />
|
||||||
|
|
||||||
<div class="mt-4 flex space-x-2 text-xs">
|
<div class="mt-4 flex space-x-2 text-xs overflow-x-auto">
|
||||||
<For each={props.snippets}>
|
<For each={props.snippets}>
|
||||||
{(tab) => (
|
{(tab) => (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import type { JSXElement } from "solid-js";
|
import type { JSXElement } from "solid-js";
|
||||||
|
|
||||||
// import { TableOfContents } from "@/partials/TableOfContents";
|
|
||||||
import { PrevNextLinks } from "@/components/PrevNextLinks";
|
import { PrevNextLinks } from "@/components/PrevNextLinks";
|
||||||
import { usePageContext } from "vike-solid/usePageContext";
|
import { usePageContext } from "vike-solid/usePageContext";
|
||||||
import { clientOnly } from "vike-solid/clientOnly";
|
import { clientOnly } from "vike-solid/clientOnly";
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import type { JSXElement } from "solid-js";
|
import type { JSXElement } from "solid-js";
|
||||||
|
|
||||||
import { usePageContext } from "vike-solid/usePageContext";
|
import { usePageContext } from "vike-solid/usePageContext";
|
||||||
|
import { SmoothScroll } from "@/components/SmoothScroll";
|
||||||
import { HeroSection } from "@/partials/HeroSection";
|
import { HeroSection } from "@/partials/HeroSection";
|
||||||
import { Navigation } from "@/partials/Navigation";
|
import { Navigation } from "@/partials/Navigation";
|
||||||
import { clientOnly } from "vike-solid/clientOnly";
|
import { clientOnly } from "vike-solid/clientOnly";
|
||||||
@ -24,7 +25,7 @@ export default function DefaultLayout(props: DefaultLayoutProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div class="flex w-full flex-col font-sans">
|
<SmoothScroll class="flex w-full flex-col font-sans">
|
||||||
<Header />
|
<Header />
|
||||||
<ReadProgressBar />
|
<ReadProgressBar />
|
||||||
|
|
||||||
@ -43,7 +44,7 @@ export default function DefaultLayout(props: DefaultLayoutProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Footer />
|
<Footer />
|
||||||
</div>
|
</SmoothScroll>
|
||||||
|
|
||||||
<Toaster />
|
<Toaster />
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -43,7 +43,7 @@ export function ReadProgressBar() {
|
|||||||
class="sticky inset-x-0 top-20 z-50 h-1 w-full bg-violet-50"
|
class="sticky inset-x-0 top-20 z-50 h-1 w-full bg-violet-50"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="bg-violet-300 h-full transition-all duration-75"
|
class="bg-violet-300 h-full transition-all duration-300 ease-in-out"
|
||||||
style={{ width: width() }}
|
style={{ width: width() }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user