Files
cfw-autumn/apps/website/components/preloader.tsx
amianthus d8a4979fe4 feat: migrate landing page to TypeScript and fix auth cookies
- Migrate all apps/website components from JS/JSX to TypeScript/TSX
- Add blog pages (layout, listing, slug) in TypeScript
- Add tsconfig.json and tailwind.config.mjs to website
- Fix cookie domain handling in vite/App.tsx (set .useautumn.com in prod)
- Update auth.ts, afterSessionCreated, afterSessionDeleted

Made-with: Cursor
2026-04-16 17:52:14 +01:00

245 lines
6.1 KiB
TypeScript

"use client";
import gsap from "gsap";
import { useEffect, useRef, useState } from "react";
import {
PRELOADER_LOGO_PATH,
PRELOADER_LOGO_VIEWBOX,
PreloaderLogo,
} from "@/app/constant";
export default function Preloader() {
const wrapperRef = useRef<HTMLDivElement | null>(null);
const logoWrapRef = useRef<HTMLDivElement | null>(null);
const gridWrapRef = useRef<HTMLDivElement | null>(null);
const blackCoverRef = useRef<HTMLDivElement | null>(null);
const fallbackRef = useRef<HTMLDivElement | null>(null);
const [done, setDone] = useState(false);
if (typeof window !== "undefined") {
window.history.scrollRestoration = "manual";
window.scrollTo(0, 0);
document.documentElement.scrollTop = 0;
document.body.scrollTop = 0;
}
useEffect(() => {
window.scrollTo(0, 0);
document.documentElement.scrollTop = 0;
document.body.scrollTop = 0;
if (fallbackRef.current) {
fallbackRef.current.style.opacity = "0";
}
const preventScroll = (e: Event) => {
e.preventDefault();
e.stopPropagation();
};
window.addEventListener("wheel", preventScroll, {
passive: false,
capture: true,
});
window.addEventListener("touchmove", preventScroll, {
passive: false,
capture: true,
});
const wrapper = wrapperRef.current;
const logoWrap = logoWrapRef.current;
const gridWrap = gridWrapRef.current;
const blackCover = blackCoverRef.current;
if (!wrapper || !logoWrap || !gridWrap || !blackCover) return;
gsap.set(gridWrap, {
clipPath: "inset(0 100% 0 0)",
willChange: "clip-path",
force3D: true,
});
gsap.set(blackCover, { opacity: 0, willChange: "opacity" });
const logoSvg = logoWrap.querySelector<SVGSVGElement>("svg");
if (!logoSvg) return;
gsap.set(logoSvg, { opacity: 0 });
const canvas = document.createElement("canvas");
Object.assign(canvas.style, {
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
pointerEvents: "none",
});
wrapper.appendChild(canvas);
const cleanup = () => {
document.documentElement.style.overflow = "";
document.body.style.overflow = "";
document.body.style.paddingRight = "";
window.removeEventListener("wheel", preventScroll, { capture: true });
window.removeEventListener("touchmove", preventScroll, {
capture: true,
});
if (canvas.parentNode) canvas.parentNode.removeChild(canvas);
};
const runExitSequence = () => {
const tl = gsap.timeline({ defaults: { overwrite: "auto" } });
tl.to(canvas, { opacity: 0, duration: 0.35, ease: "power2.out" })
.to(
gridWrap,
{
clipPath: "inset(0 0% 0 0)",
duration: 0.35,
ease: "power2.out",
force3D: true,
},
"<",
)
.to(blackCover, {
opacity: 1,
duration: 0.45,
ease: "power2.inOut",
})
.to(wrapper, {
opacity: 0,
duration: 0.45,
ease: "power2.inOut",
onStart: () => {
window.dispatchEvent(new CustomEvent("preloader:complete"));
},
onComplete: () => {
cleanup();
setDone(true);
},
});
};
gsap.to(gridWrap, {
clipPath: "inset(0 12% 0 0)",
duration: 0.8,
ease: "power2.in",
force3D: true,
});
setTimeout(() => {
const W = PRELOADER_LOGO_VIEWBOX.width;
const H = PRELOADER_LOGO_VIEWBOX.height;
const offscreen = document.createElement("canvas");
offscreen.width = W;
offscreen.height = H;
const octx = offscreen.getContext("2d");
if (!octx) return;
const path2d = new Path2D(PRELOADER_LOGO_PATH);
octx.fillStyle = "white";
octx.fill(path2d);
const { data } = octx.getImageData(0, 0, W, H);
const dpr = window.devicePixelRatio || 1;
const displayW =
logoWrap.getBoundingClientRect().width ||
logoSvg.getBoundingClientRect().width ||
54;
const displayH = Math.round((displayW / W) * H);
const isMobile = displayW < 70;
const STEP = isMobile ? 6 : 4;
const DOT_R = isMobile
? Math.max(0.6, (displayW / W) * STEP * 0.3)
: Math.max(1, (displayW / W) * STEP * 0.45);
const dots: Array<{ nx: number; ny: number }> = [];
for (let y = 0; y < H; y += STEP) {
for (let x = 0; x < W; x += STEP) {
const i = (y * W + x) * 4;
if (data[i + 3] > 40) {
dots.push({ nx: x / W, ny: y / H });
}
}
}
for (let i = dots.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[dots[i], dots[j]] = [dots[j], dots[i]];
}
canvas.width = displayW * dpr;
canvas.height = displayH * dpr;
canvas.style.width = `${displayW}px`;
canvas.style.height = `${displayH}px`;
const ctx = canvas.getContext("2d");
if (!ctx) return;
ctx.scale(dpr, dpr);
let painted = 0;
const total = dots.length;
const progress = { value: 0 };
gsap.to(progress, {
value: 1,
duration: 1.05,
ease: "power1.inOut",
onUpdate() {
const target = Math.floor(progress.value * total);
while (painted < target) {
const d = dots[painted];
ctx.beginPath();
ctx.arc(d.nx * displayW, d.ny * displayH, DOT_R, 0, Math.PI * 2);
ctx.fillStyle = "#9564FF";
ctx.fill();
painted++;
}
},
onComplete() {
gsap.delayedCall(0.2, runExitSequence);
},
});
}, 120);
return () => {
cleanup();
};
}, []);
if (done) return null;
return (
<div
ref={wrapperRef}
className="fixed inset-0 z-[9999] bg-black"
aria-hidden="true"
>
<div
ref={fallbackRef}
className="absolute inset-0 flex items-center justify-center z-10 pointer-events-none transition-opacity duration-300"
>
<PreloaderLogo className="w-[53.57px] md:w-[97.94px] h-auto opacity-20" />
</div>
<div className="absolute inset-0 flex items-center justify-center">
<div ref={logoWrapRef}>
<PreloaderLogo className="w-[53.57px] md:w-[97.94px] h-auto" />
</div>
</div>
<div ref={gridWrapRef} className="absolute bottom-0 left-0 w-full">
<img
src="/images/preloader/grid.png"
width={1440}
height={217}
alt=""
className="w-full h-auto"
/>
</div>
<div
ref={blackCoverRef}
className="absolute inset-0 bg-black pointer-events-none"
/>
</div>
);
}