feat(frontend): migrate animations to GSAP and add useGSAP lifecycle hooks
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -10,10 +10,11 @@
|
|||||||
"format": "biome format --write"
|
"format": "biome format --write"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@gsap/react": "^2.1.2",
|
||||||
"@orpc/client": "1.15.0",
|
"@orpc/client": "1.15.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"gsap": "^3.15.0",
|
||||||
"lucide-react": "^1.27.0",
|
"lucide-react": "^1.27.0",
|
||||||
"motion": "^12.0.0",
|
|
||||||
"next": "16.2.12",
|
"next": "16.2.12",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"partysocket": "1.3.0",
|
"partysocket": "1.3.0",
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
|
export { ErrorBoundary } from "./error-boundary";
|
||||||
export { GuildChannelPicker } from "./guild-picker";
|
export { GuildChannelPicker } from "./guild-picker";
|
||||||
export { MarkdownLite } from "./markdown";
|
export { MarkdownLite } from "./markdown";
|
||||||
export { PageTransition } from "./page-transition";
|
export { PageTransition } from "./page-transition";
|
||||||
export { MetricTile, SectionHeader } from "./section";
|
export { MetricTile, SectionHeader } from "./section";
|
||||||
export { ErrorBoundary } from "./error-boundary";
|
|
||||||
export {
|
export {
|
||||||
EmptyState,
|
EmptyState,
|
||||||
ErrorState,
|
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";
|
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.
|
* GSAP-powered page transition wrapper.
|
||||||
* The animation is disabled under prefers-reduced-motion via globals.css.
|
* Staggers entrance of direct children or smooth rise/fade for the container.
|
||||||
|
* Fully cleans up on unmount and respects reduced-motion.
|
||||||
*/
|
*/
|
||||||
export function PageTransition({
|
export function PageTransition({
|
||||||
children,
|
children,
|
||||||
@@ -11,5 +22,40 @@ export function PageTransition({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
className?: string;
|
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] },
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user