Skip to content

Commit

Permalink
feat(login): add login page animation in the background (#36)
Browse files Browse the repository at this point in the history
* feat(ui): add animated grid component

* feat(login): add login page animation in the background
  • Loading branch information
DikDns authored Jul 22, 2024
1 parent 837e331 commit 231c522
Show file tree
Hide file tree
Showing 3 changed files with 210 additions and 2 deletions.
6 changes: 4 additions & 2 deletions src/app/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { redirect } from "next/navigation";
import { AlertCircle } from "lucide-react";

import { GoogleLoginButton } from "@/components/common/auth";
import { LoginBackground } from "@/components/common/login-background";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import {
Card,
Expand All @@ -27,8 +28,9 @@ export default async function LoginPage({
if (session) return redirect("/");

return (
<main className="flex min-h-screen items-center justify-center">
<Card className="w-96">
<main className="flex min-h-screen items-center justify-center overflow-hidden">
<LoginBackground />
<Card className="z-50 w-96">
<CardHeader>
<CardTitle>Login</CardTitle>
<CardDescription>
Expand Down
22 changes: 22 additions & 0 deletions src/components/common/login-background.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"use client";

import AnimatedGridPattern from "@/components/ui/animated-grid-pattern";
import { cn } from "@/lib/utils";

export function LoginBackground() {
return (
<div className="absolute h-screen w-screen overflow-hidden">
<AnimatedGridPattern
maxOpacity={0.3}
duration={3}
numSquares={200}
repeatDelay={1}
className={cn(
"[mask-image:radial-gradient(500px_circle_at_center,white,transparent)]",
"lg:[mask-image:radial-gradient(800px_circle_at_center,white,transparent)]",
"absolute -inset-y-[40%] inset-x-0 h-[200%]",
)}
/>
</div>
);
}
184 changes: 184 additions & 0 deletions src/components/ui/animated-grid-pattern.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
"use client";

import { useEffect, useId, useRef, useState } from "react";
import { motion, type Variants } from "framer-motion";

import { cn } from "@/lib/utils";

interface GridPatternProps {
width?: number;
height?: number;
x?: number;
y?: number;
strokeDasharray?: string | number;
numSquares?: number;
className?: string;
maxOpacity?: number;
duration?: number;
repeatDelay?: number;
}

const variants: Variants = {
start: {
skew: 12,
transition: {
duration: 24,
ease: "easeIn",
repeat: Infinity,
repeatType: "reverse",
},
},
end: {
skew: -12,
transition: {
duration: 24,
ease: "easeOut",
repeat: Infinity,
repeatType: "reverse",
},
},
};

export function AnimatedGridPattern({
width = 40,
height = 40,
x = -1,
y = -1,
strokeDasharray = 0,
numSquares = 50,
className,
maxOpacity = 0.5,
duration = 4,
repeatDelay = 0.5,
...props
}: GridPatternProps) {
const id = useId();
const containerRef = useRef<SVGSVGElement>(null);
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
const [squares, setSquares] = useState(() => generateSquares(numSquares));
const [isClient, setIsClient] = useState(false);

function getPos() {
return [
Math.floor((Math.random() * dimensions.width) / width),
Math.floor((Math.random() * dimensions.height) / height),
];
}

// Adjust the generateSquares function to return objects with an id, x, and y
function generateSquares(count: number) {
return Array.from({ length: count }, (_, i) => ({
id: i,
pos: getPos(),
}));
}

useEffect(() => {
setIsClient(true);
}, []);

// Function to update a single square's position
const updateSquarePosition = (id: number) => {
setSquares((currentSquares) =>
currentSquares.map((sq) =>
sq.id === id
? {
...sq,
pos: getPos(),
}
: sq,
),
);
};

// Update squares to animate in
useEffect(() => {
if (dimensions.width && dimensions.height) {
setSquares(generateSquares(numSquares));
}

// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dimensions, numSquares]);

// Resize event listener to update container dimensions
useEffect(() => {
const current = containerRef.current;

if (!current) return;

const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
setDimensions({
width: entry.contentRect.width,
height: entry.contentRect.height,
});
}
});

resizeObserver.observe(current);

return () => {
resizeObserver.unobserve(current);
};
}, [containerRef, isClient]);

if (!isClient) return null;

return (
<motion.svg
ref={containerRef}
aria-hidden="true"
className={cn(
"pointer-events-none absolute inset-0 h-full w-full fill-gray-400/30 stroke-gray-400/30",
className,
)}
{...props}
initial="start"
animate="end"
variants={variants}
>
<defs>
<pattern
id={id}
width={width}
height={height}
patternUnits="userSpaceOnUse"
x={x}
y={y}
>
<path
d={`M.5 ${height}V.5H${width}`}
fill="none"
strokeDasharray={strokeDasharray}
/>
</pattern>
</defs>
<rect width="100%" height="100%" fill={`url(#${id})`} />
<svg x={x} y={y} className="overflow-visible">
{squares.map(({ pos: [x, y], id }, index) => (
<motion.rect
initial={{ opacity: 0 }}
animate={{ opacity: maxOpacity }}
transition={{
duration,
repeat: 1,
delay: index * 0.1,
repeatType: "reverse",
repeatDelay,
}}
onAnimationComplete={() => updateSquarePosition(id)}
key={`${x}-${y}-${index}`}
width={width - 1}
height={height - 1}
x={x ? x * width + 1 : 0}
y={y ? y * height + 1 : 0}
fill="currentColor"
strokeWidth="0"
/>
))}
</svg>
</motion.svg>
);
}

export default AnimatedGridPattern;

0 comments on commit 231c522

Please sign in to comment.