feat(frontend): migrate animations to GSAP and update client socket types for Next 16.3

This commit is contained in:
asepharyana
2026-08-25 22:47:25 +07:00
7 changed files with 621 additions and 8 deletions
File diff suppressed because one or more lines are too long
+3 -2
View File
@@ -10,10 +10,11 @@
"format": "biome format --write"
},
"dependencies": {
"@gsap/react": "^2.1.2",
"@orpc/client": "1.15.0",
"clsx": "^2.1.1",
"gsap": "^3.15.0",
"lucide-react": "^1.33.0",
"motion": "^13.1.1",
"next": "16.3.2",
"next-themes": "^0.4.6",
"partysocket": "1.3.0",
@@ -35,4 +36,4 @@
"tailwindcss": "^4",
"typescript": "^7"
}
}
}
@@ -1,8 +1,8 @@
export { ErrorBoundary } from "./error-boundary";
export { GuildChannelPicker } from "./guild-picker";
export { MarkdownLite } from "./markdown";
export { PageTransition } from "./page-transition";
export { MetricTile, SectionHeader } from "./section";
export { ErrorBoundary } from "./error-boundary";
export {
EmptyState,
ErrorState,
@@ -1,8 +1,19 @@
"use client";
import { useGSAP } from "@gsap/react";
import gsap from "gsap";
import { useRef } from "react";
import { cn } from "@/lib/utils";
// Register plugin once on client
if (typeof window !== "undefined") {
gsap.registerPlugin(useGSAP);
}
/**
* Wraps a view's root so the whole panel stack rises + settles on mount.
* The animation is disabled under prefers-reduced-motion via globals.css.
* GSAP-powered page transition wrapper.
* Staggers entrance of direct children or smooth rise/fade for the container.
* Fully cleans up on unmount and respects reduced-motion.
*/
export function PageTransition({
children,
@@ -11,5 +22,40 @@ export function PageTransition({
children: React.ReactNode;
className?: string;
}) {
return <div className={cn("animate-fade-up", className)}>{children}</div>;
const containerRef = useRef<HTMLDivElement>(null);
useGSAP(
() => {
if (!containerRef.current) return;
const prefersReduced = window.matchMedia(
"(prefers-reduced-motion: reduce)",
).matches;
if (prefersReduced) return;
gsap.fromTo(
containerRef.current,
{
opacity: 0,
y: 14,
filter: "blur(4px)",
},
{
opacity: 1,
y: 0,
filter: "blur(0px)",
duration: 0.45,
ease: "power2.out",
clearProps: "filter,transform",
},
);
},
{ scope: containerRef },
);
return (
<div ref={containerRef} className={cn("w-full opacity-0", className)}>
{children}
</div>
);
}
@@ -0,0 +1,96 @@
"use client";
import { useGSAP } from "@gsap/react";
import gsap from "gsap";
import { type RefObject, useRef } from "react";
if (typeof window !== "undefined") {
gsap.registerPlugin(useGSAP);
}
/**
* Reusable hook for staggered reveal animations on child elements.
*/
export function useStaggerReveal<T extends HTMLElement = HTMLDivElement>(
selector: string,
options?: {
stagger?: number;
duration?: number;
delay?: number;
y?: number;
dependencies?: unknown[];
},
) {
const containerRef = useRef<T>(null);
useGSAP(
() => {
if (!containerRef.current) return;
const elements = containerRef.current.querySelectorAll(selector);
if (!elements || elements.length === 0) return;
const prefersReduced = window.matchMedia(
"(prefers-reduced-motion: reduce)",
).matches;
if (prefersReduced) {
gsap.set(elements, { opacity: 1, y: 0 });
return;
}
gsap.fromTo(
elements,
{
opacity: 0,
y: options?.y ?? 12,
scale: 0.98,
},
{
opacity: 1,
y: 0,
scale: 1,
duration: options?.duration ?? 0.35,
delay: options?.delay ?? 0.05,
stagger: options?.stagger ?? 0.04,
ease: "power2.out",
clearProps: "transform",
},
);
},
{
scope: containerRef,
dependencies: options?.dependencies ?? [],
revertOnUpdate: true,
},
);
return containerRef;
}
/**
* Animated number counter using GSAP.
*/
export function useCounter(
targetValue: number,
ref: RefObject<HTMLElement | null>,
formatter?: (val: number) => string,
) {
useGSAP(
() => {
if (!ref.current) return;
const obj = { val: 0 };
gsap.to(obj, {
val: targetValue,
duration: 0.8,
ease: "power2.out",
onUpdate: () => {
if (ref.current) {
ref.current.textContent = formatter
? formatter(obj.val)
: Math.round(obj.val).toLocaleString();
}
},
});
},
{ dependencies: [targetValue] },
);
}
+6 -1
View File
@@ -29,7 +29,12 @@ const orpc: ORPCClient = (() => {
basePath: "trpc",
});
const link = new RPCLink({ websocket: socket });
const link = new RPCLink({
websocket: socket as unknown as Pick<
WebSocket,
"addEventListener" | "readyState" | "removeEventListener" | "send"
>,
});
return createORPCClient(link) as unknown as ORPCClient;
})();
+1 -1
View File
@@ -126,7 +126,7 @@ export class WsConnection {
sendBinary(data: ArrayBufferLike): void {
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(data);
this.ws.send(data as ArrayBuffer);
}
}